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

It answers with the original transaction as it stands now, not an error. So a retry that arrives after the customer has already approved returns a succeeded payment, which is exactly what you wanted to know.

Choosing a key

ExampleWhy
Goodorder-1043Derived from the thing being paid for. Regenerating it after a crash produces the same key.
Goodinvoice-2026-09-4417Unique, stable, and readable in a log at 2am.
Baduuid4()A fresh key on every attempt. This is the same as sending no key at all, with extra steps.
Baduser-42Not unique per payment. The customer's second order silently returns their first one.

The test

If your process died and restarted, would it compute the same key? Yes means the key is doing its job. No means it is decoration.

Scope and lifetime

Scoped toOne app. The same key in a test app and a live app are two different keys.
LivesForever. There is no expiry window to reason about.
Applies toCash in, cash out, and refunds. Everything that creates money movement.
Enforced byA 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-Keyreference
WhereHTTP headerRequest body
ForDeduplicating retries of one requestJoining our record to yours
UniqueEnforcedNot enforced, yours to manage
ReturnedNoYes, 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.