Idempotency

Make a retried create safe: the same key never bills twice.

Outvo bills per render, and every automation surface — n8n, Clay, Zapier, an agent framework, your own retry loop — resends on a network blip. Without an idempotency key, a resent create makes a second campaign, a second recipient row, a second billed render. With one, the retry resolves to the resource the first call already made.

Idempotency-Key: batch-2026-08-13-001

Optional, max 255 characters, any string you generate — a UUID or a deterministic id derived from your own record both work. It is scoped to your account, so keys never collide with another customer's.

Use a fresh key per logical operation. The key is unique per account, so reusing one across two different endpoints (or two genuinely different bodies) is rejected with 422 IDEMPOTENCY_KEY_REUSED rather than silently replayed. Replay records are kept for 7 days; after that the same key is new again.

Where it applies

Every endpoint that can create billable work honors the header:

MethodPathDescription
POST/rendersCreates up to 100 billable renders
POST/campaignsA duplicate campaign becomes duplicate renders at launch
POST/campaigns/:id/recipientsThe real duplicate-render vector — a retried batch doubles the rows
POST/campaigns/:id/assets/from-urlAvoids re-fetching and re-ingesting the same recording
POST/campaigns/:id/assets/from-librarySame ingestion path, no bytes moved

Sending the header anywhere else is harmless and has no effect. Sending no header anywhere leaves behavior exactly as it was.

Replay semantics

No headerThe handler runs. Nothing is recorded.
First useThe handler runs; a successful (2xx) response is recorded against the key.
Replay, same body, finishedThe recorded response is replayed byte for byte, with the original status and an Idempotency-Replayed: true header.
Replay, same body, still running409 CONFLICT with Retry-After: 5 — the original is in flight. Wait and retry the same request.
Replay, different body422 IDEMPOTENCY_KEY_REUSED. Reuse the original body or mint a new key.
The first call failedThe key is released. A corrected retry with the same key runs for real — a transient 402 or 400 is never frozen into a permanent replay.

Bodies are compared canonically: object key order does not matter, but array order does — a reordered recipients array is a different request.

POST /renders is different

The render endpoint uses the same header with one deliberate difference: a replay does not return a frozen snapshot. It returns 200 (rather than the original 202) with the batch at its current status, so a retry doubles as a poll. One key covers the whole batch, and any render still sitting in queued is re-enqueued — a replay is how a batch orphaned by a crash gets a second chance to run, not just a way to read it back.

bash
# First call
POST /renders  (Idempotency-Key: batch-001)  -> 202  renders[].status = "queued"

# Same key, same body, two minutes later
POST /renders  (Idempotency-Key: batch-001)  -> 200  renders[].status = "completed"

# Same key, a recipient added
POST /renders  (Idempotency-Key: batch-001)  -> 422  IDEMPOTENCY_KEY_REUSED