9 min readpaymentsapi-design
Idempotency is a product decision
Retry semantics look like an infrastructure detail until the second charge lands. Someone has to decide what "the same request" means, and it is not the database.
Retry semantics look like an infrastructure detail right up until the second charge lands in someone's account. Then it becomes a product question, asked loudly, usually by someone in support.
"The same request" is a business definition
An idempotency key is a promise that two calls describe one intention. Nothing about the transport can tell you where that boundary sits. Is a payment with the same amount, to the same payee, four seconds apart, one intention or two? The database cannot answer that. The person who owns refunds can.
Keys belong to the caller
If you generate the key server-side you have not built idempotency, you have built deduplication with a guess in the middle. The client knows when it is retrying. Make it say so.
Store the response, not just the fact
The second call has to get the same answer as the first — same body, same status, same identifiers. Recording only that a key was seen turns a retry into a different kind of failure, and it is a worse one, because now the client believes something happened that it cannot name.
Expiry is a policy, not a TTL
Twenty-four hours is the number everyone picks. It is fine, and it is arbitrary. What matters is that the window is longer than your longest client-side retry schedule, and that it is written somewhere a support engineer can find it at 3am.