Idempotency
Every state-changing gateway request takes an Idempotency-Key header, and on
POST /v1/charges it is required.
Idempotency-Key: 7c9e6679-7425-40de-944b-e07fc1f90ae7The problem it solves
Section titled “The problem it solves”A request can time out after we have already done the work. Your connection drops, your load balancer gives up, your process is killed mid-flight — and the charge exists. From your side the two cases are indistinguishable: you got no response.
With no idempotency key, your only options are to retry (and risk charging twice) or not to retry (and risk silently losing the payment). Neither is acceptable when the subject is money.
With a key, a retry is a replay. The second request returns the first request’s response — the same transaction id, the same status, the same everything — and no second charge happens.
How to use it
Section titled “How to use it”One key per logical operation, not per HTTP attempt. Generate it when you decide to take a payment, store it alongside your order, and send the same value on every attempt at that payment.
order 1043 → key 7c9e6679-… attempt 1: timeout attempt 2: same key → replay attempt 3: same key → replayA new payment for the same order later — a customer retrying after a genuine failure — is a new logical operation and gets a new key. If you reuse the key, you will be replayed the old failure instead of making a new attempt.
Use a UUIDv4 or anything else with enough entropy to be unique. Do not use your order id: two payment attempts on one order need two keys.
What a replay returns
Section titled “What a replay returns”The original response, including its status code. A replayed 201 is a 201. You
do not get a special “this was a replay” status, because your code should not need
to branch on it — that is the point.
Claiming a key is a conditional insert, so two concurrent requests with one key produce exactly one winner. The loser does not create a second charge.
The two errors that mean your client has a bug
Section titled “The two errors that mean your client has a bug”idempotency_key_reused (409)
Section titled “idempotency_key_reused (409)”The same key arrived with a different request body.
This is not a transient condition and retrying will not help. It means two different operations are sharing a key — most often because the key was derived from something insufficiently unique, like an order id or a timestamp truncated to the second.
We return an error rather than replaying the first response, which is the
important design choice here. Replaying would hand you a 201 describing a charge
for an amount you did not ask for, and you would have no way to notice. Being told
is worse in the moment and far better overall.
request_in_progress (409)
Section titled “request_in_progress (409)”A concurrent request is holding this key right now. The first one has not finished.
Retry shortly — a few hundred milliseconds — and you will get either the real response or a replay of it. Do not generate a new key and resend: that is how you turn one payment into two.
What not to do
Section titled “What not to do”Do not use idempotency as your only safeguard against duplicates in your own system. It protects the boundary between you and us. If your own code can decide to take the same payment twice, it will, and it will correctly generate two keys while doing it.
Reads do not need it
Section titled “Reads do not need it”GET requests change nothing, so they take no key. Neither does creating a
webhook endpoint — registering the same URL twice in one environment is a 409
rather than a second endpoint, which is its own form of idempotency. If a create
there times out, list your endpoints: if it is present, rotate its secret rather
than creating it again.

