Skip to content
kaleemsajid.com
← All posts
· 1 min read

Designing idempotent webhooks that never double-charge

architecture payments reliability

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.