Idempotency in payment flows: retries, webhooks, and double-charge prevention

Innorise Engineering · 15-02-2026 · 6 min read

The problem

A customer clicks "Pay". The request times out. They click again. Or the provider's webhook fires twice. Without idempotency, you've charged them twice.

The pattern: idempotency keys

Every payment attempt gets a client-generated key (UUID). The provider (Stripe, PayPal, etc.) deduplicates on that key.

// Client side
const idempotencyKey = crypto.randomUUID();
await fetch('/api/payments', {
  method: 'POST',
  headers: { 'Idempotency-Key': idempotencyKey },
  body: JSON.stringify({ amount, currency, ... })
});

Webhook deduplication

Webhooks can fire multiple times for the same event. Store the event ID (e.g., Stripe's event.id) and process only once.

const processed = await db.eventProcessed.findUnique({ where: { eventId } });
if (processed) return 200; // Acknowledge, no-op
await processEvent(event);
await db.eventProcessed.create({ data: { eventId } });

Idempotent gateway abstraction

We wrap all providers behind an internal gateway that enforces idempotency keys on outbound calls and deduplicates inbound webhooks. The booking logic never sees a duplicate charge.


Have a system like this to build?

We architect and ship platforms where correctness and automation matter.

Start a project