WritingStripe Idempotency Keys: How They Work and When You Must Use Them — Clixo
5 min readstripe, idempotency, payments, backend, reliability

Stripe Idempotency Keys: How They Work and When You Must Use Them

A deep dive into Stripe idempotency keys — how they prevent duplicate charges, when to generate them, and how to build retry-safe payment logic in production.

Network timeouts happen. Servers crash mid-request. Retry logic fires twice. In payment systems, any of these events can create a duplicate charge — a customer billed twice for a single transaction. Stripe's idempotency key mechanism is the tool designed to prevent this, but it is also one of the least understood parts of the API. Here is how it actually works, and where most teams get it wrong.

What Stripe Idempotency Keys Are and Why They Exist

An idempotency key is a unique string you attach to a Stripe API request. If Stripe receives two requests with the same key for the same endpoint within a 24-hour window, it returns the result of the first request rather than processing a second operation. No duplicate charge. No duplicate subscription. No duplicate payout.

This is not Stripe being clever — it is a foundational property of safe distributed systems. Any operation that moves money should be idempotent: running it multiple times should produce the same outcome as running it once.

How Stripe Idempotency Keys Work in Practice

Pass the key as a header on any mutating request:

const charge = await stripe.paymentIntents.create(
  {
    amount: 4900,
    currency: "usd",
    customer: customerId,
    payment_method: paymentMethodId,
    confirm: true,
  },
  {
    idempotencyKey: "order_abc123_attempt_1",
  }
);

If your server crashes after Stripe processes the request but before you receive the response, you can safely retry with the same key. Stripe will return the original PaymentIntent object without charging the customer again.

Stripe Idempotency Keys: Rules and Constraints

Keys are scoped to an endpoint and Stripe account. The same key used on paymentIntents.create and customers.create are independent — there is no collision.

Keys expire after 24 hours. After expiry, a new request with the same key is treated as a fresh operation. Design your retry windows to stay within the 24-hour boundary.

If the request parameters change, Stripe returns a 400. Idempotency keys are not a cache-busting mechanism. If you send a key with amount: 4900 and retry with amount: 9900, Stripe will reject the second request. The key is tied to the exact parameters of the original call.

Only use keys on mutating operations. GET requests are already idempotent by nature. Applying keys to read operations has no effect.

How to Generate Idempotency Keys

The key needs to be deterministic — derived from the business operation, not randomly generated at retry time. If you generate a new random key on each retry attempt, you lose the protection entirely.

Good approaches:

  • order_${orderId}_payment — ties the key to a specific order record
  • subscription_${userId}_${planId}_${timestamp_rounded_to_day} — ties to a specific user/plan combination within a day
  • A UUID generated once per checkout attempt and stored in your session

Bad approaches:

  • uuid() called inside a retry loop — generates a new key on every attempt, providing zero protection
  • The customer's email — not unique enough; two payments from the same customer break the constraint
  • A timestamp — insufficiently unique and not tied to the business operation

Where Teams Get Idempotency Wrong

Relying on Stripe alone for idempotency. Stripe's key only protects against duplicate Stripe API calls. If your webhook handler runs twice, Stripe does not know about that — you need a separate idempotency mechanism on your own database for event processing.

Not storing the key before calling Stripe. If you generate the key, call Stripe, and then store the key only on success, a crash between the API call and the storage leaves you with no way to recover safely. Generate and persist the key before making the API call.

Treating the 24-hour window as unlimited. For long-running operations — for example, a scheduled retry loop that runs over multiple days — you need to generate a new key per day or per attempt batch.

Idempotency Keys and Webhook Deduplication Are Different Problems

A common confusion: teams implement Stripe idempotency keys and assume they have solved duplicate webhook processing. These are separate problems.

  • Idempotency keys prevent duplicate Stripe API calls from your server to Stripe.
  • Webhook event deduplication prevents duplicate processing when Stripe delivers the same event more than once.

You need both. The webhook deduplication layer belongs in your own database, typically as a processed_events table with a unique constraint on the Stripe event ID.

A Simple Mental Model

Think of an idempotency key as a receipt number. If you submit the same operation twice with the same receipt number, the cashier hands you the original receipt rather than ringing up a second transaction. The underlying state does not change. Your receipt is always the same.

Build your key generation to be deterministic, derive it from your own business identifiers, and store it before making the API call. That covers the vast majority of production edge cases.

For payment systems where duplicate charges or missed fulfillment would directly impact revenue, correctness is not optional. Start a build with Clixo to get the full architecture right from day one.