Webhooks

A webhook is a way for one system to notify another that something happened, by making an HTTP request to a URL you provide. Instead of your app repeatedly asking a service "any updates yet?" (polling), the service calls you the moment an event occurs — "a payment succeeded," "a pull request was opened," "an email bounced." Webhooks are how the integrated web talks to itself: Stripe, GitHub, Slack, Shopify, and virtually every SaaS platform uses them to push events to your backend in real time.

They're simple in concept — "reverse API calls" — but building them reliably involves genuine subtleties: verifying the request really came from the sender, tolerating duplicate and out-of-order deliveries, responding fast enough not to time out, and handling the retries that failures trigger. Getting these right is the difference between a robust integration and one that silently double-charges customers or misses events.

TL;DR

How Webhooks Work

The flow when you receive a webhook:

  1. You register a URL with the sender (https://api.yourapp.com/webhooks/stripe).
  2. An event occurs on their side (a payment succeeds).
  3. They POST a JSON payload (plus a signature header) to your URL.
  4. You verify the signature, return 2xx fast, and process the event.
  5. If you don't return 2xx (error, timeout), they retry later.

Quick Example

Receiving and verifying a webhook (Node/Express — the signature check is non-negotiable):

Three things that make it correct: verify the signature (authenticity), dedupe by event ID (idempotency), enqueue and return 2xx immediately (don't time out).

Core Concepts (Receiving)

Signature Verification

Your webhook URL is public — anyone who discovers it can POST forged events. The defense: senders sign each payload with an HMAC using a shared secret, and you recompute and compare. A matching signature proves the payload came from someone holding the secret (the sender) and wasn't tampered with. Critical details:

Unverified webhooks are an open door — this is the API security fundamental for the pattern.

Idempotency

Webhook delivery is at-least-once: senders retry on any non-2xx or timeout, and networks duplicate requests, so the same event will eventually arrive twice. If processing isn't idempotent, you double-charge, double-ship, or double-email. Every event carries a unique ID — record processed IDs and skip repeats:

Respond Fast, Process Async

Senders enforce a short timeout (often a few seconds). If your handler does slow work (database writes, third-party calls) inline, you risk timing out — the sender marks it failed and retries, causing duplicates and load. The pattern: verify → enqueue → return 2xx immediately, and do the actual processing in a background job. Your endpoint's only synchronous job is to authenticate and accept.

Handle Retries and Missed Events

Because delivery isn't guaranteed exactly-once or in order:

Sending Webhooks

If you're the one emitting webhooks (building a platform), you owe consumers reliability and tooling:

This is real infrastructure; many teams use webhook-delivery services (Svix, Hookdeck) rather than building retry/signing/replay from scratch.

Common Mistakes

Not Verifying Signatures

Trusting any POST to your webhook URL lets attackers forge events — fake "payment succeeded" to unlock paid features, fake "user deleted" to cause havoc. Always verify the HMAC signature against the raw body. This is the most common and most dangerous webhook mistake.

Parsing Before Verifying

Frameworks that auto-parse JSON change the raw bytes, so the recomputed signature won't match. Capture the raw body and verify before parsing — a subtle bug that makes verification silently fail (or tempts developers to skip it).

Assuming Exactly-Once, In-Order Delivery

Webhooks are at-least-once and unordered. Code that assumes each event arrives exactly once, in sequence, breaks on the inevitable duplicate or reordered delivery. Dedupe by ID; derive state from payloads and timestamps, not arrival order.

Slow Synchronous Handlers

Doing heavy processing inside the webhook response causes timeouts → sender retries → duplicates and cascading load. Acknowledge immediately (2xx) and offload work to a background job.

No Fallback for Missed Events

Treating webhooks as a guaranteed delivery channel means silently losing data when your endpoint is down past the retry window. For anything critical (payments, orders), add reconciliation — periodically re-fetch state to catch missed events.

FAQ

Webhooks vs polling — which should I use?

Prefer webhooks for real-time notification without wasteful repeated requests — you're told the instant something happens. Use polling when the source doesn't offer webhooks, or as a reconciliation fallback for missed webhook events on critical data. The robust pattern is often webhooks for immediacy plus periodic polling as a safety net.

How do I secure a webhook endpoint?

Verify the HMAC signature against the raw body with constant-time comparison; check the timestamp to reject replays; use HTTPS; and treat the payload as untrusted input (validate it). Optionally restrict to the sender's known IP ranges. The signature is the core control — see API Security.

Why do I keep getting duplicate webhook events?

Because delivery is at-least-once — any timeout, non-2xx response, or network glitch causes the sender to retry, and you get the event again. This is expected, not a bug. Handle it with idempotency: dedupe on the event's unique ID so reprocessing is a no-op.

My webhook handler times out — what's wrong?

You're likely doing slow work (database writes, external API calls) inside the response. Senders enforce short timeouts; exceeding them triggers retries. Fix: verify and enqueue the event, return 2xx immediately, and process asynchronously in a background job.

How do I test webhooks locally?

Expose your local server to the internet with a tunneling tool (ngrok, Cloudflare Tunnel) so the sender can reach it, or use the sender's CLI (e.g. Stripe CLI) which forwards events to localhost and can trigger test events. Also build a way to replay captured payloads against your handler for repeatable testing.

Related Topics

References