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

Network failures happen. Here's how we design payment integrations so retries and webhook duplicates can never double-charge a customer.

Innorise Engineering15-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?

Tell us what you're building. We'll review scope, architecture, and where AI fits, and reply within one business day.