Skip to content
JamFrame
Docs menu

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 id

The 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 expiresIn from 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 with 400 WEBHOOK_URL_INVALID.
  • webhookUrl works only with "async": true. On a normal render it’s a 400.
  • Answer with any 2xx status within 10 seconds.
  • If your server can’t be reached, takes too long, or answers 408, 429 or 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.

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.