When it is required
- Required for job creation on
POST /api/v1/generate-and-post,POST /api/v1/blog-to-videoandPOST /api/v1/jobs. - Required for live keys on every call that costs money (Magic Video, text/image-to-video, articles, posting, schedules and YouTube clipping).
- Recommended on other
POSToperations that do not require it.
Free Generate previews with dry_run: true do not require an idempotency key and do not enter the replay cache. You can reuse a preview's key for the separately approved real request. See Dry runs.
A missing key where one is required returns 400 IDEMPOTENCY_KEY_REQUIRED. The header is ignored on GET. MCP clients pass idempotency_key where the discovered tool schema supports it. Current MCP clipping does not forward that required key; use REST for paid clipping, and MCP for clipping previews with dry_run: true.
Header shape
Idempotency-Key: 5f9e1a2b-4c8d-4e3f-9a1b-2c3d4e5f6a7b- Length: 8–256 characters. Anything else returns
400 INPUT_INVALID_FORMAT. A UUIDv4 is recommended. - Scope: per account, not per key. Your live key and your
rb_test_sandbox key share the same cache, so never reuse a key value between sandbox and live. - Lifetime: 24 hours from the first request. After that the key is forgotten and the same value runs fresh.
Replay behavior
- First request with a key: runs normally. When it finishes, its status code and JSON body are stored.
- Same key, same method, path and body: the stored status and body are returned with the header
x-idempotent-replay: true. Nothing runs and nothing is charged. Other response headers are not replayed. - Same key, different body or different path:
409 RESOURCE_ALREADY_EXISTS. The error details contain onlyheaderandkey_prefix, not a field diff. The body comparison is exact, including JSON field order — serialize retries identically. - After 24 hours: the key behaves like a new one.
Error responses are cached too
Every response is stored, including 4xx responses such as 400, 402 INSUFFICIENT_API_CREDITS, 422 and 429, and sandbox fixtures. Retrying with the same key replays that same error for 24 hours.
- After fixing a request, topping up your balance, or waiting out a 429, send it with a new key.
- Reuse a key only to retry the exact same request after a network error or timeout where you never saw a response.
- The response is stored when the first request answers. On the job-creating endpoints a duplicate sent while the first is still running is recognised (see the next section); on other endpoints it is not, so wait for the first attempt to return before retrying.
- A
409 REQUEST_IN_PROGRESSis never stored: retry it with the same key.
The same request twice
A second safety net works without the key. On the endpoints that create a video job or a persona, the same request from your account (same endpoint, same JSON body, in any field order) is never run twice by accident, whatever Idempotency-Key it carries:
- While the first is still being handled: the second waits up to 10 seconds for the first answer and returns it. If the first is still running, it gets
409 REQUEST_IN_PROGRESSwithRetry-After: 5. Nothing is created or charged for the second call; retry it after a few seconds to receive the first request's result. - For 5 minutes after the first was accepted: the second gets the first answer again, with the headers
x-idempotent-replay: trueandx-idempotent-replay-match: body, andmeta.replayed: truein the body. The job ids are the first request's; nothing new is created or charged. - If the first was rejected, or every job it created has already failed: the next attempt runs normally.
- In a series (
series_id), once the first request was accepted, the same topic sent again with a different key is refused before it reaches this check, with a free409 SERIES_TOPIC_REPEATED. Nothing is charged; to get the first answer again, resend with the key that created it. - To repeat a request on purpose inside those 5 minutes (another take of the same prompt), change the body or send the header
X-Repeat-Tokenwith any value of up to 128 characters, for exampleX-Repeat-Token: take-2. A different token is a different request.
Covered: /generate-and-post, /jobs, /blog-to-video, /videos/magic, /videos/text-to-video, /videos/image-to-video, /videos/ai-influencer (including variations), /clipper/youtube, /avatars and /avatars/{id}/looks. Each item of a /batch is its own request, so two identical items in one batch are two jobs. Other endpoints (/post, /articles, /schedules) rely on the key alone.
Slow responses and client timeouts
Creation requests answer within a few seconds; creating a persona can take longer. Set your HTTP client's timeout to at least 120 seconds on every paid POST anyway.
- A client timeout does not cancel the request: the work continues and is charged.
- The answer is stored as soon as it is produced, even if your client has already disconnected, so a retry with the same key returns it.
- After a timeout, resend the identical body with the same key. You get the first answer, or
409 REQUEST_IN_PROGRESSwhile it is still being handled; either way nothing more is charged. - Prefer the same key for a retry. On the endpoints listed above a retry with a new key inside 5 minutes is still recognised and returns the first result; on the others a new key is a new order.
- If a timeout did leave you with duplicate jobs, contact support with both job ids.
Examples
cURL
curl -X POST https://api.reelsbuilder.ai/api/v1/generate-and-post \
-H "Authorization: Bearer $REELSBUILDER_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "prompt": "Q3 launch announcement for Acme" }'TypeScript — retry only on network failure
const idempotencyKey = crypto.randomUUID();
const body = JSON.stringify({ prompt: "Q3 launch announcement for Acme" }); // serialize once
async function startRender() {
for (let attempt = 0; attempt < 3; attempt++) {
try {
const res = await fetch("https://api.reelsbuilder.ai/api/v1/generate-and-post", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.REELSBUILDER_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": idempotencyKey,
},
body,
});
// Any HTTP response (2xx or 4xx) is now cached for this key; return it.
return { status: res.status, json: await res.json() };
} catch (err) {
// Network error: no response seen, safe to resend with the SAME key.
if (attempt === 2) throw err;
await new Promise((r) => setTimeout(r, 2 ** attempt * 1000));
}
}
}Python
import os, uuid, time, requests
idempotency_key = str(uuid.uuid4())
payload = {"prompt": "Q3 launch announcement for Acme"}
def start_render():
for attempt in range(5):
try:
r = requests.post(
"https://api.reelsbuilder.ai/api/v1/generate-and-post",
headers={
"Authorization": f"Bearer {os.environ['REELSBUILDER_API_KEY']}",
"Idempotency-Key": idempotency_key,
},
json=payload,
timeout=120,
)
except (requests.ConnectionError, requests.Timeout):
# No answer seen. The request may still be running and will be
# charged once: resend with the SAME key to read its answer.
if attempt == 4:
raise
time.sleep(2 ** attempt)
continue
if r.status_code == 409 and r.json().get("error", {}).get("code") == "REQUEST_IN_PROGRESS":
# The first attempt is still being handled. Nothing was charged
# for this call; ask again shortly.
time.sleep(int(r.headers.get("Retry-After", "5")))
continue
return r # stored for this key, success or error
raise RuntimeError("no final answer after 5 attempts")Best practices
- One key per logical operation, generated randomly (UUIDv4). Don't derive it from the request content.
- Persist the key with the operation in your own store so a retry after a process restart sends the same key.
- New key after any change — a corrected body, a top-up, or a switch between sandbox and live.