Audience: Backend, Security, DBAs
βOutcomes: No tampering, no duplicates, predictable reconciliation
Stripe (raw body + signature)
app.post('/api/webhooks/stripe', express.raw({type:'application/json'}),(req,res)=>{ try{ const sig=req.header('Stripe-Signature')!; const evt=stripe.webhooks.constructEvent(req.body,sig,process.env.STRIPE_WEBHOOK_SECRET!); if(hasProcessed(evt.id)) return res.json({received:true}); handleStripeEvent(evt); rememberProcessed(evt.id); res.json({received:true}); }catch{ res.status(400).send('Invalid signature'); } });
HMAC (bank/open banking)
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).digest('hex'); if(!crypto.timingSafeEqual(Buffer.from(mac),Buffer.from(sig))) return res.status(400).send('Bad signature'); const evt=JSON.parse((req.body as Buffer).toString()); if(hasProcessed(evt.id)) return res.json({received:true}); handleBankEvent(evt); rememberProcessed(evt.id); res.json({received:true}); });
Idempotency table (Postgres)
CREATE TABLE webhook_events_processed ( id TEXT PRIMARY KEY, received_at TIMESTAMPTZ DEFAULT now() );
Rate limiting & abuse
import rateLimit from 'express-rate-limit'; app.use('/api/', rateLimit({ windowMs:60_000, max:100, standardHeaders:true, legacyHeaders:false }));
QA checklist
Tampered payload β 400; duplicate β processed once.
Flood test: system stays consistent; no double releases.
Runbook: webhook flood
Throttle workers; verify indexes; ensure dedupe; drain queue; restore limits.