WritingStripe Idempotency Keys: How They Work and When You Must Use Them — LinkIT
4 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.

A payment request can succeed even when the response never reaches your server. Reusing an idempotency key lets you retry that operation without treating the retry as a new purchase.

What Stripe Idempotency Keys Are and Why They Exist

An idempotency key identifies retries of one API request. For Stripe API v1, once execution begins, Stripe stores the response and returns it for matching retries, including stored server errors. See Stripe’s idempotency documentation.

How Stripe Idempotency Keys Work in Practice

Pass the key with a POST 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

Use a distinct key for each logical operation. Reuse it only for retries of the same request.

Keys may be pruned once they are at least 24 hours old. Reusing a pruned key starts a new request.

Keep request parameters unchanged on retry. Stripe rejects mismatched parameters for a retained key.

Keys apply to POST requests. They have no effect on GET or DELETE requests.

How to Generate Idempotency Keys

Generate a unique key once and persist it before sending the request. Reuse it for retries; a new random key on each attempt identifies a different operation.

Good approaches:

  • order_${orderId}_payment — for one defined payment operation on an order
  • A UUID generated once per operation and stored with its database record
  • A stable internal operation ID that contains no personal data

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.

Retrying after the retention window without reconciliation. Check the original operation’s outcome before attempting it again. Generating a fresh key each day can create duplicates.

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.

Persist the operation ID, request parameters, Stripe object ID, and retry state together so a worker can recover after a crash.

Discuss payment retry handling with LinkIT.