# Idempotency

> The header that stops a retry becoming a double debit.

Source: https://useroutegate.com/docs/concepts/idempotency

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](/docs/api/public/getTransactionByReference) 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.

```javascript
// 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

| Situation                                       | Response                                                      |
| ----------------------------------------------- | ------------------------------------------------------------- |
| First request                                   | `202` and a new transaction, `status: "queued"`               |
| Same key, same request, first one finished      | The first response again, with `Idempotent-Replayed: true`    |
| Same key, same request, first one still running | `409` with `IDEMPOTENCY_IN_PROGRESS`; retry shortly, same key |
| Same key, different body                        | `422` with `IDEMPOTENCY_FINGERPRINT_MISMATCH`                 |
| No `Idempotency-Key` header                     | `400` with `IDEMPOTENCY_KEY_MISSING`                          |
| Key longer than 128 characters                  | `400` with `VALIDATION_FAILED` on the `Idempotency-Key` field |
| First request answered `500` or above           | Nothing stored; the retry runs the request                    |
| Key first used more than 24 hours ago           | Treated 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](/docs/concepts/errors) for every code.
