Idempotency
Every POST and PATCH on the v1 surface
requires an Idempotency-Key header. It makes retries safe:
a network blip never creates a duplicate source, destination, or route.
The header
Send a client-generated key on every state-changing request:
Idempotency-Key: 7d6f4c2e-1a3b-4c5d-8e9f-0a1b2c3d4e5f - Required on every
POSTandPATCH. Omitting it returns400idempotency-key-required. - Maximum 255 characters. Longer keys return
400idempotency-key-too-long. - Generate a fresh key per logical operation — not per HTTP attempt. Retries of the same operation reuse the same key.
Recommended: a UUID per operation
A v4 UUID is the right default — collision-free and exactly 36 characters. Generate it once, before the first attempt, and hold it across all retries of that operation:
// Generate once per logical operation, reuse on every retry.
const idempotencyKey = crypto.randomUUID();
await lr.POST("/v1/sources", {
headers: { "Idempotency-Key": idempotencyKey },
body: { name: "Website contact form", source_type: "wordpress" },
}); # Shell: capture the key, then reuse $KEY on retries.
KEY=$(uuidgen)
curl -s -X POST https://api.leadrails.dev/v1/sources \
-H "Authorization: Bearer $LR_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $KEY" \
-d '{"name":"Website contact form","source_type":"wordpress"}' Scope and retention
Keys are scoped to your workspace, not to a single API key. Rotate your API key mid-workflow and a replay still resolves against the original response, because both keys belong to the same workspace.
Each key is retained for 24 hours from first use
(table: migrations/0035_api_idempotency_keys; an expiry
sweep prunes older rows). After the window the same key string is free
to use again as a fresh operation. Within the window, a replay returns
the original response.
Because the scope is (workspace, key) and carries no notion of which endpoint you called, do not reuse one key across two different operations in the same workspace within 24 hours — they'd collide. One key, one operation.
Replay behavior
Retry a completed request with the same key + the same body and the API
replays the original response verbatim — same status,
same body — without re-running the mutation. Replays are flagged with
an Idempotent-Replayed: true response header so you can
tell a replay from a fresh execution.
If a first request with a key is still in flight when a second arrives
with the same key, the second gets 409 idempotency-in-progress —
don't fire concurrent requests that share a key; wait for the first to
finish, then retry to receive its result.
Conflict: same key, different body
The first body sent with a key is fingerprinted (SHA-256). A later
request that reuses the key with a different body is a
mistake, so the API rejects it with 422 idempotency-conflict rather
than silently returning the stale first result.
- Retrying the exact same request? Reuse the key — you get the cached result.
- Sending a changed request? Generate a new key.
What's next
- Rate limits — pair idempotent retries with correct backoff.
- Error catalog — every idempotency problem type and its fix.
- Quickstart — see
Idempotency-Keyused on every write.