Idempotency keys, and why we made them mandatory
Every write endpoint now requires one. It is a small imposition that eliminates an entire category of support ticket.
As of this month, every endpoint on the gateway that creates or charges something requires an Idempotency-Key header. A request without one is rejected. This is a breaking change, we gave ninety days of notice, and we would do it again.
The ticket that prompted it
A customer's payment retried on a timeout. The original request had succeeded; the response was lost on the way back. Their code, correctly and sensibly, retried. Two charges.
Finding this took two engineers most of a day across two companies, because from their logs the call failed and from ours it succeeded. Neither party was wrong. The protocol simply has no way to distinguish "did not happen" from "happened, and you did not hear about it."
What an idempotency key does
The client generates a unique value per logical operation — a UUID is fine — and sends it with the request. We store the result against that key for twenty-four hours. A repeat with the same key returns the stored result rather than performing the operation again.
The retry becomes safe. The client cannot tell whether it was the first attempt or the fourth, which is the point.
Why mandatory rather than optional
We shipped it as optional first. Adoption after six months was under fifteen per cent, and the accounts that adopted it were the ones who had already been bitten. Everyone else was one lost response away from the same afternoon.
An optional safety mechanism protects the people who already know they need it. Making it required means the developer meets it while writing the integration, when it costs thirty seconds, rather than during an incident.
The details that matter
The key must be unique per operation, not per request — that is exactly what makes the retry work. Do not derive it from the request body, or two genuinely separate identical charges collapse into one. Generate it when the operation begins and reuse it for every attempt.
We return the original status code on a replay, plus an Idempotent-Replay header so you can tell. And if the same key arrives with a different body, that is a client bug and we return 422 rather than guessing which one you meant.