LeadRails

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 POST and PATCH. Omitting it returns 400 idempotency-key-required.
  • Maximum 255 characters. Longer keys return 400 idempotency-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-Key used on every write.