What Are Webhooks? A Developer's Guide

A comprehensive introduction to webhooks — what they are, how they work, when to use them, and how to implement them securely. Everything developers need to know.

Webhooks are one of the most common patterns in modern API development, yet they're often misunderstood. This guide covers everything you need to know — from the basics to production best practices.

Webhooks in a Nutshell

A webhook is an HTTP callback: when something happens in one system, it automatically sends an HTTP request to a URL you've configured in advance.

Think of it as the difference between:

  • Polling (you ask): "Did anything happen?" → "No." → "How about now?" → "No." → "Now?" → "Yes!"
  • Webhooks (they tell you): "Hey, something just happened. Here are the details."

How Webhooks Work

┌──────────┐    1. Register URL    ┌──────────────┐
│  Your    │ ──────────────────── │  Webhook     │
│  Server  │                      │  Provider    │
│          │ ◄─────────────────── │  (e.g.       │
│          │    2. Event occurs,   │   Stripe)    │
│          │       POST to URL     │              │
└──────────┘                      └──────────────┘
  1. You register a URL with the webhook provider (e.g., https://api.example.com/webhooks/stripe)
  2. An event occurs in the provider's system (e.g., a payment succeeds)
  3. The provider sends an HTTP POST request to your URL with event data
  4. Your server processes the event and returns a 2xx status code
  5. If delivery fails, the provider typically retries with exponential backoff

Webhooks vs. Polling vs. WebSockets

WebhooksPollingWebSockets
DirectionServer → Your ServerYour Server → ServerBidirectional
EfficiencyHigh (event-driven)Low (repeated requests)High (persistent)
Real-timeNear real-timeDepends on intervalReal-time
ComplexityMediumLowHigh
Use caseEvent notificationsSimple data fetchingLive interactions

When to Use Webhooks

  • Reacting to events in third-party services (payments, deployments, form submissions)
  • Syncing data between systems
  • Triggering workflows based on external events
  • Any scenario where polling would waste resources

When NOT to Use Webhooks

  • You need sub-second latency (use WebSockets)
  • The data changes very frequently (use streaming)
  • You control both systems (consider message queues)

Common Webhook Providers

Almost every modern SaaS app sends webhooks:

ProviderCommon Events
Stripepayment_intent.succeeded, customer.created, invoice.paid
GitHubpush, pull_request, issues, release
Shopifyorders/create, products/update, app/uninstalled
Twiliomessage.received, call.completed
Slackmessage, reaction_added, channel_created
SendGriddelivered, opened, bounced

Implementing a Webhook Endpoint

Here's a basic webhook endpoint in different frameworks:

Node.js (Express)

import express from 'express'

const app = express()

// Important: use raw body for signature verification
app.post('/webhooks/stripe', 
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const event = JSON.parse(req.body.toString())
    
    switch (event.type) {
      case 'payment_intent.succeeded':
        console.log('Payment succeeded:', event.data.object.id)
        break
      case 'customer.created':
        console.log('New customer:', event.data.object.email)
        break
      default:
        console.log('Unhandled event type:', event.type)
    }
    
    res.status(200).json({ received: true })
  }
)

Python (FastAPI)

from fastapi import FastAPI, Request

app = FastAPI()

@app.post("/webhooks/stripe")
async def handle_webhook(request: Request):
    payload = await request.json()
    
    match payload["type"]:
        case "payment_intent.succeeded":
            print(f"Payment succeeded: {payload['data']['object']['id']}")
        case "customer.created":
            print(f"New customer: {payload['data']['object']['email']}")
        case _:
            print(f"Unhandled: {payload['type']}")
    
    return {"received": True}

Nuxt.js (API Route)

export default defineEventHandler(async (event) => {
  const body = await readBody(event)
  
  switch (body.type) {
    case 'payment_intent.succeeded':
      console.log('Payment succeeded:', body.data.object.id)
      break
    case 'customer.created':
      console.log('New customer:', body.data.object.email)
      break
  }
  
  return { received: true }
})

Webhook Payload Anatomy

A typical webhook payload looks like:

{
  "id": "evt_1234567890",
  "type": "payment_intent.succeeded",
  "created": 1708300800,
  "data": {
    "object": {
      "id": "pi_abc123",
      "amount": 2000,
      "currency": "usd",
      "status": "succeeded",
      "customer": "cus_xyz789"
    }
  }
}

Key elements:

  • id — Unique event identifier (use for idempotency)
  • type — What happened (use for routing)
  • created — When the event occurred (use for ordering and replay detection)
  • data — The actual event payload

Production Best Practices

1. Verify Signatures

Always verify that the webhook came from the expected sender:

import crypto from 'crypto'

function verifySignature(payload: string, signature: string, secret: string): boolean {
  const expected = crypto.createHmac('sha256', secret).update(payload).digest('hex')
  return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
}

2. Respond Quickly

Return 200 OK immediately, then process asynchronously:

app.post('/webhooks', async (req, res) => {
  res.status(200).send('OK')  // Respond immediately
  await queue.add(req.body)    // Process later
})

3. Handle Duplicates

Webhooks can be delivered more than once. Track processed event IDs:

const processed = new Set()

function handleWebhook(event) {
  if (processed.has(event.id)) return
  processed.add(event.id)
  // ... handle event
}

4. Handle Failures Gracefully

If your endpoint returns a non-2xx status, most providers will retry. Make sure:

  • Transient errors (DB timeout, network issue) return 5xx → provider retries
  • Permanent errors (invalid payload) return 4xx → provider stops retrying

5. Use a Webhook Gateway

Instead of building all this infrastructure yourself, use a webhook gateway like Mitte that provides:

  • Automatic retries with configurable policies
  • Real-time delivery logs with full request/response data
  • HMAC signature verification
  • Payload transforms
  • AI-powered error analysis

Testing Webhooks

Testing webhooks is uniquely challenging because you need a publicly accessible URL during development.

Options:

  1. Webhook gateway (Mitte) — Persistent URL with full logging
  2. Tunneling (ngrok) — Expose localhost temporarily
  3. Provider CLI (Stripe CLI) — Provider-specific testing tools
  4. Unit tests — Mock the webhook payload locally

Read our detailed guide: How to Test Webhooks Locally →

Summary

Webhooks are the backbone of event-driven integrations. To use them effectively:

  1. ✅ Register your URL with the provider
  2. ✅ Verify signatures on every request
  3. ✅ Return 200 quickly, process async
  4. ✅ Handle duplicates with idempotency keys
  5. ✅ Use a webhook gateway for production reliability

Ready to receive webhooks the right way? Create your first Mitte endpoint for free →