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.
The header
Idempotency-Key: batch-2026-08-13-001Optional, 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.
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:
| Method | Path | Description |
|---|---|---|
| POST | /renders | Creates up to 100 billable renders |
| POST | /campaigns | A duplicate campaign becomes duplicate renders at launch |
| POST | /campaigns/:id/recipients | The real duplicate-render vector — a retried batch doubles the rows |
| POST | /campaigns/:id/assets/from-url | Avoids re-fetching and re-ingesting the same recording |
| POST | /campaigns/:id/assets/from-library | Same 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 header | The handler runs. Nothing is recorded. |
| First use | The handler runs; a successful (2xx) response is recorded against the key. |
| Replay, same body, finished | The recorded response is replayed byte for byte, with the original status and an Idempotency-Replayed: true header. |
| Replay, same body, still running | 409 CONFLICT with Retry-After: 5 — the original is in flight. Wait and retry the same request. |
| Replay, different body | 422 IDEMPOTENCY_KEY_REUSED. Reuse the original body or mint a new key. |
| The first call failed | The 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.
# 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