Idempotency
status: written from ADR-060 section 3, ADR-045 and apps/api/src/app/modules/player/s2s-idempotency.ts. [BT]
Send an Idempotency-Key header on every write (POST, PUT, PATCH, DELETE) to the Operator API. A key is a UUID or 16 to 64 characters of A-Z a-z 0-9 _ -. Use a new key for each new action. Reuse the same key only to retry the same call.
Required on money writes. Missing key gives HTTP 400. The same rule holds for every operator write: a missing or badly formed key gives HTTP 400 IDEMPOTENCY_KEY_REQUIRED. The OpenAPI file still shows the header as optional, because v1 is not released and the contract gate treats a newly required header as breaking. Owner row N162 decides when the schema changes.
| Case | Result |
|---|---|
| New key | The call runs. A success is stored. |
| Same key, same body | The stored answer is returned again. Nothing runs twice. |
| Same key, different body | HTTP 422 IDEMPOTENCY_KEY_REUSED (owner decision). ADR-060 says 409. Open: N45. |
| Same key, first call still running | HTTP 409 IDEMPOTENCY_KEY_IN_PROGRESS, retryable. Wait and send again. |
| Call failed | The key is freed. You can retry with it. Only successes are stored. |
| Key missing or malformed | HTTP 400 IDEMPOTENCY_KEY_REQUIRED |
Replay storage is behind S2S_IDEMPOTENCY_ENABLED (default off). [FO] With it off, the key is still checked but answers are not replayed.
Key lifetime
ADR-060 says 7 days. The code keeps a key for the longer of 7 days and IDEMPOTENCY_KEY_RETENTION_DAYS (default 30, never under 30). Show 30 days to your team. Shown-once secrets are not stored in a replay.
Wallet API
The wallet side is different. Every call carries an operationId. The PAM must return the first outcome with alreadyProcessed: true for an identical request, and 409 IDEMPOTENCY_CONFLICT for the same operationId with a different body. Return no money movement with that 409. Neun treats it as unknown, stops retrying that operation and opens a recovery case (see the sandbox guide). See the signing guide.