Archived docs Get your API Key
Get started
Tutorials
Guides
Reference
Help for AI agents
🤖 AI Assistant

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);
});

See also