Skip to content

Idempotency

The header that stops a retry becoming a double debit.

Every call that creates a transaction takes an Idempotency-Key header: POST /api/v1/airtime, /api/v1/data, /api/v1/vend, /api/v1/bills/electricity and /api/v1/bills/cable. It protects you from the one failure that costs real money: a request that succeeds on our side and then dies on the wire before the response reaches you.

Your server sees a timeout and retries, which is correct. With the same key, that retry returns the first answer instead of placing a second order.

How it works

The first request with a key runs, and its response is stored against the key. A later request with the same key and the same request does not run again: it gets the stored response back, with the same status code and body and an Idempotent-Replayed: true header. The stored body is the answer as it was first given, so a replayed vend still says queued. Read the current state from the transaction or its webhook.

  • Keys are scoped to your business and to the mode: a key sent with a test key and the same key sent with a live key are two different orders.
  • A key is kept for 24 hours from its first request. After that it is forgotten and a request carrying it is treated as new.
  • "The same request" means the same method, path and body, byte for byte. Build the body once and send the same bytes on every attempt; a copy re-serialised with its fields in another order is a different request.
  • A key is at most 128 characters.
  • A response of 500 or above is not stored. The key is released, so a retry with it runs the request again.
  • A 400 or 402 from the vend itself is stored and replayed like a success. Nothing was created, so after you fix the cause (top up the wallet, correct the field), send the corrected request with a new key. A 401 or 429 is answered before the key is read, so nothing is stored for it.

Choosing a key

Generate the key once, when the order is created, and store it with the order. Every attempt at that order sends that key. A new key means a new order.

 // When the order is created, once:
order.idempotencyKey = order.id; // or crypto.randomUUID(), saved on the order
order.vendBody = JSON.stringify({
  network: "mtn",
  destination: order.phone,
  amount_kobo: order.amountKobo,
  client_reference: order.id,
});

// Every attempt at this order, however many times your queue runs it:
await fetch("https://api.useroutegate.com/api/v1/airtime", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.ROUTEGATE_API_KEY}`,
    "Idempotency-Key": order.idempotencyKey,
    "Content-Type": "application/json",
  },
  body: order.vendBody,
});

Two rules:

  • Reuse the same key, with the same body, for every retry of the same order. A key generated inside the retry makes every attempt a new transaction.
  • Never reuse a key for a different request. The same key with a different amount or destination is a bug, not a retry, and it is refused with 422.

Send client_reference too. It is unique per business and never expires: a second vend with a client_reference already used is refused with 409 CONFLICT, and the message names the transaction that has it.

What you get back

SituationResponse
First request202 and a new transaction, status: "queued"
Same key, same request, first one finishedThe first response again, with Idempotent-Replayed: true
Same key, same request, first one still running409 with IDEMPOTENCY_IN_PROGRESS; retry shortly, same key
Same key, different body422 with IDEMPOTENCY_FINGERPRINT_MISMATCH
No Idempotency-Key header400 with IDEMPOTENCY_KEY_MISSING
Key longer than 128 characters400 with VALIDATION_FAILED on the Idempotency-Key field
First request answered 500 or aboveNothing stored; the retry runs the request
Key first used more than 24 hours agoTreated as a new key

When to retry

Retry with the same key on a network error, a timeout, a 500 or above, 409 IDEMPOTENCY_IN_PROGRESS, and 429 after its Retry-After. Do not retry 400, 402 or 422 with the same key: the same answer comes back. See errors for every code.