Skip to main content

Verification & Exactly-Once Processing

Tamper-proof handlers for Stripe and HMAC providers; dedupe safely with SQL.

C
Written by Catalin Fetean
Updated over 2 weeks ago

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.

Did this answer your question?