Idempotency
Send an Idempotency-Key header on every request that creates something. It is one line of code, and it is the difference between a retry being safe and a retry being a double charge.
The problem it solves
The dangerous moment is not failure, it is uncertainty. A timeout tells you nothing about whether the request arrived. Without a key you have two bad options: retry and risk charging twice, or do not retry and risk losing a payment you may have already taken.
The key removes the dilemma. Retry freely; at most one payment exists.
Using it
Code
Generate the key once per operation, before the first attempt, and reuse the same value for every retry of that operation. A UUID is ideal.
Generating it inside your retry loop defeats the whole mechanism: each attempt looks like a new operation, and you are back to double charges.
What you get back
A replay returns the original response β same status, same body, same payment id β plus a header:
Code
Use it in logs when you are working out whether a spike was real traffic or a retry storm. Your business logic should not need it: the point is that a replay is indistinguishable from the first call.
The rules
Keys live 24 hours. Long enough for any sane retry policy. After that the key is forgotten and a request carrying it is treated as new.
Keys are scoped to your business. Yours cannot collide with another merchant's.
Same key, different body is an error. It means a bug on your side β two different operations sharing a key β and we would rather tell you than guess which one you meant. You get a 409.
Concurrent requests with the same key also 409. The first is still in flight. Retry after it settles; you will get the replay.
Where it applies
Any call that creates something and would be expensive to duplicate:
| Endpoint | Why |
|---|---|
POST /v1/payments | A duplicate debits the customer twice |
POST /v1/invoices | A duplicate sends two bills for one order |
POST /v1/payment-links | A duplicate leaves an orphan link live |
Reads (GET) are naturally idempotent and ignore the header. POST /v1/invoices/{id}/send is deliberately safe to repeat β resending is how you chase a late payment.
Things that surprise people
It is not deduplication of your business logic. Two genuinely different orders that happen to have the same amount and phone number are two payments. Only an identical key collapses them.
A 409 is a bug signal, not a transient error. Do not retry through it with a fresh key until you understand which two operations shared one.
24 hours is not a reconciliation window. For anything older, reconcile on your own external_reference and our payment id.
Next steps
- How a payment works β why a create call can time out mid-flight
- Payment states β making fulfilment exactly-once on your side too
- Create a payment β the header on the reference

