# Start an Async Render

> Add response.mode "async" to any render to run it in the background, then collect the result by polling or webhook. Built for video, big PDFs, and batch pipelines that outlive HTTP timeouts.

- **URL**: https://orshot.com/docs/api-reference/async-render-start

---

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.

```markdown tab="Endpoint"
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](https://orshot.com/docs/api-reference/async-render-job-get) — poll one job for its result
- [List Render Jobs](https://orshot.com/docs/api-reference/async-render-jobs-list) — your workspace's jobs, newest first
- [Cancel a Render Job](https://orshot.com/docs/api-reference/async-render-job-cancel) — cancel a job that has not started

## Start an async render

Add `mode: "async"` to the `response` object of a normal [render request](https://orshot.com/docs/api-reference/render-from-studio-template). Everything else (modifications, `videoOptions`, Smart Resize, publish) works exactly the same. Async works for any output format: video, PDF, or image.

```js {11}
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
```

```js {2-4}
{
  "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](https://orshot.com/docs/api-reference/async-render-job-get) 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.

```js {4}
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:

```js {2}
{
  "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 reads `failed`.
- 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 &lt; 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 |