Webhooks are how the internet talks to itself — Stripe fires one when a payment completes, GitHub when a PR merges, Twilio when a message arrives. Building a receiver that's fast, secure and never drops an event requires more than a simple POST handler. This guide covers the full pattern.
Step 1 — Always verify the signature
Every serious webhook provider signs its payloads with an HMAC-SHA256 signature. Your receiver must verify this before doing anything else — an unverified handler is an open door for replay attacks and forged events.
import Stripe from "stripe";const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!); export async function POST(req: Request) { const body = await req.text(); // raw body — required for signature check const sig = req.headers.get("stripe-signature")!; let event: Stripe.Event; try { event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!); } catch { return new Response("Bad signature", { status: 400 }); } await queue.add("stripe-event", event); // hand off immediately return new Response("ok");}Step 2 — Respond fast, process async
Webhook providers (Stripe, GitHub, etc.) expect a 2xx response within 5–30 seconds or they retry. Heavy processing — sending emails, updating databases, calling other APIs — should happen in a background queue, not in the handler itself.
Step 3 — Idempotent processing
Every webhook can be delivered more than once (network blips, provider retries). Your processor must be idempotent — running it twice on the same event must have the same result as running it once. The simplest pattern: store the event ID and skip if already seen.
export async function handleStripeEvent(event: Stripe.Event) { const exists = await db.processedEvent.findUnique({ where: { id: event.id } }); if (exists) return; // already handled — safe to skip await db.processedEvent.create({ data: { id: event.id } }); if (event.type === "checkout.session.completed") { const session = event.data.object as Stripe.Checkout.Session; await activateSubscription(session.metadata!.orgId); }}Step 4 — Handle retries gracefully
When your handler returns a non-2xx, providers retry with exponential backoff. Configure your queue with the same pattern for downstream failures — up to 3 retries with delays, then move to a dead-letter queue for manual inspection.
< 200ms
Handler response time
event_id
Idempotency key
3×
Retry attempts
DLQ
Failed events
Need this built? Explore our API Development & Integrations service.
View service →Written by Zahid Ghotia · Published 5 June 2026 · 7 min read


