Skip to content

Idempotency

Network calls fail. Clients retry. Without idempotency, retries create duplicates: two customers, two charges, two subscriptions. Paylera’s contract eliminates the class.

When required

Every POST that creates a resource or moves money requires an Idempotency-Key header:

  • All POST /v1/* (the obvious ones).
  • All POST /v1/*/* operations on existing resources (e.g. /v1/invoices/{id}/pay, /v1/subscriptions/{id}/cancel).
  • All POST /v1/usage (event ingestion).

GET and DELETE are inherently idempotent and don’t need the header. PATCH accepts the header but doesn’t require it.

A required-but-missing key returns 400 idempotency.required.

The contract

You generate a unique key per intent. You include it on the request. On replay with the same key, Paylera returns the original response byte-for-byte, including the original status code, body, and headers — even if the original was an error.

Key format

A key is an opaque string up to 255 characters. Most teams use UUIDs; that’s fine. Be sure they’re unique:

  • sub-create:user_28471:plan_pro:2026-05-06T12:34:56Z — derived from intent fields.
  • 01H8MZ7… — a fresh ULID / UUID per call.
  • retry-1 — collides instantly with every other “retry-1.”
  • customer_28471 — reused across requests; the second request with the same key returns the first response, even if you wanted something different.

A good rule: a key represents one intent. If you want a different outcome, use a different key.

Scope

Keys are scoped per (tenant, route, key). The same key on POST /v1/customers and POST /v1/subscriptions are independent. The same key on the same route from a different tenant is independent.

TTL

Keys live for 24 hours from first use. After that, the key is forgotten — a request with the same key starts fresh and may produce a different result. Don’t rely on indefinite replay.

Body matching

If you replay a key with a different body than the original, the API returns 422 idempotency.body_mismatch. The body comparison is canonical-JSON: whitespace and key order don’t matter; values do.

If you legitimately need the new body to apply, generate a new key.

In-flight replays

If you replay a key while the original request is still in flight, Paylera waits for the original to finish (up to the route’s normal timeout) and returns its response. You won’t see two parallel writes to the same intent, even with concurrent retries.

What gets cached

The full response: status, headers (including idempotency-related headers below), and body. Side effects of the original request — DB writes, provider calls, webhook emissions — happen exactly once.

Replay headers

Idempotent responses carry:

Idempotency-Replay: false on the first execution
Idempotency-Replay: true on subsequent retries with the same key
Paylera-Original-Request-Id: req_… the trace ID of the first execution

Use them in client logs to disambiguate “did the request go through? or did I just see the cached response?”

Errors are cached too

If the original request returned 422 with a validation error, replaying returns the same 422. This is intentional — the client should observe the same outcome regardless of network drama. To recover, fix the inputs and use a fresh key.

Keys you should generate per call

  • POST /v1/customers — fresh per attempt.
  • POST /v1/subscriptions — derived from your intent, e.g. sub:{user_id}:{plan_code}:{your_request_id}.
  • POST /v1/payments, POST /v1/invoices/{id}/pay — fresh per attempt (a “retry to capture” is genuinely a new intent).
  • POST /v1/usage — derived from (subscription_id, meter, event_id) in your domain.
  • POST /v1/refunds — fresh per attempt.

Common pitfalls

  • Reusing keys across deploys. A long-lived key like nightly-batch-2026-05-06 works once; the second time the same batch runs (you re-deployed the cron mid-day), it returns the prior result. Include a per-execution random suffix.
  • Batching with one key. POST /v1/usage/batch takes one key for the whole batch. Atomicity is at the batch level. Don’t try to mix batches under one key across calls.
  • Generating keys client-side and trusting the user. Anything a user can replay can replay your charges. Generate keys server-side.