Idempotency
A header that turns “I don't know if that request went through” into a question you can answer by asking again.
The problem it solves
You POST a payment. The connection drops before the response arrives. You now know one of two things happened, and you cannot tell which:
- the request never landed, and nobody was charged
- the request landed, the customer got a prompt, and only the reply was lost
Retrying is either necessary or catastrophic, with no way to tell from your side. That is not a problem you can solve with a timeout value.
The fix
Send an Idempotency-Key header. Send the same key when you retry.
POST /v1/payments
Authorization: Bearer xpay_live_…
Idempotency-Key: order-1043
Content-Type: application/json
{ "amount": 20000, "msisdn": "0788924941" }The first request creates a payment. Every subsequent request carrying that key returns the same payment, with the same id and the same status, and does not talk to the network at all. Retrying becomes free, and is now the correct thing to do.
A replay is a normal response
succeeded payment, which is exactly what you wanted to know.Choosing a key
| Example | Why | |
|---|---|---|
| Good | order-1043 | Derived from the thing being paid for. Regenerating it after a crash produces the same key. |
| Good | invoice-2026-09-4417 | Unique, stable, and readable in a log at 2am. |
| Bad | uuid4() | A fresh key on every attempt. This is the same as sending no key at all, with extra steps. |
| Bad | user-42 | Not unique per payment. The customer's second order silently returns their first one. |
The test
Scope and lifetime
| Scoped to | One app. The same key in a test app and a live app are two different keys. |
| Lives | Forever. There is no expiry window to reason about. |
| Applies to | Cash in, cash out, and refunds. Everything that creates money movement. |
| Enforced by | A unique index in the database, not a check in the handler. |
The last line matters more than it looks. A handler that reads, decides and writes can be beaten by another handler doing the same thing a millisecond later on a different server. A unique constraint cannot: one of the two writes loses, and the loser is answered with the winner. Two identical requests arriving at the same instant produce one payment.
If you send no key
Nothing breaks and nothing is refused. You simply have no replay protection, and a retry after a timeout may create a second payment and prompt your customer twice. That is your call to make, and we record that it was made. For anything that moves money, send the key.
It is not the same as a reference
| Idempotency-Key | reference | |
|---|---|---|
| Where | HTTP header | Request body |
| For | Deduplicating retries of one request | Joining our record to yours |
| Unique | Enforced | Not enforced, yours to manage |
| Returned | No | Yes, on the payment and every webhook |
They often hold the same string, and they do different jobs. Send both.
Related: webhooks are delivered at least once. Idempotency is the same discipline applied to the inbound direction.