Retries & idempotency
Production workflows must be safe to re-run. Network blips, server restarts, and worker crashes happen, and your code may end up calling POST /v2/movies more than once for the same business event. This guide explains how to build that safety in.
Why naive retries are unsafe
POST /v2/movies is not idempotent. Every successful call returns a new project ID and consumes credits for any fresh AI generation that occurs (voice, image, video). A retry-on-network-error loop without de-duplication can double or triple your billing.
The good news: the engine's caching system deduplicates identical AI generations across runs. The same voice element with the same text is rendered once and re-used. But the render orchestration still runs, so retry storms still cost you in scene-stitching + final encoding work.
The right pattern: client-side idempotency keys
Generate a stable key from your business event and store the resulting project ID against it. Before submitting a render, check whether you already have a project for this key.
async function submitOnce(idempotencyKey, movieJson) {
// 1. Check your own DB for an existing project tied to this key
const existing = await db.movies.findOne({ idempotency_key: idempotencyKey });
if (existing) return existing.project;
// 2. Submit
const r = await fetch("https://api.json2video.com/v2/movies", {
method: "POST",
headers: { "x-api-key": API_KEY, "content-type": "application/json" },
body: JSON.stringify(movieJson)
});
const { success, project, message } = await r.json();
if (!success) throw new Error(message);
// 3. Persist before returning
await db.movies.insert({ idempotency_key: idempotencyKey, project });
return project;
}
Use the same idempotency key on every retry of the same business event (e.g. order:12345:promo-video). The first call creates the render; subsequent calls return the existing project ID without re-submitting.
Idempotency key conventions
Good keys are:
- Deterministic — derived from the business object, not random.
- Unique per logical render — different rendered variants should have different keys.
- Reasonably stable across retries — re-derive the same key from the same inputs.
Examples:
order-12345-thumbnail— render a thumbnail for order #12345.user-42-onboarding-video-v3— version embedded for explicit invalidation.campaign-summer-2026-ad-spanish— multi-dimensional.
Server-side retries (between submission and rendering)
JSON2Video automatically retries transient failures it sees while producing a render (AI provider rate limits, brief asset fetch failures). You do not need to retry on your side just because the first attempt's status was briefly error — but do check the final status after the configured timeout.
Element-level cache means transient AI failures cost almost nothing on retry: only the failed element is re-attempted; everything else is served from cache.
Polling and idempotency
GET /v2/movies?project=... is naturally idempotent. Poll it as often as you need — once per 5-10 seconds is the recommended cadence. The response is the same regardless of how many times you call it.
For long-running renders (60s+), webhooks are preferable to polling — see webhooks (production).
Webhook delivery and de-duplication
Webhook receivers should de-duplicate on movie.project because:
- A timed-out webhook delivery may be retried (today, this is not the case but it is a likely future behaviour).
- You may receive a duplicate from a re-submitted render that hit the movie-level cache.
A simple de-dup pattern at the receiver:
app.post("/json2video/done", async (req, res) => {
res.status(200).end();
const { movie } = req.body;
const seenBefore = await db.processed.upsert({ project: movie.project });
if (seenBefore) return; // de-duplicate
await handleRender(movie);
});