> ## Documentation Index
> Fetch the complete documentation index at: https://docs-provider.voucher.allride.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Idempotency

> Retry POST requests safely with the Idempotency-Key header.

## When to use idempotency keys

Every `POST` accepts an optional `Idempotency-Key` header. With it, a retry of the same request never has a second effect: the stored response is returned instead. Without it, the request runs normally every time.

Send a key on every POST, especially those that create things: voucher sets, change requests, vouchers (`:batch`), uploads, webhook endpoints, and commands such as `:activate`. A batch is covered by one key; there are no per-item keys.

`GET`, `PUT`, `PATCH` and `DELETE` do not use it: a key sent there is ignored and the response carries `Idempotency-Key-Ignored: true`. (`PUT` and `DELETE` are naturally repeatable, and a `PATCH` writes only the fields you send; see [Concurrent changes](https://docs-provider.voucher.allride.io/guides/requests-and-responses.md#concurrent-changes).)

## Choosing a key

* 1-255 printable ASCII characters, no spaces. Anything else → 400 `invalid-idempotency-key`.
* Use a UUID (v4 or v7) per **intent**, or a deterministic business key such as `inventory-2026-11:batch-7`.
* Generate the key once per intent and reuse it for **every retry** of that intent, including after timeouts and 5xx.
* Keys are kept for **24 hours**. After that the same key runs again, so never reuse a key for a different operation.

## How replays work

| Situation | Result |
| - | - |
| Same key, same request | The stored status, headers and body, with `Idempotent-Replayed: true`. No second effect: no new rows, no audit entry, no webhook. |
| Same key, different request | 422 `idempotency-key-reused` |
| Same key while the first request is still running | 409 `idempotency-key-in-use` with `Retry-After: 1` |
| No key | Normal processing |

"Same request" means same method, path, query and body. The body is compared as canonical JSON (key order and whitespace do not matter), but numbers are compared as written: `1.0` and `1` are different bodies, so resend identical values.

**Stored and replayed:** 2xx responses and final 4xx outcomes (400 `data-failed-validation`, `malformed-json` and `invalid-header`, 404, 409 domain conflicts, 410, 422, for example `scope-not-delegable`). Fixing an invalid body therefore needs a new key. A stored domain conflict is also **final for that key**: after 409 `voucher-value-exists`, retrying with the same key replays the 409 even if you fixed the data. A new attempt needs a new key.

**Not stored** (a retry with the same key runs again): 409 `concurrent-update`, 409 `idempotency-key-in-use`, 422 `idempotency-key-reused`, every 5xx, timeouts and network failures. 401 and 403 are decided before the key is looked at and are never stored.

**Show-once responses** (API key create and roll, webhook endpoint create and secret rotation, and `POST /provider/v1/vouchers/{voucherId}:reveal`) are stored with the secret or revealed value set to `null`. A replay returns that body with `Idempotent-Replay-Redacted: true`. If you lost the original response, revoke and create again (or reveal again with a new key).

An idempotent replay of a 202 shows the job as it was when accepted; follow `Location` for the current state.

## Key scope and key rotation

Keys are scoped to the caller, so two callers can never collide or read each other's responses:

* For an API key, the scope is its **roll chain**: a key created with `:roll` shares the scope of the key it replaced. Retrying an intent with the same `Idempotency-Key` through the rolled key **replays**.
* An **independently created** API key has its own scope: retrying through it is **not** a replay and runs again (it could create vouchers twice, which then fails with `voucher-value-exists`).
* For dashboard users, the scope is the user within the partner.

Rotate keys with [`:roll`](https://docs-provider.voucher.allride.io/guides/api-keys#rotate-a-key), never by switching to a fresh key in the middle of open retries.

## Errors

| Status and code | Meaning | What to do |
| - | - | - |
| 400 `invalid-idempotency-key` | Empty, longer than 255, spaces or non-ASCII | Send a valid key |
| 409 `idempotency-key-in-use` | The first request with this key is still running | Retry with the **same** key after `Retry-After` |
| 422 `idempotency-key-reused` | The key was used for a different request | Use a new key for the new request |

## Retry strategy

Retry with the same key on network errors, 5xx, `idempotency-key-in-use` and `concurrent-update`; use exponential backoff with jitter, honour `Retry-After`, and stop after a few attempts.

```ts theme={null}
async function postIdempotent(path: string, body: unknown, key = crypto.randomUUID()): Promise<Response> {
  for (let attempt = 0; attempt < 5; attempt++) {
    const res = await fetch(`${process.env.VOUCHER_API_BASE_URL}${path}`, {
      method: "POST",
      headers: { Authorization: `Bearer ${process.env.VOUCHER_API_KEY}`,
                 "Content-Type": "application/json", "Idempotency-Key": key },
      body: JSON.stringify(body),
    }).catch(() => undefined);                       // network error: retry with the same key
    if (res && res.status < 500 && res.status !== 409 && res.status !== 429) return res;
    if (res?.status === 409) {                       // only transient 409s are retried with the same key
      const { errors } = await res.clone().json();
      const code = errors?.[0]?.errorCode;
      if (!["idempotency-key-in-use", "concurrent-update"].includes(code)) return res;
    }
    const retryAfter = Number(res?.headers.get("Retry-After") ?? 0);
    await new Promise(r => setTimeout(r, Math.max(retryAfter * 1000, 2 ** attempt * 250 + Math.random() * 250)));
  }
  throw new Error("Gave up after 5 attempts; the same Idempotency-Key is still safe to retry later (24 h)");
}
```

## Related

* [Errors](https://docs-provider.voucher.allride.io/errors.md#retrying-safely)
* [API keys](https://docs-provider.voucher.allride.io/guides/api-keys.md#rotate-a-key)
* [Requests and responses](https://docs-provider.voucher.allride.io/guides/requests-and-responses.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.