Skip to content

Render with the Renders API

The Renders API turns a published template version, or HTML your application builds, into a PDF. Each request creates a render with its own ID that you can wait for, download or cancel, and that can notify you through webhooks. To create many at once, see batches. To render a template by name and get the file straight back, or to get an Excel file, use the Reports API instead.

  • An API key with render:create and render:read, plus template:read to look up template versions; see the HTTP API.
  • A published template, unless you render your own HTML; see Build a template.
  • curl or a client library, set up as in the quickstart.

A version ID keeps your PDFs on that version until you change the ID, even after the template is published again. Look the ID up once and put it in your configuration. The clients don’t look up versions, so use curl; the current version’s ID is publishedVersionId:

Terminal window
curl "$PDF_URL/v1/templates?search=Invoice" -H "x-api-key: $API_KEY"

Then render that version with your data:

Terminal window
curl "$PDF_URL/v1/renders" \
-H "x-api-key: $API_KEY" \
-H "content-type: application/json" \
-d '{
"templateVersionId": "c7e0b9a4-1f2d-4c3b-9a8e-5d6f7a8b9c0d",
"data": { "invoice": { "number": "INV-2041" }, "customer": { "name": "Marta Tamm" } },
"waitMs": 10000
}'

With curl, waitMs keeps the request open; see wait in the request. The clients wait, download the PDF and throw if the render fails.

  • data is required. Send {} if the template needs none.
  • Paper, fonts, images and translations come from the version, so there is no settings field.
  • locale, such as de-DE, picks which of the template’s translations to use. Without it, the template’s own language is used.
  • Any version can be rendered, not only the latest. Once the template is deleted, renders of any of its versions answer 404 NOT_FOUND.
  • metadata, encryption, signature and attachments change the PDF’s properties, passwords, signature and embedded files for this render only; see PDF standards and security.
  • destination also uploads the PDF to a bucket of yours; see storage destinations.

Send HTML your code builds as a document, with the paper and margins in settings:

Terminal window
curl "$PDF_URL/v1/renders" \
-H "x-api-key: $API_KEY" \
-H "content-type: application/json" \
-d '{
"document": {
"kind": "html",
"html": "<h1>Invoice INV-2041</h1><p>Total due: 360.00 EUR</p>",
"css": ["h1 { color: #1d3557; }"]
},
"settings": {
"pageSize": { "widthMm": 210, "heightMm": 297 },
"margins": { "topMm": 20, "rightMm": 15, "bottomMm": 20, "leftMm": 15 }
},
"waitMs": 10000
}'
  • Your HTML is used as sent: it is not a Handlebars template, so {{name}} stays text, and JavaScript in it never runs.
  • html is up to 5,000,000 characters. css holds up to 20 stylesheets, applied in order; <style> elements in the HTML work too. See supported CSS.
  • settings takes the same fields as a template’s page.json. pageSize and margins are required. operations is refused with 400 BAD_REQUEST, because only templates add pages from other templates. metadata, encryption and signature are used as written, since there is no data to fill {{path}} placeholders.
  • The request also takes metadata, encryption, signature, attachments and destination, as in step 1.
  • Instead of HTML, "kind": "node" with a node field takes a tree of container, text and image objects. Its fields are in the API reference.

The renderer doesn’t load files from the web. Upload each font, image or stylesheet once (scope asset:manage; see the HTTP API), list its ID in assetIds (up to 100), and use the ID where HTML or CSS takes a URL:

{
"document": {
"kind": "html",
"html": "<img src=\"3f2b6c1d-logo\" width=\"120\"><h1>Invoice INV-2041</h1>",
"css": [
"@font-face { font-family: 'Brand'; src: url('9a7e5d3c-brand'); } body { font-family: 'Brand'; }"
],
"assetIds": ["3f2b6c1d-logo", "9a7e5d3c-brand"]
},
"settings": {
"pageSize": { "widthMm": 210, "heightMm": 297 },
"margins": { "topMm": 20, "rightMm": 15, "bottomMm": 20, "leftMm": 15 }
}
}
Asset File types Use it as
Image PNG, JPEG, SVG <img src="ASSET_ID"> or CSS url("ASSET_ID")
Font TTF, OTF, WOFF2 @font-face { src: url("ASSET_ID"); }
Stylesheet CSS Applied automatically, before css
  • An unknown, deleted or duplicate asset ID answers 400 ASSET_INVALID, and no render is created.
  • Changing or deleting an asset later does not change renders that already exist.
  • Small images can be inlined as data: URLs instead.

A render ends succeeded, failed or cancelled; see render states. The clients’ render method does this step for you. Use the calls below to create a render now and collect the PDF later.

waitMs (0 to 10,000, default 0) keeps POST /v1/renders open until the render ends or the time runs out:

Response When Body
200 The render ended in time. The render: succeeded, failed or cancelled.
202 Time ran out, or waitMs was 0. The render, still queued or processing.

200 means ended, not succeeded: check state. Set your HTTP client’s timeout above waitMs.

Read the render every few seconds until it has ended, or let a webhook tell you:

Terminal window
curl "$PDF_URL/v1/renders/0b6f0f8e-3c55-4c1e-a3a7-8d1a6f2c9e41" \
-H "x-api-key: $API_KEY"

A succeeded render has a result with downloadUrl, pageCount, outputBytes and durationMs, and destination when it was uploaded to your bucket. downloadUrl works without the API key, so you can hand it to a browser, for five minutes (expiresAt):

Terminal window
curl -o invoice.pdf "PASTE-THE-downloadUrl-HERE"

Don’t store downloadUrl: every read of the render gives a fresh one, and the clients fetch it for you when it has expired. Keep your own copy of PDFs you need, because renders are deleted after 30 days. The other result fields are in the API reference.

A render that hasn’t ended can be cancelled (scope render:cancel):

Terminal window
curl -X POST "$PDF_URL/v1/renders/0b6f0f8e-3c55-4c1e-a3a7-8d1a6f2c9e41/cancel" \
-H "x-api-key: $API_KEY"
  • A queued render never starts. A processing render becomes cancelled at once, and no PDF is kept.
  • A render that has already ended answers 409 CONFLICT; an unknown ID answers 404 NOT_FOUND.

In Studio, Render jobs lists the workspace’s renders, newest first, with a Status filter. Open one to see its PDF and Diagnostics, then Download PDF or Cancel render.

Through the API, GET /v1/renders (scope render:read) lists them the same way, paged with limit and cursor. Add ?state=failed, or any other state, to see only those:

Terminal window
curl "$PDF_URL/v1/renders?state=failed" -H "x-api-key: $API_KEY"

List items have no result, error or download link; read the single render for those.

queued

processing

succeeded

failed

cancelled

cancel while queued or processing

final states

State Ended Meaning
queued No Accepted and waiting to start.
processing No Rendering, or waiting to try again after a temporary problem.
succeeded Yes The PDF is stored. The render has a result.
failed Yes No PDF could be made. The render has an error.
cancelled Yes You cancelled it before it ended. It has neither result nor error.

A render that has ended never changes state again.

A render that fails is not an HTTP error: the request succeeded, and the render’s state becomes failed with an error that says why. A render.failed webhook event carries only its code.

{
"id": "5e2a7c1b-9d0f-4b8e-a6c3-2f1e0d9c8b7a",
"state": "failed",
"createdAt": "2026-10-08T09:20:11.002Z",
"updatedAt": "2026-10-08T09:20:13.480Z",
"error": {
"code": "LIMIT_EXCEEDED",
"message": "Reduce the document length to fit the page limit.",
"retryable": false,
"diagnostics": [
{ "code": "max_pages", "message": "Reduce the document length to fit the page limit." }
]
}
}
Field Description
code ASSET_INVALID, UNSUPPORTED_FEATURE, LIMIT_EXCEEDED or RENDER_FAILED.
message What to change.
retryable Whether creating the same render again later might work. See retries.
diagnostics Optional. Each problem, with a code, a message and sometimes a path.
details Optional. The same problems as { path?, message }, as in other API errors.

What each code and diagnostic means, and what to do about it, is under render failures in the HTTP API. Other optional fields are for support requests; don’t branch on them.

Temporary problems are retried for you while the render stays processing. So when you see failed:

  • retryable: false: the problem is in the request (the document, its files or settings), and the same request will fail again. Fix it first.
  • retryable: true: the service had a temporary problem on every attempt. Creating the render again later may work. If it keeps happening, tell whoever runs your installation.

To render again, create a new render with a new Idempotency-Key: the old key returns the same failed render.

If creating a render times out or the connection drops, send it again with the same Idempotency-Key header (idempotencyKey in the .NET and TypeScript clients, idempotency_key in Python). You get the render the first request created instead of a second one. Use a value that names the document, such as invoice-2026-0042.

  • A key is 1 to 255 characters, per workspace.
  • A repeat returns the first render in its current state, and waits for it if it has waitMs. Repeats don’t count against your render allowances.
  • The key belongs to the method, path and body. Field order in the JSON doesn’t matter, but any other change does, including a different waitMs: reusing a key for a different request answers 409 CONFLICT.
  • Two identical requests sent at the same moment still create only one render.
  • A key is remembered as long as its render exists. After retention deletes the render, the key can be used again.
  • POST /v1/renders and the Reports API’s PDF requests share one set of keys. Batches have their own.

Set format to png, jpeg or webp to get one page of the document as an image instead of the PDF, for a thumbnail, a preview or a social card.

Terminal window
curl "$PDF_URL/v1/renders" \
-H "x-api-key: $API_KEY" \
-H "content-type: application/json" \
-d '{
"templateVersionId": "c7e0b9a4-1f2d-4c3b-9a8e-5d6f7a8b9c0d",
"data": { "invoice": { "number": "INV-2041" }, "customer": { "name": "Marta Tamm" } },
"format": "png",
"image": { "page": 1, "width": 400 },
"waitMs": 10000
}'
image field Default
page 1. The page to draw, counted from 1 in the finished PDF, after pageRanges.
width The page’s width in CSS pixels, at most 4096. From 16 to 4096; the height keeps the page’s shape.
quality 90. JPEG only, from 1 to 100.
  • The image shows the page as the PDF does, with its header and footer, page numbers and watermarks. A test render draws “TEST” on it too.
  • For a social card (an Open Graph image), make the template’s page 1200 × 630 px (317.5 × 166.7 mm) with no margins. The image is then 1200 × 630 pixels.
  • webp images are lossless. jpeg is smallest for photos.
  • An image counts as one document, like a PDF. The render’s result.mediaType is the image’s type, pageCount is 1, and the download link serves the image.
  • encryption, signature, attachments and checkConformance are for PDFs, so with an image they answer 400 BAD_REQUEST, and so do settings.encryption, signature, pdfa and pdfua on your own HTML. A template’s own passwords, signature, attachments, PDF/A and PDF/UA are left out.
  • A page the document doesn’t have fails the render with the diagnostic image_page_missing. An image taller than 8192 pixels fails with LIMIT_EXCEEDED and image_too_large; ask for a smaller width.
  • Batch items take format and image too, but a batch with images can’t be downloaded as one PDF. The Reports API returns the image in the response.

Add watermark to a render request to draw text such as DRAFT, COPY or PAID across every page, in front of the content.

Terminal window
curl "$PDF_URL/v1/renders" \
-H "x-api-key: $API_KEY" \
-H "content-type: application/json" \
-d '{
"templateVersionId": "c7e0b9a4-1f2d-4c3b-9a8e-5d6f7a8b9c0d",
"data": { "invoice": { "number": "INV-2041" }, "customer": { "name": "Marta Tamm" } },
"watermark": { "text": "DRAFT", "color": "#c00000", "opacity": 0.2 },
"waitMs": 10000
}'
Field Default
text Required. One line, up to 200 characters.
opacity 0.3. From 0 (invisible) to 1 (solid).
rotation Along the page’s diagonal. Degrees counter-clockwise; 0 is horizontal.
fontSize Fills 60% of the page along the text’s angle, at most 144 pt. In points.
color #808080 (gray). A hex colour such as #c00000.
  • The text is centred on each page in a bold sans-serif font. It covers Latin, Greek and Cyrillic letters; other characters fail the render with UNSUPPORTED_FEATURE and the diagnostic watermark_text_unsupported.
  • The watermark is not part of the document’s text: it can’t be selected or searched, and screen readers skip it. PDF/A and PDF/UA files stay valid.
  • Every request shape takes watermark, and so does the Reports API in pdf. With test, both watermarks are drawn.
  • For a logo or styled text, draw another template over every page instead; see Combine templates.

Add "test": true to a render request to get a free test PDF or image. It is the same document with a large, translucent “TEST” drawn diagonally across every page. Use it while you build and check your integration.

Terminal window
curl "$PDF_URL/v1/renders" \
-H "x-api-key: $API_KEY" \
-H "content-type: application/json" \
-d '{
"templateVersionId": "c7e0b9a4-1f2d-4c3b-9a8e-5d6f7a8b9c0d",
"data": { "invoice": { "number": "INV-2041" }, "customer": { "name": "Marta Tamm" } },
"test": true,
"waitMs": 10000
}'
  • Test renders don’t count against your workspace’s render allowances. They have their own: 10 queued or processing and 10 created per minute, per workspace, or your own allowance if lower. Over either, the request answers 429 RATE_LIMITED with Retry-After.
  • Every request shape takes test: published versions, your own HTML, previews and batch items. So does the Reports API for PDFs.
  • On a workspace with a monthly document allowance, every PDF preview is a test render, with or without test, and test renders have the workspace’s own allowances.
  • Otherwise a test render is an ordinary render: it has an ID, appears in lists, sends webhooks and counts in usage.
  • A render’s test field, and data.test in its webhook events, is true for a test render and false otherwise.

Renders that have ended, and their PDFs, are deleted 30 days after they were created. After that, reading the render answers 404 NOT_FOUND, and it is gone from lists and batches. Renders still queued or processing are never deleted. On a self-hosted installation, set the period with RENDER_RETENTION_DAYS in .env; see configuration.

A preview request renders template content you send, without saving or publishing anything, and needs template:read as well. The result is an ordinary render you wait for and download. The clients don’t send previews, so use curl:

Terminal window
curl "$PDF_URL/v1/renders" \
-H "x-api-key: $API_KEY" \
-H "content-type: application/json" \
-d '{
"preview": {
"composition": { "root": { "type": "html", "html": "<h1>Invoice {{invoice.number}}</h1>" } },
"settings": {
"pageSize": { "widthMm": 210, "heightMm": 297 },
"margins": { "topMm": 20, "rightMm": 15, "bottomMm": 20, "leftMm": 15 }
}
},
"data": { "invoice": { "number": "INV-2041" } },
"waitMs": 10000
}'
  • preview takes the same object as the content of a template’s draft (GET /v1/templates/{id}/draft).
  • templateId names a template whose folder the draft’s workspace files are found from. Without it, paths start at the workspace root.
  • data replaces the draft’s sample data.
  • "checkConformance": true renders a plain PDF and lists in result.conformance which PDF/A levels and PDF/UA parts it meets; see PDF standards and security.

To get the file straight back instead, use POST /v1/templates/{id}/preview; see templates. Every field is in the API reference.