Start an Async Render
POST /studio/render with response.mode "async" runs the render in the background. Collect the result by polling or webhook.
Updated
/v1/studio/rendercurl -X POST "https://api.orshot.com/v1/studio/render" \
-H "Authorization: Bearer <ORSHOT_API_KEY>" \
-H "Content-Type: application/json" \
-d '{
"templateId": 1,
"modifications": {},
"response": {},
"pdfOptions": {}
}'const res = await fetch("https://api.orshot.com/v1/studio/render", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer <ORSHOT_API_KEY>",
},
body: JSON.stringify({
"templateId": 1,
"modifications": {},
"response": {},
"pdfOptions": {}
}),
});
const data = await res.json();import requests
response = requests.post(
"https://api.orshot.com/v1/studio/render",
headers={"Authorization": "Bearer <ORSHOT_API_KEY>"},
json={
"templateId": 1,
"modifications": {},
"response": {},
"pdfOptions": {}
},
)
data = response.json()$ch = curl_init("https://api.orshot.com/v1/studio/render");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer <ORSHOT_API_KEY>",
"Content-Type: application/json",
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
"templateId" => 1,
"modifications" => [],
"response" => [],
"pdfOptions" => []
]));
$data = json_decode(curl_exec($ch), true);
curl_close($ch);require "net/http"
require "json"
uri = URI("https://api.orshot.com/v1/studio/render")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer <ORSHOT_API_KEY>"
req["Content-Type"] = "application/json"
req.body = {
"templateId": 1,
"modifications": {},
"response": {},
"pdfOptions": {}
}.to_json
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(res.body)A synchronous render holds the HTTP connection for the whole render. That is fine for images, but video renders can take minutes, longer than most client timeouts (Zapier ~30s, Make ~60s, many AI agents 120s). Async mode returns immediately with a job, the render runs server-side, and you collect the result by polling or via a webhook.
POST https://api.orshot.com/v1/studio/render (with response.mode: "async")The related endpoints for working with the job it creates:
- Get a Render Job — poll one job for its result
- List Render Jobs — your workspace's jobs, newest first
- Cancel a Render Job — cancel a job that has not started
Start an async render#
Add mode: "async" to the response object of a normal render request. Everything else (modifications, videoOptions, Smart Resize, publish) works exactly the same. Async works for any output format: video, PDF, or image.
const res = await fetch("https://api.orshot.com/v1/studio/render", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer <ORSHOT_API_KEY>",
},
body: JSON.stringify({
templateId: <TEMPLATE_ID>,
modifications: { title: "Launch day!" },
response: {
mode: "async", // the one field that makes this a background job
type: "url",
format: "mp4", // or "pdf", "png", …
},
videoOptions: { duration: 30, fps: 30 },
}),
});
const job = await res.json(); // HTTP 202{
"id": 1204,
"status": "queued",
"finished": false,
"created_at": "2026-08-25T10:30:00.000Z",
"self": "https://api.orshot.com/v1/studio/render-jobs/1204"
}Async mode requires response.type: "url" (the default). Results are delivered as hosted URLs, not base64 or binary.
The job object#
Every async endpoint returns this shape. Poll Get a Render Job until finished is true.
| Field | Type | Description |
|---|---|---|
id | number | The job's id. Use it to poll, list, or cancel. |
status | string | queued, processing, succeeded, failed, or canceled. |
finished | boolean | false while the job is queued or processing, true once it reaches a terminal status. Poll until this is true rather than comparing statuses, so new statuses can't break your integration. |
result | object | Only on succeeded. The exact same response body a synchronous render returns (data, format, publish, …), so your handling code is mode-agnostic. |
error | string | Only on failed. A human-readable, actionable message. |
error_code | string | Only on failed. A stable machine code, e.g. video-duration-exceeds-plan, invalid-video-duration, render-timeout. |
metadata | string | Your metadata string, echoed back verbatim (max 1024 chars). |
created_at / started_at / completed_at | string | ISO timestamps for each phase. |
expires_at | string | ISO timestamp when the job record is removed (7 days after creation). The rendered asset is unaffected. |
self | string | The job's poll URL. |
Webhook (optional)#
Pass a webhook_url in the render request and Orshot POSTs the finished job to it, no polling needed.
body: JSON.stringify({
templateId: <TEMPLATE_ID>,
response: { mode: "async", format: "mp4" },
webhook_url: "https://acme.com/hooks/orshot",
metadata: "order-8412", // echoed back so you can correlate
}),The delivery payload:
{
"event": "render_job.finished",
"job": { "id": 1204, "status": "succeeded", "finished": true, "result": { ... }, "metadata": "order-8412" }
}- Delivery is attempted 3 times (immediately, +30s, +2m) with a 10-second timeout each; respond with any 2xx to acknowledge.
- The URL must be publicly reachable over http(s). Private and loopback addresses are rejected at create time.
- Webhooks are a convenience; polling remains the source of truth. If delivery fails, the job is still there to poll.
- Jobs you cancel do not fire a webhook (you already know the outcome). Every other terminal status does.
Statuses & guarantees#
| Status | Meaning |
|---|---|
queued | Accepted; the render has not started yet. |
processing | Rendering now. |
succeeded | Done. result holds the render response. |
failed | Did not complete. error says why and what to change; error_code is stable. |
canceled | You canceled it before it started. Nothing was billed. |
- Renders have a 15-minute ceiling. A job that exceeds it fails with
error_code: "render-timeout"(reduce duration/fps/quality or split pages into separate renders). A render that keeps running and completes just after the ceiling may still be billed, even though the job readsfailed. - Credits are charged exactly as in sync mode, on render completion. A job that never renders (validation failure, or canceled while queued) bills nothing; a render that actually ran is billed even if the job later reads
failed. - Jobs are scoped to your workspace; a job id from another workspace answers
404.
When to use async vs sync#
| Use sync (default) | Use async |
|---|---|
| Images and PDFs (typically < 5s) | Any video render |
| You want the file in one request | Your platform enforces short HTTP timeouts (Zapier, Make, AI agents/MCP) |
| Simplest possible integration | Long or multi-page renders, or fire-many-collect-later pipelines |
Ready to automate?
Start rendering images, PDFs and videos from your templates in under 2 minutes. Free plan, no credit card.
Get your API key- Image, PDF and video generation via API
- Visual editor with AI and smart layouts
- Zapier, Make, MCP and 50+ integrations
- White-label embed for your own app
- 30 free credits — no credit card required