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

HeaderDirectionDescription
Idempotency-KeyRequestUnique key. Required for retry safety on POST/PATCH/PUT. Omitting it leaves the operation un-protected.
X-Idempotency-ReplayedResponsePresent 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. 550e8400e29b41d4a716446655440000

Example: 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

StatusReasonWhat to do
400 Bad RequestInvalid key format.Fix the key (1–255 alphanumeric characters).
409 ConflictSame key, different request body.Use a new key.
429 Too Many RequestsAnother 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".