Idempotency and errors
A billing API that gets a retry wrong charges twice. These rules make every write safe to repeat.
The Idempotency-Key header
Every POST that changes something, except the event and cost endpoints below, accepts an Idempotency-Key header, and so do PATCH and DELETE. Send a UUID, or a key from your own system such as reverse-oc_2xK9-ticket-118. The three read-only POST calls, POST /v1/config/validate, POST /v1/payment_providers/resolve and POST /v1/events/preview, change nothing and don’t need one. A request sent with Runledger-Dry-Run: true writes nothing, so its result isn’t stored against the key.
Keys are scoped to your account, environment and endpoint. The SDKs generate a key for each call and reuse it across their own retries; pass your own when a retry may come from a different process, such as a job queue.
Event and cost keys
track(), trace() and their batch forms deduplicate on the idempotency_key field of each item, for 35 days, and don’t use the header. Build it from your own data so a replay from your queue lands on the same key:
A repeat returns the original event. In a batch, each item reports created, duplicate or rejected on its own.
Fix a mistake in an event
An event is immutable once it is recorded. Its name (the event_name that rates and rules match) never changes, and there is no call to edit or delete an event. To fix a mistake, send a correcting event with its own idempotency_key, such as a signal that reverses an outcome charge that shouldn’t stand. The ledger works the same way: a correction is a new entry that links to the one it corrects.
Retries
The SDKs retry twice by default, with exponential backoff and jitter, on connection errors, 408, 429 and 5xx, and honour Retry-After. Errors with retryable: false won’t succeed on retry; fix the request first. If a write stays unresolved, stop retrying, then read the object or GET /v1/webhook_events to find out what happened.
The error body
Errors use RFC 9457 problem details, sent as application/problem+json:
type and doc_url link to the error codes table below. Branch on code, which is stable. detail is for people and may change. param, when present, names the field at fault as a dotted path, such as events.3.timestamp. On insufficient_scope, required_scope names the scope the call needed, such as config:write. On an invalid_state that names objects in the way, blocked_by lists them as {object, id}: removing a payment provider lists the routing_rules that use it and the customers whose own payment_provider names it, in one list, and an Admin changes or deletes them first.
Status codes
A check() denial is not an error. It returns 200 with allowed: false.
Error codes
New codes may be added. Treat an unknown code by its status.
See also
- Conventions covers versions, rate limits and request IDs.
- Going live shows retries in a job queue.