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 executionIdempotency-Replay: true on subsequent retries with the same keyPaylera-Original-Request-Id: req_… the trace ID of the first executionUse 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-06works 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/batchtakes 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.