Definition
The idempotency key is a unique identifier that a client sends with a mutating request (typically a POST) to ensure that, on a network retry, the operation will be executed only once.
The server stores the key; if the same request comes back, it returns the previous response without replaying the operation. It is a critical best practice for any payment API, where a double execution means a double payment, double billing or corrupted state. Stripe, Bridge, Adyen, Mollie, AWS and many others require or support it.
Why it is critical
On the network, uncertainty is the rule: a request can time out without your knowing whether it was received, processed, or whether a retry will duplicate the operation. Without an idempotency key, you cannot retry safely; with one, the client can retry with confidence, and the server returns the stored response if the operation has already happened.
How it works
Format and best practices
- Generation: UUID v4 (random), ULID (time-sortable, debug-friendly), or a deterministic business hash (
SHA256(user_id + transaction_ref)). - Header:
Idempotency-Key: 4a7f3b80-fe5c-4d12-9c7e-83b3a4ab9b1c. The IETF has published a draft, but the convention was adopted well before it was made official. - Cache duration: 24h at Stripe and Bridge, 1h at Mollie. Recommended: 24 to 48h for payments, 7 to 30 days for asynchronous flows. Beyond that, a retry would replay the operation.
- Scope: per merchant / per account (recommended), rather than global (collision risk).
Different payload, same key
If the same key is sent with a different payload, the server should reject the request (409 Conflict), as Stripe and AWS do. This prevents a payment €200 from being silently ignored after a payment €100 carrying the same key.
Use cases
- Payments:
POST /payments,/transfers,/charges,/refunds— always. - Resource creation:
POST /customers,/subscriptions— to avoid duplicates. - Expensive operations:
POST /kyc,/loans/disburse. - Unnecessary on:
GET(idempotent by nature),PUTwith an explicit ID,DELETE.
Idempotency-Key vs other techniques
| Technique | Guarantee | Complexity |
|---|---|---|
| Idempotency-Key (header) | Exactly-once per call | Low on the client side |
| Unique transaction token | Exactly-once per business transaction | Medium |
| Optimistic DB lock | Exactly-once per resource | Medium |
| Saga + commands | Exactly-once per flow | High |
The Idempotency-Key works at the API level: it does not protect against a business bug (a user who pays twice via two different buttons generates two distinct keys).
Server-side implementation
Typical storage: Redis (native TTL, key idempotency:{merchant}:{key}), DynamoDB for scalability, or PostgreSQL at low volume.
def handle_payment(req):
key = req.headers.get("Idempotency-Key")
if not key:
return create_payment(req)
cached = redis.get(f"idempotency:{merchant}:{key}")
if cached:
return cached
with redis.lock(f"idempotency-lock:{merchant}:{key}", ttl=30):
cached = redis.get(f"idempotency:{merchant}:{key}")
if cached:
return cached
response = create_payment(req)
redis.setex(f"idempotency:{merchant}:{key}", 86400, response)
return responseThe lock is necessary if two requests carrying the same key arrive at the same time (a retry race condition), to avoid a double execution in parallel.
Behavior on failure
- Client timeout, server success: retry with the same key → cached response (success).
- 5xx error: most providers replay (a 5xx is not considered final).
- 4xx error: retry with the same key → the cached 4xx response is returned.
What the Idempotency-Key is not
- Not a distributed transaction: it does not guarantee cross-service consistency.
- Not a business safeguard: two clicks on two buttons generate two keys.
- Not a replacement for authentication: it is complementary.
- Not required on GET / PUT: natively idempotent.
- Not (yet) a formal standard: an IETF draft, but a near-universal convention.
In the PSD2 / Open Finance ecosystem
- Berlin Group: recommends
X-Request-ID(equivalent). - STET: the
X-Request-IDheader. - OBIE UK:
x-idempotency-key. - Acquiring PSPs (Stripe, Adyen, Mollie): universally adopted.
- Webhooks: the key is also crucial on the receiving side, so as not to process the same event twice.
Concrete examples
- Stripe: the
Idempotency-Keyheader, a 24h cache, rejection of a different payload — the industry standard. - Bridge:
Idempotency-Key+X-Request-IDdepending on the endpoint. - Mollie:
Idempotency-Key, a 1h cache. - AWS: the
client-tokenparameter. - Real-world case: a merchant without an idempotency key retries after a timeout → double debit on 0.5% of payments → tens of thousands of euros in refunds and lost trust.
- UI pattern: generate the key on click and persist it in the form state — a re-click 5 seconds later reuses the same key, with no double charge.