# Idempotency

> Exactly how the Idempotency-Key behaves: format, scope, 24-hour window, what is compared, what is memorised, and what happens on a retry, a conflict or concurrent requests.

The `Idempotency-Key` header makes a request safe to retry: if the response is lost on the network, the same request sent again with the same key returns the response already computed instead of computing and billing a second time.

## Format

- 1 to 255 bytes, visible ASCII only (`0x21` to `0x7E`): no space, no tab, no accented character.
- No normalisation: the value you send is the value stored, compared byte for byte.
- An empty, too long, repeated header, or one containing a forbidden character, is refused with `400 invalid_idempotency_key`, before any computation.

One UUID per logical operation is a good choice. Generate it **once**, before the first attempt, and keep it for every retry of that operation.

## Scope and window

| Property | Value |
|---|---|
| Scope | The workspace and environment of the API key. Every API key of a workspace shares this space; Sandbox and Production are separate. |
| Window | 24 hours from the first request. |
| After 24 hours | The key becomes free again, including for a different request. |

## What is compared

Two requests are "the same request" when they concern the same operation (creation, or refresh of a given target), the same economic input in its canonical form and the same `external_transaction_id`. JSON field order, case and whitespace in `descriptor`, or a fraction of a second in `occurred_at` change nothing. A different amount, currency or identifier makes a different request.

The same key used for a create call and then for a refresh designates two different requests: the second returns `409 idempotency_key_reuse`.

## What happens

| Situation | Response |
|---|---|
| First request with this key | Processed normally |
| Same key, same request, after a `200` | The memorised body of the first response, `Noryo-Idempotency-Replayed: true`. Nothing is recomputed or billed. |
| Same key, different request, within 24 hours | `409 idempotency_key_reuse` |
| Same key, same request, while the first is still in progress | The second waits for the first to finish, then returns its response as a replay; if the wait lasts too long, `503 busy` with `Retry-After` |
| Same key, same request, after a refusal returned at settlement (`402`, `403 production_not_enabled`, `409 transaction_id_conflict`, `410`, `503`) | Re-evaluated as a new attempt: a refusal is not memorised |
| Same key, different request, after such a refusal | `409 idempotency_key_reuse`: a refusal does not free the key |
| Same key after a refusal that came before settlement (validation `400`, `401`, `403 insufficient_scope`, `429`) | The key had not been reserved: it is still free |
| Same key after 24 hours | Processed as a first request |

Two rules sum up the table. **Only a success is memorised**: an error is replayed by recomputing, which lets you retry after a failure. **The link between a key and its request never changes for 24 hours**, even after an error: a key is not recycled for another payload.

## What a replay contains

- The **same body**, `usage` included: a response that was billed originally still shows `billable: true`. Do not count it twice.
- The `Noryo-Idempotency-Replayed: true` header.
- A **new** `Noryo-Request-Id`: every attempt has its own identifier, replays included.
- One rate-limit token consumed, like any authenticated request.

## Concurrent requests

Two identical requests sent at the same time with the same key never produce two settlements. The second waits for the first instead of being refused: it then receives the replay of the response. If the wait exceeds the delay the server allows itself, it receives `503 busy` with a `Retry-After`: nothing was written or billed, and the same request can be sent again as it is.

```json
{
  "type": "https://developer.noryo.app/errors/busy",
  "title": "Service busy",
  "status": 503,
  "code": "busy",
  "detail": "The service is temporarily contended. Retry after the delay given by Retry-After.",
  "request_id": "req_…"
}
```

## Choosing keys

- One key per logical operation, never one key per session or per customer.
- Several services in one workspace share the key space: prefix them, for example `billing-<uuid>`.
- A retry reuses the same key; a new operation takes a new one.
- Put no personal data in the key: it is not logged in clear, but nothing justifies placing information in it.

---

Canonical page: https://developer.noryo.app/en-GB/merchant/requests/idempotency
