Skip to Content

Renders

A render is a job, not a request. POST /v1/renders returns as soon as the job is accepted; the pixels happen elsewhere.

Create a render

POST /v1/renders Authorization: Bearer pm_live_… Content-Type: application/json Idempotency-Key: order-4711-render
{ "templateId": "tpl_7f8a9b", "inputs": { "headline": "Launch Special 2026", "accentColor": "#2988ff", "character": "https://cdn.example.com/photo.jpg" }, "output": { "format": "mp4", "width": 1080, "height": 1920, "fps": 30 }, "callbackUrl": "https://hooks.example.com/premation" }
FieldRequiredNotes
templateIdyesMust belong to your account
inputsyesKeys must be declared ids; every required field must be present
outputnoDefaults to the template’s own dimensions and fps
callbackUrlnoSee Webhooks

Response:

{ "jobId": "clx9zz110000", "status": "queued" }

Scope: renders:write.

Output overrides

FieldAllowed
formatmp4 only
width, heightIntegers, 64–4096, and at most 16 megapixels together
fpsInteger, 1–120

Anything outside those answers 400. Omit output entirely and the template’s published dimensions are used.

MP4 is the only output format over the API. The desktop exporter’s other nine formats — WebM with alpha, ProRes 4444, PNG sequence, Lottie, SVG, GIF — are not exposed here. MP4 also means no alpha channel: transparency is flattened onto the composition background. Audio is not muxed by the hosted renderer yet.

Idempotency

Pass Idempotency-Key on POST /v1/renders and a repeat of the same key returns the original job instead of queueing a second render.

Idempotency-Key: order-4711-render

The key must be 8–128 characters from A-Z a-z 0-9 . _ : -; anything else is a 400. Keys are scoped to your account, so they never collide with another customer’s.

Use it. A retry-on-timeout in n8n or a re-run of a CI job is exactly the case this exists for — without a key, a network blip that hides a 201 costs you a duplicate render and duplicate quota. Derive the key from something stable in your domain (an order id, a row id), not from a timestamp.

Poll a job

GET /v1/renders/{jobId}
{ "jobId": "clx9zz110000", "status": "completed", "progress": 100, "videoUrl": "https://res.cloudinary.com/…/out.mp4", "error": null, "createdAt": "2026-08-27T09:00:00.000Z" }

Scope: renders:read. A job id belonging to another account answers 404.

Lifecycle

queued ──▶ processing ──▶ completed ├──▶ failed └──▶ cancelled
StatusMeaning
queuedAccepted and enqueued; no worker has picked it up
processingA render worker is executing it; progress moves
completedvideoUrl is populated
failederror explains why, sanitised for public consumption
cancelledCancelled by you

completed, failed and cancelled are terminal — a job never leaves them.

Renders are CPU-bound and run on SwiftShader, so 1080p is often slower than realtime. Budget generously: poll on a 3–5 second interval rather than a tight loop, or use a webhook and stop polling altogether.

List jobs

GET /v1/renders?limit=50&offset=0
{ "items": [ { "jobId": "…", "status": "completed", "progress": 100, "videoUrl": "…", "error": null, "createdAt": "…" } ], "total": 128, "limit": 50, "offset": 0 }

Newest first. limit defaults to 50, capped at 100. Scope: renders:read.

Cancel a job

POST /v1/renders/{jobId}/cancel

Cancelling releases the render minutes that were reserved when the job was created. A job that is already terminal is returned as-is rather than erroring — a cancel that races a completion is not your fault, and you get back the status that actually won:

{ "jobId": "clx9zz110000", "status": "completed" }

A render already executing at the worker finishes there; its result is discarded. If the job carried a callbackUrl, the cancellation is delivered to it. Scope: renders:write.

Download

GET /v1/renders/{jobId}/download → 302 Location: https://res.cloudinary.com/…/out.mp4

A redirect, not a byte stream — the file lives on a CDN, and proxying video through the API would tie up a request thread for the length of a download. Most HTTP clients follow it by default, n8n’s HTTP Request node included.

Job stateAnswer
completed302 to the file
queued / processing409, with the current status in the body
failed / cancelled409, with the reason
{ "message": "Render is still processing. Poll GET /v1/renders/:id or use callbackUrl.", "status": "processing" }

Scope: renders:read.

Rate limiting

Render creation is limited to 60 per minute per account, independently of your monthly quota. Over it you get 429 with a Retry-After hint in the body:

{ "code": "rate_limit_exceeded", "retryAfterSeconds": 60, "message": "Too many render requests. Try again shortly." }

429 rather than 403 is deliberate: HTTP clients and n8n back off on 429 and treat 403 as “your credentials are wrong”, which used to kill workflows that were merely early.

Failure modes worth handling

StatusWhen
400Unknown input key, wrong value type, missing required input, bad output dimensions, malformed Idempotency-Key
403Plan lacks API access, monthly render minutes or API requests exhausted
404Template or job id not on your account
409Downloaded a job that is not completed
429More than 60 renders in a minute

A 400 for bad inputs enumerates every offending field at once:

{ "statusCode": 400, "message": "Invalid inputs.", "errors": [ { "field": "headline", "message": "Input \"headline\" must be a string." }, { "field": "logo", "message": "Unknown input \"logo\"." } ] }
Last updated on