Concept · Networking & Delivery

Webhooks

01

Why this matters

Stripe processes a payment. Your app needs to know when it succeeds. Two options:

  • Polling: your app calls Stripe every 30 seconds asking "did payment X succeed yet?" Wasteful at scale (millions of polls/sec for events that happen rarely). High latency to learn (up to 30s).
  • Webhook: Stripe POSTs to your URL (https://your-app.com/webhooks/stripe) the moment the payment completes. Sub-second notification, zero waste.

Webhooks are the standard async-callback pattern across the modern API ecosystem — Stripe, GitHub, Slack, Twilio, every webhook-capable SaaS. Building a reliable webhook system is harder than it looks.

02

The shape of a good webhook

A typical webhook POST:

POST /webhooks/stripe HTTP/1.1
Host: your-app.com
Stripe-Signature: t=1700000000,v1=abc123...
Content-Type: application/json

{
  "id": "evt_3OABC123",
  "type": "payment_intent.succeeded",
  "created": 1700000000,
  "data": { "object": { ... } }
}
  • Stable URL per integration. Configurable in the sender's dashboard.
  • HMAC signature header proves the sender. Receiver verifies with shared secret. Without this, anyone can forge events to your endpoint.
  • Event ID for idempotency — sender may retry; receiver must dedupe.
  • Event type dispatches to handlers.
  • Timestamp + signature window prevents replay (reject events older than 5 min).
Webhook Receiver — Signature First, Then Dedup, Then ACK Mermaid
sequenceDiagram participant S as Sender (e.g. Stripe) participant R as Receiver (your app) participant DB as Idempotency Table participant Q as Worker Queue S->>R: POST /webhooks/stripe
headers: Stripe-Signature, body: {event} R->>R: 1. Verify HMAC signature (raw body) Note over R: Reject 401 if invalid R->>R: 2. Verify timestamp within 5 min Note over R: Reject 401 if stale (replay) R->>DB: 3. INSERT event_id (idempotency check) alt First time DB-->>R: ok R->>Q: enqueue for processing R-->>S: 200 OK (within 1s) else Already seen DB-->>R: duplicate key R-->>S: 200 OK (no-op) end Note over Q: Worker processes async — never block the ACK
3 days
Stripe webhook retry window
24 hr
GitHub webhook retry window
5 min
signature replay window
< 1 s
target ACK time
03

Receiver-side rules

  • Verify the signature first. Reject unsigned or stale events with 401. Anyone can hit your URL.
  • Persist before processing. Insert event_id into a "processed" table. Idempotency built in. Subsequent retries dedupe instantly.
  • Return 200 fast. Senders typically retry on non-2xx OR on timeout (often 10-30s). Long processing → ack quickly, push real work to a queue.
  • Don't crash on unknown event types. Senders add new types regularly. Log + ack 200 instead of 500.
  • Be ready for out-of-order delivery. Network = no order guarantees. Reorder by timestamp if the order matters.
04

Sender-side reliability

You're Stripe. Your customer's webhook endpoint returns 500 because their server is down. What do you do?

  1. Retry with exponential backoff. Most webhook senders retry for 24-72 hours on failure. Backoff with jitter prevents thundering-herd recovery.
  2. At-least-once delivery. Same event may arrive multiple times. Receiver dedupes via event_id (the contract).
  3. Per-endpoint queues — a slow customer endpoint shouldn't block others. Bulkhead one queue per destination.
  4. Dashboard for failures — show customers which events failed. Allow manual replay.
  5. Eventual stop. After N days of failure, stop retrying and notify the integration owner. Endless retries waste compute.
05

Deep dive — the security checklist

Webhooks are public HTTP endpoints. Security failures are common and severe. The mandatory checklist:

  • HMAC signature. Sender computes HMAC(secret, timestamp + body). Receiver re-computes and compares. Use constant-time comparison to prevent timing attacks.
  • Timestamp window. Reject events older than 5 minutes. Prevents replay of old captured events.
  • HTTPS only. Plain HTTP exposes the signature + body to network attackers.
  • IP allowlist (optional). Senders publish their IP ranges (Stripe, GitHub do); receivers can firewall to those. Defense in depth.
  • Rotate secrets. If the shared secret leaks, generate a new one. Allow both old + new for a transition window.
  • Don't trust the URL itself. Don't put the secret in the URL — it ends up in logs.
Common bug

"We checked the signature later, after parsing the body" → attacker can send malformed JSON that crashes your parser before signature check. Always: read raw body, verify signature, then parse.

06

Real-world

Stripe webhooks

Industry gold standard

HMAC signed, retried for 3 days, dashboard with failed-event replay. Pioneered idempotency-key + signature pattern.

GitHub webhooks

Push, PR, issues, releases

HMAC-SHA256 signed via X-Hub-Signature-256. Per-repo configurable; per-org for org events.

Twilio

SMS, voice, status callbacks

Validates via X-Twilio-Signature. Retries delivery on non-2xx. Standard webhook pattern.

Svix / Hookdeck

Webhook-as-a-service

SaaS that handles delivery, retries, signing for you. Used by APIs that don't want to build the reliability layer.

07

Used in problems

Payment gateway publishes webhooks for every charge/refund/dispute. E-commerce uses webhooks for order-state notifications. Notification system itself is fundamentally a webhook delivery problem.

Next up