Designing idempotent webhooks that never double-charge
Every payment integration eventually teaches you the same lesson: webhooks are delivered at least once, not exactly once. If your handler is not idempotent, a single retried event can double-charge a customer or double-credit an account.
Store what you have seen
The simplest reliable pattern is a dedupe table keyed by the provider’s event id:
async function handleEvent(evt: StripeEvent) {
if (await seen(evt.id)) return ack();
await tx(async (db) => {
await applyEffect(db, evt);
await markSeen(db, evt.id);
});
}
Marking the event seen inside the same transaction as the effect is the part most people miss — otherwise a crash between the two leaves you in an inconsistent state.
Make the effect itself idempotent
Where you can, design the downstream effect so applying it twice is a no-op anyway — upserts instead of inserts, set-this-value instead of increment-by-one. Defence in depth beats a single dedupe table.
That is most of it. Two small habits, and an entire class of billing bugs disappears.