Skip to main content

Webhooks & Idempotency

Verify on raw bodies, dedupe in SQL, and withstand retries/floods.

C
Written by Catalin Fetean
Updated over 2 weeks ago

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.

Did this answer your question?