Audience: Backend Engineers, Security, DBAs
Outcomes: Durable, idempotent webhook pipeline
Stripe (Node/Express)
import express from 'express'; import Stripe from 'stripe'; const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!); app.post('/api/webhooks/stripe', express.raw({ type: 'application/json' }), (req, res) => { try { const sig = req.header('Stripe-Signature')!; const event = stripe.webhooks.constructEvent( req.body, sig, process.env.STRIPE_WEBHOOK_SECRET! ); if (hasProcessed(event.id)) return res.json({ received: true }); handleStripeEvent(event); // business logic rememberProcessed(event.id); // record idempotency res.json({ received: true }); } catch { res.status(400).send('Invalid signature'); } } );
Stripe (Python/Flask)
from flask import Flask, request, abort import stripe, os app = Flask(__name__) stripe.api_key = os.environ["STRIPE_SECRET_KEY"] @app.post("/api/webhooks/stripe") def stripe_webhook(): payload = request.get_data() # raw body sig = request.headers.get("Stripe-Signature") try: event = stripe.Webhook.construct_event( payload, sig, os.environ["STRIPE_WEBHOOK_SECRET"] ) except Exception: abort(400) if has_processed(event["id"]): return {"received": True} handle_stripe_event(event) remember_processed(event["id"]) return {"received": True}
HMAC providers (Node + Go)
import crypto from 'crypto'; app.post('/api/webhooks/bank', express.raw({ type: '*/*' }), (req, res) => { const sig = String(req.header('X-Hmac-Signature') || ''); const mac = crypto.createHmac('sha256', process.env.BANK_WEBHOOK_SECRET!) .update(req.body as Buffer).digest('hex'); const ok = crypto.timingSafeEqual(Buffer.from(mac), Buffer.from(sig)); if (!ok) return res.status(400).send('Bad signature'); const event = JSON.parse((req.body as Buffer).toString()); if (hasProcessed(event.id)) return res.json({ received: true }); handleBankEvent(event); rememberProcessed(event.id); res.json({ received: true }); });
func verifyHMAC(body []byte, sig, secret string) bool { mac := hmac.New(sha256.New, []byte(secret)) mac.Write(body) expected := hex.EncodeToString(mac.Sum(nil)) return subtle.ConstantTimeCompare([]byte(expected), []byte(sig)) == 1 }
Idempotency table (PostgreSQL)
CREATE TABLE webhook_events_processed ( id TEXT PRIMARY KEY, received_at TIMESTAMPTZ DEFAULT now() );
Handler pattern:
if (!tryInsert('webhook_events_processed', event.id)) return 200; // apply business changes (in txn), then commit
Common pitfalls
Parsing JSON before signature check
Using wrong secret (test vs prod)
Long work inside webhook thread (use queue/worker)
QA checklist
Tampered payload ⇒ 400
Replayed event processed once; second attempt returns 200 quickly
Runbook: “Webhook flood”
Throttle worker concurrency; ensure DB indexes; verify idempotency; drain queue.