Async renders
Set async to true on a render to get a job id at once. Then check the job, or let JamFrame call your URL when the files are ready.
A normal POST /v1/render keeps the connection open until the files are ready. An async render answers at
once with a job id and renders in the background. You then check the job, or get a call when it ends.
When to use async
- Your code can’t wait, for example in a web request that has to answer quickly.
- You start many renders at once and want to handle each one as it finishes.
- You want JamFrame to call your server when the files are ready, instead of waiting.
- A render takes a while, such as a PDF with many pages.
An async render has the same limits as a normal one, including the time limit. It only means your code doesn’t wait. See Limits.
response.sizes can’t run async. To make several sizes in the background, send one async request per
size, each with response.size. See Smart Resize.
Start an async render
Add "async": true to a normal render request:
curl -i https://api.jamframe.com/v1/render \
-H "Authorization: Bearer $JAMFRAME_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"templateId": "clx8m2k3p0003abcd1234efgh",
"async": true,
"modifications": { "customer_name": "Jane Doe" },
"response": { "format": "pdf" },
"expiresIn": 3600
}'const res = await fetch("https://api.jamframe.com/v1/render", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.JAMFRAME_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
templateId: "clx8m2k3p0003abcd1234efgh",
async: true,
modifications: { customer_name: "Jane Doe" },
response: { format: "pdf" },
expiresIn: 3600,
}),
});
const { data } = await res.json(); // res.status is 202
console.log(data.jobId);import os
import requests
res = requests.post(
"https://api.jamframe.com/v1/render",
headers={"Authorization": f"Bearer {os.environ['JAMFRAME_API_KEY']}"},
json={
"templateId": "clx8m2k3p0003abcd1234efgh",
"async": True,
"modifications": {"customer_name": "Jane Doe"},
"response": {"format": "pdf"},
"expiresIn": 3600,
},
)
print(res.status_code, res.json()["data"]["jobId"]) # 202 and the job idThe answer is 202 Accepted. Its Location header points at the job, such as
/v1/render-jobs/clx8n0q1r0008abcd1234efgh:
{ "data": { "jobId": "clx8n0q1r0008abcd1234efgh", "status": "queued" } }
JamFrame checks the request before it queues the job. An unknown template (404), a wrong key or value
(400) or too few credits (402) fail at once, as they would for a normal render.
Async renders always answer with links. "type": "base64" with async is a 400.
Check the job
GET /v1/render-jobs/{id} returns the job. Ask about once a second until status is succeeded or
failed.
curl https://api.jamframe.com/v1/render-jobs/clx8n0q1r0008abcd1234efgh \
-H "Authorization: Bearer $JAMFRAME_API_KEY"
A finished job:
{
"job": {
"id": "clx8n0q1r0008abcd1234efgh",
"status": "succeeded",
"progress": 1,
"format": "pdf",
"templateId": "clx8m2k3p0003abcd1234efgh",
"createdAt": "2026-11-14T12:36:24.161Z",
"startedAt": "2026-11-14T12:36:24.165Z",
"finishedAt": "2026-11-14T12:36:31.786Z",
"result": {
"content": "https://cdn.jamframe.com/renders/ws_abc123/c724edf3.pdf?Expires=…",
"expiresAt": "2026-11-14T13:36:40.000Z",
"type": "url",
"format": "pdf",
"pages": [1, 2, 3],
"credits": 3,
"renderId": "clx8n0q1r0009abcd1234efgh",
"responseTime": 7604
},
"error": null
}
}
| Field | What it holds |
|---|---|
status |
queued, running, succeeded or failed. The last two are final. |
progress |
From 0 to 1. Images and PDFs jump from 0 to 1 when they’re done. |
format |
The file type. |
templateId |
The template, or null if it has been deleted since. |
createdAt, startedAt, finishedAt |
When the job was queued, started and ended. |
result |
When succeeded: the same object a normal render returns as data. Otherwise null. |
error |
When failed: the error a normal render would have returned, with error and message. Otherwise null. |
warnings |
As in a normal render, such as keys that matched nothing. Only there when there are some. |
- Fresh links. Every time you read the job, its links are new. They work for the request’s
expiresInfrom that moment, for as long as the files exist (8 days). So you can read a job again later to get working links. - Credits. A job is charged when it succeeds. A failed job costs nothing.
- Other workspaces. A job from another workspace answers
404.
A simple way to wait for a job in Node.js:
async function waitForJob(jobId) {
for (;;) {
const res = await fetch(`https://api.jamframe.com/v1/render-jobs/${jobId}`, {
headers: { Authorization: `Bearer ${process.env.JAMFRAME_API_KEY}` },
});
const { job } = await res.json();
if (job.status === "succeeded") return job.result;
if (job.status === "failed") throw new Error(`${job.error.error}: ${job.error.message}`);
await new Promise((resolve) => setTimeout(resolve, 1000));
}
}
Get a callback with webhookUrl
Instead of checking the job, add a webhookUrl. When the job ends, JamFrame sends a POST to that URL:
{
"templateId": "clx8m2k3p0003abcd1234efgh",
"async": true,
"webhookUrl": "https://example.com/hooks/jamframe/8f3c1d9e2b",
"modifications": { "customer_name": "Jane Doe" },
"response": { "format": "pdf" }
}
The call has these headers:
| Header | Value |
|---|---|
Content-Type |
application/json |
User-Agent |
JamFrame-Webhook/1 |
X-Webhook-Event |
render.completed or render.failed |
X-Render-Job |
The job id |
The body holds the event and the same job object that GET /v1/render-jobs/{id} returns:
{
"event": "render.completed",
"job": {
"id": "clx8n0q1r0008abcd1234efgh",
"status": "succeeded",
"result": { "content": "https://cdn.jamframe.com/renders/…", "credits": 3 },
"error": null
}
}
Rules for the callback:
- The URL must start with
https://and point at a public address. If not, the request is refused with400 WEBHOOK_URL_INVALID. webhookUrlworks only with"async": true. On a normal render it’s a400.- Answer with any 2xx status within 10 seconds.
- If your server can’t be reached, takes too long, or answers
408,429or a 5xx, JamFrame tries twice more: after 5 seconds, then 30 seconds later. Any other answer is final. Redirects aren’t followed. - The links in the callback are made when it’s sent, and work for the request’s
expiresIn.
webhookUrl or account webhooks?
You can also subscribe to render.completed and render.failed events for the whole workspace. They fire
for every async render, whoever started it.
webhookUrl |
Account webhooks | |
|---|---|---|
| Set up | In each request | Once, for the workspace |
| Which renders | This one job | Every async render in the workspace |
| Signed | No | Yes, so you can check they came from JamFrame |
| Retries | 2 more tries, within about 35 seconds | Several, over more than a day |
Normal renders, the ones that wait for the answer, don’t send these events. To set up account webhooks, see Webhooks.
Related
- Every field of a render request: Render images and PDFs.
- Events for the whole workspace: Webhooks.
- What to do when a job fails: Errors.
Last updated
Still stuck?
Email [email protected] and a real person will reply. Tell us the template you were using and what you expected to happen.