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