# Idempotency

Make POST/PATCH/PUT requests safe to retry with the Idempotency-Key header.

`POST`, `PATCH`, and `PUT` endpoints accept an `Idempotency-Key` header that makes retries safe. A duplicate request with the same key and the same body returns the original cached response instead of running the operation twice.

## Key format and scope

* **Format:** 1–255 characters, alphanumeric only (`[a-zA-Z0-9]+`).
* **Scope:** keys are scoped per actor (distributor / partner / organization), so two actors using the same key do not collide.
* **Cache:** the first response is cached for 24 hours.
* **Methods:** only `POST`, `PATCH`, and `PUT`.

The header is **auto-injected on every write operation in the OpenAPI spec** — you don't need to look it up per route. Routes that intentionally opt out set the `x-bp-skip-idempotency: true` extension on their operation.

The middleware fails open: if the cache layer is unavailable the request proceeds without idempotency protection.

## How it works

1. **First request** — the API processes the request and caches the response under your key for 24 hours.
2. **Duplicate (same key, same body)** — the API returns the cached response without re-running the operation. `X-Idempotency-Replayed: true` is set on the response.
3. **Conflict (same key, different body)** — `409 Conflict`.
4. **In-flight (same key, original still running)** — `429 Too Many Requests` with `Retry-After`.

## Header reference

| Header                   | Direction | Description                                                                                             |
| ------------------------ | --------- | ------------------------------------------------------------------------------------------------------- |
| `Idempotency-Key`        | Request   | Unique key. Required for retry safety on POST/PATCH/PUT. Omitting it leaves the operation un-protected. |
| `X-Idempotency-Replayed` | Response  | Present and set to `true` only when the response is a cached replay.                                    |

## Generating keys

Use UUID v4 with hyphens stripped, or any 1–255-character alphanumeric value tied to the logical operation you're performing:

```javascript
import { v4 as uuidv4 } from 'uuid';
const idempotencyKey = uuidv4().replace(/-/g, '');
// e.g. 550e8400e29b41d4a716446655440000
```

## Example: first request

```http
POST https://api.benjipays.com/v2/partners
Authorization: Bearer YOUR_ACCESS_TOKEN
Idempotency-Key: createPartnerAbc123
Content-Type: application/json

{
  "name": "Acme MSP",
  "externalId": "acme-001"
}
```

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

{
  "data": {
    "partnerId": "6022f98854f2c41d7ef2b1fe",
    "name": "Acme MSP",
    "externalId": "acme-001"
  }
}
```

## Example: duplicate request (cached replay)

Same key, same body:

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

{
  "data": {
    "partnerId": "6022f98854f2c41d7ef2b1fe",
    "name": "Acme MSP",
    "externalId": "acme-001"
  }
}
```

No new partner was created.

## Example: conflict

Same key, different body returns `409 Conflict`:

```http
HTTP/1.1 409 Conflict
Content-Type: application/problem+json

{
  "type": "https://api.benjipays.com/problems/idempotency-key-conflict",
  "title": "Conflict",
  "status": 409,
  "detail": "Idempotency key conflict: request body does not match previous request with the same key",
  "instance": "/v2/partners"
}
```

When you see this, generate a new idempotency key and retry as a new operation.

## Error reference

| Status                  | Reason                                                 | What to do                                                   |
| ----------------------- | ------------------------------------------------------ | ------------------------------------------------------------ |
| `400 Bad Request`       | Invalid key format.                                    | Fix the key (1–255 alphanumeric characters).                 |
| `409 Conflict`          | Same key, different request body.                      | Use a new key.                                               |
| `429 Too Many Requests` | Another request with the same key is still processing. | Wait the `Retry-After` duration and retry with the same key. |

## Best practices

* Send an idempotency key on every `POST`, `PATCH`, and `PUT`.
* Generate the key **once per logical operation** and reuse it across retries of that operation.
* On `409 Conflict`, generate a fresh key for the new operation.
* Inspect `X-Idempotency-Replayed` if you need to distinguish "the work just happened" from "the work happened before".