payments · webhooks
Webhooks Must Be Idempotent or They Will Double-Charge Your Soul
Payment providers retry webhooks. Dedupe on payment ref or create duplicate orders.

Boring Team
September 7, 2026 · 1 min read
Every payment provider retries webhooks. Stripe replays for days; Dodo and LemonSqueezy behave the same. If your handler creates an order row per delivery, one payment becomes two licenses, two invites, two headaches.
The fix is one lookup before every write: Order.findByPaymentRef(paymentRef) — if it exists, return the duplicate and stop. All three Boring rails normalize to the same { action, userId, email, paymentRef } shape in verifyAndParse, and providers/billing/access.js dedupes before inserting. The database backs it with a unique partial/sparse index on providerPaymentRef, so even a race between two simultaneous deliveries collapses into one row.
Two more rules complete the pattern. Verify signatures first and return 4xx on failure (no work done, no retry storm), return 500 on processing failure (so the provider retries), and 200 only on success. And never trust amounts from the payload — pull amountCents from your own product row.
Exactly once is a property of the ledger, not a hope about the network.
Keep reading