Idempotency

Safely retry write requests without creating duplicates

Network failures, timeouts, and worker restarts make retries unavoidable. The Idempotency-Key header lets you retry a write request without risking duplicate side effects: the first successful response is cached for 24 hours, and any subsequent request with the same key is replayed verbatim from cache.

When to Use

Send Idempotency-Key on every state-changing request where a duplicate would be problematic — creating invoices, posting payments, bulk imports, etc.

The header is optional. If omitted, the request is processed normally without idempotency protection.

How It Works

  1. The client generates a unique key (UUID/ULID recommended) and sends it in the Idempotency-Key header.
  2. On the first request with that key, the server processes normally and caches the 2xx response.
  3. On a retry with the same key:
    • Content-Type: application/json (JSON tier) — body is compared using a SHA-256 hash:
      • Same body → cached response returned with X-Idempotent-Replayed: true.
      • Different body → 422 Unprocessable Entity (catches accidental key reuse with mutated payload).
    • All other content types (e.g. multipart file uploads) — key-only match: a retry with the same key returns 422 Unprocessable Entity, since the binary body cannot be hashed to confirm it matches the original. Use a new key for a new request.
  4. If a retry arrives while the first request is still being processed → 409 Conflict. Retry after the original completes.

Example

$curl -X POST https://api.ottimate.com/v1/invoices \
> -H "X-API-Key: YOUR_API_KEY" \
> -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
> -H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
> -H "Content-Type: application/json" \
> -d '{ "vendor_id": "...", "amount": 1234.56 }'

If the same request is retried with the same Idempotency-Key and body, the response is identical, no new invoice is created, and the response includes:

X-Idempotent-Replayed: true

Key Format

ConstraintValue
Length1–128 characters
Allowed charactersA–Z, a–z, 0–9, ., _, -, +, =, /
RecommendedUUIDv4 or ULID

Invalid keys are rejected with 400 Bad Request.

Scope and Lifetime

AttributeBehavior
Cache lifetime24 hours from the first response
ScopePer API key + HTTP method + path + idempotency key
Cached responsesOnly 2xx responses are cached. 4xx/5xx responses are not — retries are processed fresh.
API key rotationRotating your API key invalidates all cached entries scoped to the old key.
Because scope includes the path and method, the same Idempotency-Key value can be safely reused across different endpoints — they will not collide.

Response Codes

StatusMeaning
2xx + X-Idempotent-Replayed: trueReplay of a cached response. Side effects already happened on the original request.
208 Already ReportedThe original response was too large to cache (>64 KB) or was streamed. The original operation did succeed — fetch the current state via the corresponding GET endpoint.
400 Bad RequestMalformed Idempotency-Key header (length / charset).
409 ConflictA concurrent request with the same key is still being processed. Wait and retry.
422 Unprocessable EntitySame key was previously used with a different request body (JSON requests only).

Applicability

Idempotency-Key takes effect on all POST requests under /v1/*. Each endpoint that supports it lists Idempotency-Key in its parameter table — refer to the per-endpoint API reference.

The header is ignored (no caching) for:

  • Non-POST methods (GET, PATCH, PUT, DELETE — currently)
  • Requests without an API key

For multipart file uploads and other non-JSON content types, the server cannot verify the request body without loading the full upload into memory. A retry with the same key always returns 422 — use a new key for each new upload.