case-study

Stripe: Idempotency Keys as a Public API Contract

also called Idempotency-Key Header

Stripe made safe retry a documented, client-controlled property of its API, which is why network failures during payments do not produce duplicate charges.

stripepaymentsapi-designreliability

The problem

A client that sends a payment request and receives a network timeout is in an unresolvable position: the request may have succeeded and the response been lost, or it may never have arrived. Retrying risks charging twice; not retrying risks not charging at all. Neither is acceptable, and no amount of client-side cleverness resolves it.

This is not an edge case. At payment volume, network failures during the response are a routine occurrence.

What they did

Stripe exposes an Idempotency-Key header on mutating requests. The client generates a key per logical operation; Stripe stores the result against that key and, on a repeat, returns the original response rather than performing the operation again. Keys expire after a documented period.

Two details make it a genuine contract rather than a convenience. The result is returned, not merely the duplicate rejected — so the client learns the outcome. And it is documented, versioned and guaranteed, so integrators can build retry logic against it with confidence.

The failure mode that persists

The most common defect is on the client side: generating a new key on each retry attempt. If the key is created inside the retry loop, or regenerated when the HTTP request is constructed, every attempt is a distinct operation and the mechanism provides nothing.

The key must be generated when the operation is created — when the user commits to paying — and persisted with it, so that every attempt, including after an application restart, carries the same value. Deriving it deterministically from a stable business identifier is more robust still.

The trade-off

The server must store keys with their results for the retention window, which is a table that grows with request volume and needs pruning. Concurrent requests with the same key need a lock or a unique constraint. And the key should be bound to the request payload, so a client reusing a key with different parameters receives an error rather than the wrong cached response.

The transferable lesson

Any endpoint that moves money, sends a message, provisions a resource or creates a record should expose idempotency. It converts a whole class of duplicate-action incidents into a non-event, and it is dramatically cheaper to design in than to retrofit once duplicates are in production data.