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, andPUT.
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
- First request — the API processes the request and caches the response under your key for 24 hours.
- Duplicate (same key, same body) — the API returns the cached response without re-running the operation.
X-Idempotency-Replayed: trueis set on the response. - Conflict (same key, different body) —
409 Conflict. - In-flight (same key, original still running) —
429 Too Many RequestswithRetry-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:
import { v4 as uuidv4 } from 'uuid';
const idempotencyKey = uuidv4().replace(/-/g, '');
// e.g. 550e8400e29b41d4a716446655440000Example: first request
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/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/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/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, andPUT. - 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-Replayedif you need to distinguish "the work just happened" from "the work happened before".
