# 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.

- **Published:** 2026-02-07
- **Author:** Clixo
- **Reading time:** 5 min read
- **Tags:** stripe, idempotency, payments, backend, reliability
- **Canonical URL:** https://clixo.sh/blog/stripe-idempotency-keys-deep-dive

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.

```mermaid
sequenceDiagram
  participant S as Your Server
  participant DB as Database
  participant A as Stripe API
  S->>DB: Generate and persist idempotency key
  S->>A: paymentIntents.create with key order_abc_payment
  Note over S,A: Network timeout — no response received
  S->>A: Retry with same key order_abc_payment
  A-->>S: Returns original PaymentIntent — no new charge
  S->>DB: Mark payment as succeeded
```

## 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](https://clixo.sh/#contact) with Clixo to get the full architecture right from day one.

---

Clixo · 1141 W Bryn Mawr Ave, Itasca, IL 60143, US · [hello@clixo.sh](mailto:hello@clixo.sh)
[Start a build](https://clixo.sh/#contact) · [All services](https://clixo.sh/services) · [Agent guide (llms.txt)](https://clixo.sh/llms.txt)
