# Idempotency-Key

> Safely retry unsafe requests with an optional Idempotency-Key header.

Every **unsafe** method (`POST`, `PUT`, `PATCH`, `DELETE`) on the **organization-scoped** `/api/v1` operations — those that run in an active organization context, such as projects, defects, test cases, runs, and plans — accepts an OPTIONAL `Idempotency-Key` request header. Send the same key on a retry and Probara replays the original response instead of re-executing the mutation — safe for network retries, client timeouts, and "did that click actually go through" double-submits. `GET`/`HEAD` requests ignore the header entirely.

Authentication (`/api/v1/auth/*`), user profile (`/api/v1/profile`, `/api/v1/profile/avatar`), and the project avatar upload (`/api/v1/projects/{projectId}/avatar`) endpoints do **not** support this header — it is silently ignored there.

File uploads (any request with a `multipart/form-data` body, such as attachment staging or defect evidence uploads) ignore the header too: nothing is recorded or replayed. Only stage requests are safe to retry without a key, because a staged file that no later request references expires on its own. A defect evidence upload is not a stage: a retried upload can store the same files twice, so check the defect's attachment list before you retry it.

Keys are scoped per organization and per acting actor: the same key value used by a different org, or by a different member within the same org, is treated as an independent key and never replays across that boundary.

## Key format

- Non-empty
- No longer than **255 visible ASCII characters** (no whitespace, control characters, or non-ASCII code points)

An invalid key responds `400` and the handler never runs.

## Usage

```bash
# First attempt — executes normally and records the response under the key.
curl -sS -X POST \
  -H "Authorization: Bearer $PROBARA_API_TOKEN" \
  -H "Idempotency-Key: 8f14e45f-ceea-4a3c-b9a8-1a6b3d2f0e91" \
  -H "Content-Type: application/json" \
  -d '{"name":"Checkout flow"}' \
  "https://app.probara.net/api/v1/projects"
```

```http
HTTP/1.1 201 Created
Content-Type: application/json

{ "ulid": "01J...PROJ", "name": "Checkout flow", ... }
```

Retrying the identical request with the same key and body within the retention window returns the same status and body — still `01J...PROJ`, no second project created — with an extra response header:

```http
HTTP/1.1 201 Created
Idempotency-Replayed: true
Content-Type: application/json

{ "ulid": "01J...PROJ", "name": "Checkout flow", ... }
```

## Response semantics

| Situation                                                    | Response                                                                                      |
| ------------------------------------------------------------ | --------------------------------------------------------------------------------------------- |
| First request with an unseen key                             | Executes normally; response recorded for replay (JSON, status `< 500`, not `204`, not binary) |
| Identical retry within the retention window                  | Same status and body as the original, plus `Idempotency-Replayed: true`                       |
| Duplicate arrives while the first request is still in flight | `409` with the standard error envelope (`code: "conflict"`) and a `Retry-After` header        |
| Same key reused with a different method, path, or body       | `422` with `code: "validation_failed"`; nothing executes                                      |
| Invalid key (empty or over 255 visible ASCII characters)     | `400`; nothing executes                                                                       |
| First request fails                                          | The claim is released; a later retry with the same key executes normally                      |
| Response is `204`, non-JSON, or binary                       | Passed through unrecorded — a retry with the same key executes the handler again              |

## Retention

Key records are kept for **24 hours**. After the window elapses, the same key behaves as a fresh, unseen key: an identical request executes the handler again, and `Idempotency-Replayed` is absent from that response.

## Errors

| Status | Code                | Meaning                                                                         |
| ------ | ------------------- | ------------------------------------------------------------------------------- |
| 400    | `validation_failed` | `Idempotency-Key` is empty or longer than 255 visible ASCII characters          |
| 409    | `conflict`          | A request with the same key is still in flight; includes a `Retry-After` header |
| 422    | `validation_failed` | The key was already used with a different method, path, or request body         |

## Related

- [API authentication](https://docs.probara.net/en/developers/authentication/) — Bearer tokens and session auth
- [API reference overview](https://docs.probara.net/en/developers/api-reference/) — pagination, errors, consolidated PATCH
