Renders
Create PDFs and images with POST /v1/renders, from the request body and sync or async modes to delivery, idempotency, listing and rate limits.
A render is one document generation job. Every PDF, image and HTML output is created through the same resource, POST /v1/renders, whatever the input: a stored template, raw HTML, a web page URL or Markdown.
Create a render
curl https://api-eu.dynamicdocumentapi.com/v1/renders \
-H "Authorization: Bearer $DYNAMIC_DOCUMENT_API_KEY" \
-H "Idempotency-Key: invoice-2026-0042" \
-H "Content-Type: application/json" \
-d '{
"input": { "type": "template", "template_id": "tpl_01J9ZK3M7Q8V5W2X4Y6Z8A0B1C", "version": "live" },
"data": { "number": "2026-0042", "customer_id": "c_981", "items": [ { "sku": "A1", "qty": 2 } ] },
"output": { "format": "pdf", "filename": "invoice-{{ data.number }}.pdf" },
"delivery": { "type": "url", "expires_in": 3600 },
"reference": "inv_2026_0042",
"metadata": { "customer_id": "c_981" }
}'Possible responses:
| Status | When | Body |
|---|---|---|
200 OK | A sync render succeeded | The render object, or the file itself with delivery.type: "binary" |
202 Accepted | An async render was accepted, or a sync render was still running when the sync timeout was reached | The render object with status queued or processing, and a Location: /v1/renders/{id} header |
408 Request Timeout | A sync render reached the sync timeout and timeout_behavior is cancel | Problem details with code render_timeout |
422 Unprocessable Entity | A sync render failed, for example because of a template error or a page that couldn't be loaded | Problem details with render_id and the render error |
Other 4xx and 5xx | Invalid requests, authentication, limits, outages | Problem details; see Errors |
Request body
| Field | Type | Default | Description |
|---|---|---|---|
input | object | required | What to render. See input types. |
data | object | none | JSON data for the template engine |
data_url | string | none | URL to fetch the JSON data from instead of data. See data. |
output | object | Output format, filename and format options. See output. | |
delivery | object | url delivery, or none without a hosted copy | How you receive the files. See delivery. |
mode | string | sync | sync waits for the result; async returns immediately. See sync and async. |
timeout_behavior | string | continue | Set to cancel to cancel a sync render that reaches the sync timeout instead of letting it finish in the background |
webhook | object | none | Per-request webhook: url and events, for example ["render.succeeded", "render.failed"]. See Webhooks. |
reference | string | none | Your own identifier, such as an invoice number. Returned on the render and usable as a list filter. |
metadata | object | none | Up to 50 key-value pairs, values up to 500 characters. Returned on the render and usable as list filters. |
engine | string | template or workspace default | Engine channel to render with, for example "2026.4". See engine channels. |
priority | string | normal | normal or low. low puts an async render in the lower-priority queue that batch items use; sync renders ignore it. A cheaper price for low is planned. |
test | boolean | false | Marks a test render. Test keys always create test renders, and true with a live key fails with test_key_required. |
Input types
Set input.type and the fields for that type.
template
Renders a template stored in your workspace.
| Field | Description |
|---|---|
template_id | Template ID (tpl_…), shown in the editor and returned by GET /v1/templates |
version | "live" (default) for the currently published version, a version number such as 7 to pin one, or "draft" for the unpublished draft (test keys; live keys only if your workspace settings allow it) |
The template's stored output options apply, and anything you send in output overrides them.
html
Renders an HTML document you send in the request.
| Field | Description |
|---|---|
html | Body markup. The template language is available, for example <h1>Hello {{ name }}</h1>. |
head | Optional contents of <head>: <style>, <link>, <script> and <meta> elements |
templating | Whether to run the template engine on html and head. Defaults to true when data is present, otherwise false. |
url
Loads a web page and renders it.
| Field | Description |
|---|---|
url | The page to render |
http.headers | Extra request headers, for example {"Authorization": "Bearer …"} |
http.cookies | Cookies to set, each with name, value and domain |
http.basic_auth | username and password for HTTP basic authentication |
http.user_agent | A custom User-Agent string |
Headers, cookies and basic authentication are only sent to the same registrable domain as url, not to third-party assets on the page. Pages and assets must be publicly reachable over the standard ports (80, 443, 8080 and 8443): private and internal addresses are blocked with url_not_allowed. Blocking ads and trackers and hiding elements such as cookie banners are planned.
markdown
Converts GitHub Flavored Markdown (tables, task lists, footnotes, strikethrough and autolinks) to a styled document.
| Field | Description |
|---|---|
markdown | Markdown source. The template language runs first, so expressions can produce Markdown. |
theme | default, github, academic, minimal or none |
css | Additional CSS applied after the theme |
Raw HTML inside Markdown is sanitized unless your workspace allows raw HTML.
Data
Keys in data become top-level variables in the template: with "data": {"customer": {"name": "Example GmbH"}}, the template uses {{ customer.name }}. In output.filename, the data is available under data, as in invoice-{{ data.number }}.pdf.
For template renders you can send data_url instead. The API fetches the JSON document from that URL (up to 50 MB) through the same network policy as page assets. The URL must be on the template's list of allowed data sources, and a failed fetch fails the render with data_url_fetch_failed.
The size of the request body is limited by plan, from 2 MB on Free to 50 MB on Scale. Templates can enforce a JSON Schema for their data; a mismatch fails with 422 data_schema_mismatch, and errors[] lists each problem with a JSON Pointer path.
Output
| Field | Description |
|---|---|
format | pdf, png, jpeg, webp or html |
filename | File name, with template expressions allowed, for example invoice-{{ data.number }}.pdf |
pdf | PDF options such as paper size, margins, headers and footers, PDF/UA, PDF/A and attachments |
image | Image options such as viewport, scale and quality |
einvoice | Turns the PDF into an e-invoice: Factur-X, ZUGFeRD or XRechnung, with the invoice XML embedded. Replaces a template's default e-invoice; null turns that default off. See E-invoicing. |
The html format returns the rendered HTML document. For standalone e-invoice XML, use POST /v1/einvoices, described in E-invoicing.
Delivery
| Field | Default | Description |
|---|---|---|
type | url, or none without a hosted copy | url, binary, base64 or none. Without a hosted copy, url fails with 400 validation_error. See receiving files. |
expires_in | 3600 | Lifetime of signed URLs in seconds, from 60 seconds to 7 days |
retention_days | workspace setting | How long the hosted file is kept, up to your plan's maximum |
retention | none | "none" for zero-retention delivery |
hosted | true, unless your destinations keep no copy | false skips the hosted copy and only uploads to your storage destinations. When you leave it out, it is false if every destination of the render (those in storage, else your default destination) has keep_hosted_copy: false. Zero-retention renders never keep one. See Hosted copy. |
storage | your default destination | Uploads to your own storage buckets: up to 10 entries, each with a destination_id and an optional path. [] uploads nowhere. See Your own storage. |
email | the template's rules | Email rules to run: false for none, or a list such as [{"rule_id": "emr_…"}] for only those. See Email delivery. |
Sync and async
Sync mode
mode: "sync" is the default. The API waits for the render and responds with the result, up to your plan's sync timeout:
| Plan | Sync timeout | Async maximum duration |
|---|---|---|
| Free | 30 s | 60 s |
| Starter | 60 s | 5 min |
| Growth | 60 s | 10 min |
| Pro | 90 s | 15 min |
| Scale | 120 s | 30 min |
| Enterprise | 300 s | custom |
If a sync render is still running when the timeout is reached, the API responds with 202 Accepted and the render continues in the background, exactly like an async render. Set timeout_behavior: "cancel" if you'd rather have the render canceled; the response is then 408 with the code render_timeout.
Set your HTTP client's timeout a little above your plan's sync timeout so that you receive the 202 response instead of closing the connection.
Async mode
With mode: "async", the API responds immediately with 202 Accepted, the render object and a Location header. Wait for a webhook or poll the render. A render that runs longer than your plan's async maximum duration fails with the code render_timeout.
curl -i https://api-eu.dynamicdocumentapi.com/v1/renders \
-H "Authorization: Bearer $DYNAMIC_DOCUMENT_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"input": { "type": "url", "url": "https://example.com/reports/42" },
"output": { "format": "pdf" },
"mode": "async",
"webhook": { "url": "https://example.com/webhooks/documents", "events": ["render.succeeded", "render.failed"] },
"reference": "report-42"
}'HTTP/1.1 202 Accepted
Location: /v1/renders/rnd_01J9ZM1X3F7R8K2C4V6B8N0P2Q
Content-Type: application/json
{"id": "rnd_01J9ZM1X3F7R8K2C4V6B8N0P2Q", "object": "render", "status": "queued", "mode": "async"}The response body above is shortened.
Poll for the result
Webhooks are the most efficient way to learn that a render finished. If you poll, back off between requests:
async function waitForRender(id: string, timeoutMs = 10 * 60_000) {
const deadline = Date.now() + timeoutMs;
let delayMs = 1000;
while (Date.now() < deadline) {
const response = await fetch(`https://api-eu.dynamicdocumentapi.com/v1/renders/${id}`, {
headers: { Authorization: `Bearer ${process.env.DYNAMIC_DOCUMENT_API_KEY}` },
});
if (!response.ok) throw new Error(`Retrieve failed (${response.status}): ${await response.text()}`);
const render = await response.json();
if (render.status !== "queued" && render.status !== "processing") return render;
await new Promise((resolve) => setTimeout(resolve, delayMs));
delayMs = Math.min(delayMs * 2, 5000);
}
throw new Error(`Render ${id} did not finish in time`);
}Receiving files
Delivery types
delivery.type | Response | Size limit |
|---|---|---|
url | The render object. Each file has a signed url and url_expires_at. | none |
binary | The file as the response body, with Content-Type, Content-Disposition, X-Render-Id, X-Pages and X-Billed-Renders headers | 20 MB |
base64 | The render object in JSON, with the file content base64-encoded | 10 MB |
none | The render object without file URLs, for example when the files go to your storage destinations | none |
Without delivery.type, a render that keeps a hosted copy gets url, and one that keeps none gets none: with hosted: false, under zero retention, or when every storage destination of the render has keep_hosted_copy: false. Such a render answers without file URLs, and its files go to your storage destinations and email rules only. An explicit url without a hosted copy fails with 400 validation_error; see Hosted copy.
binary and base64 are the simplest choice when your code stores the file itself. Files larger than the limit fail with 413 output_too_large; use url delivery for them. If a sync render with binary or base64 delivery returns 202 because it reached the sync timeout, retrieve the render once it has succeeded.
Signed URLs
Generated files are private. File URLs are signed and expire after delivery.expires_in seconds (one hour by default, at most seven days).
GET /v1/renders/{id}returns fresh signed URLs for as long as the files are retained, so store the render ID rather than the URL.GET /v1/renders/{id}/files/{file_id}redirects (302) to a fresh signed URL. Add?download=trueto get an attachmentContent-Disposition, or?inline=trueto stream the file through the API.
File retention
Hosted files are kept for your workspace's default retention period: 1, 7, 30, 90 or 365 days, or forever on paid plans, up to your plan's maximum. Override it per request with delivery.retention_days. When the period ends, the files are deleted, the render's status changes to expired and a render.expired webhook event is sent. Downloading an expired file fails with 410 file_expired.
To delete files before then, call DELETE /v1/renders/{id}/files. The hosted copy and any cached copies are purged immediately and the render becomes expired. Copies in your storage destinations stay.
Zero-retention delivery
For sensitive documents, set delivery.retention to "none", or make it the default in your workspace settings:
{
"input": { "type": "template", "template_id": "tpl_01J9ZK3M7Q8V5W2X4Y6Z8A0B1C" },
"data": { "employee": { "name": "Alex Example" }, "salary": 5400 },
"output": { "format": "pdf" },
"delivery": { "type": "binary", "retention": "none" }
}With zero retention:
- no hosted copy is kept: the file is returned in the response with sync
binaryorbase64delivery, or uploaded to your storage destinations. Uploading is the default withoutdelivery.type, and it also works for async renders and batches. For the upload, each file is kept as a transient copy until it has reached every destination, at most about a day, and the API never serves it. urldelivery, email rules and the ZIP and merged PDF of a batch aren't available- request and response payloads are not logged; only metadata is recorded
- your data is processed in memory and is not written to disk
- the render record keeps only non-personal metadata such as the ID, status, timings and billed renders
The render object
{
"id": "rnd_01J9ZM1X3F7R8K2C4V6B8N0P2Q",
"object": "render",
"status": "succeeded",
"mode": "sync",
"test": false,
"region": "eu",
"source": "api",
"input_type": "template",
"template": { "id": "tpl_01J9ZK3M7Q8V5W2X4Y6Z8A0B1C", "version": 7 },
"engine": "2026.4",
"output_format": "pdf",
"files": [
{
"id": "file_01J9ZM4T6W8Y0A2C4E6G8J0K2M",
"format": "pdf",
"variant": null,
"filename": "invoice-2026-0042.pdf",
"bytes": 48213,
"pages": 2,
"width": null,
"height": null,
"url": "https://files-eu.dynamicdocumentapi.com/f/…",
"url_expires_at": "2026-09-17T16:06:34Z",
"sha256": "9f2c…"
}
],
"storage": [],
"email": [],
"billed_renders": 1,
"timings": { "queued_ms": 3, "render_ms": 612, "postprocess_ms": 21, "upload_ms": 38, "total_ms": 681 },
"warnings": [
{ "code": "slow_asset", "message": "https://cdn.example.com/logo.png took 2.4s" }
],
"error": null,
"result": null,
"reference": "inv_2026_0042",
"metadata": { "customer_id": "c_981" },
"batch_id": null,
"created_at": "2026-09-17T15:06:33Z",
"completed_at": "2026-09-17T15:06:34Z",
"expires_at": "2026-10-17T15:06:34Z"
}| Field | Description |
|---|---|
id | Render ID (rnd_…) |
object | Always render |
status | See status values |
mode | sync or async |
test | true for test renders |
region | Region that processed the render, such as eu |
source | Where the request came from, such as api or dashboard |
input_type | template, html, url, markdown, pdf_tool for the PDF tools, or einvoice for XML exported with POST /v1/einvoices |
template | Template ID and the version number that was rendered (template renders only) |
engine | Engine channel used |
output_format | The requested output format |
files | Output files; see the table below |
storage | Upload result per storage destination: destination_id, status (pending, succeeded, failed or skipped), location and error |
email | The render's email sends, each with id, rule_id, status, to, provider_message_id, error and sent_at. See Email delivery. |
billed_renders | Renders billed for this request, a whole number (0 for test, failed and canceled renders) |
timings | Milliseconds spent queued, rendering, post-processing, uploading and in total |
warnings | Non-fatal problems such as slow or missing assets. See render warnings. |
error | For failed renders: code and message, plus line, column and excerpt for template errors |
result | Machine-readable results, otherwise null. PDF/UA, PDF/A and e-invoice renders carry their validation report in result.conformance, both when they succeed and when they fail. |
reference, metadata | Values from your request |
batch_id | The batch the render belongs to, otherwise null |
created_at, completed_at | When the render was created and finished |
expires_at | When the files will be deleted |
Each entry in files has:
| Field | Description |
|---|---|
id | File ID (file_…) |
format | File format, such as pdf, png or xml |
variant | Size variant of a multi-size image render (planned); otherwise null |
filename | File name |
bytes | File size in bytes |
pages | Number of pages (PDF) |
width, height | Dimensions in pixels (images) |
url | Signed download URL. Omitted when no hosted copy exists. |
url_expires_at | When url stops working |
sha256 | SHA-256 checksum of the file |
Status values
| Status | Meaning |
|---|---|
queued | Accepted and waiting for a renderer |
processing | Rendering |
succeeded | Finished; files are available |
failed | Failed; error explains why. Nothing is billed. |
canceled | Canceled with POST /v1/renders/{id}/cancel |
expired | The files were deleted at the end of the retention period or purged |
Convenience endpoints
These endpoints are thin wrappers around POST /v1/renders with a flatter body, which is handy in no-code tools and quick scripts. They return the same responses.
| Endpoint | Body | Equivalent |
|---|---|---|
POST /v1/pdf/from-template | template_id, version, data, pdf | input.type: "template", output.format: "pdf" |
POST /v1/pdf/from-html | html, head, data, pdf | input.type: "html", output.format: "pdf" |
POST /v1/pdf/from-url | url, http, pdf | input.type: "url", output.format: "pdf" |
POST /v1/pdf/from-markdown | markdown, theme, css, data, pdf | input.type: "markdown", output.format: "pdf" |
POST /v1/images/from-template | template_id, data, image, format | input.type: "template", PNG by default |
POST /v1/images/from-html | html, head, data, image, format | HTML screenshot |
POST /v1/images/from-url | url, image, format | URL screenshot |
The image endpoints produce PNG unless format is jpeg or webp. delivery, mode, webhook, reference, metadata, test and filename are accepted at the top level of these bodies:
curl https://api-eu.dynamicdocumentapi.com/v1/pdf/from-html \
-H "Authorization: Bearer $DYNAMIC_DOCUMENT_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"html":"<h1>Hello {{ name }}</h1>","data":{"name":"World"},"pdf":{"paper":{"size":"A4"}},"filename":"hello.pdf"}'Idempotency
Network errors and timeouts leave you unsure whether a request was processed. Send an Idempotency-Key header (up to 255 characters) with every POST request and reuse it when you retry:
- The same key with the same body within 24 hours returns the original response. No second render is created and nothing is billed again.
- If the original request is still being processed, the retry fails with
409 idempotency_in_progress. Wait briefly and retry with the same key. - The same key with a different body fails with
422 idempotency_key_reused.
Keys are scoped to your workspace. Derive them from the business event when you can, such as invoice-2026-0042, or generate a UUID per logical operation and store it with your retry state.
List renders
GET /v1/renders returns renders newest first, with cursor pagination:
curl -G https://api-eu.dynamicdocumentapi.com/v1/renders \
-H "Authorization: Bearer $DYNAMIC_DOCUMENT_API_KEY" \
--data-urlencode "status=failed" \
--data-urlencode "created[gte]=2026-09-01T00:00:00Z" \
--data-urlencode "metadata[customer_id]=c_981" \
--data-urlencode "limit=50"{
"object": "list",
"data": [
{ "id": "rnd_01J9ZM1X3F7R8K2C4V6B8N0P2Q", "object": "render", "status": "failed" }
],
"has_more": true,
"next_cursor": "eyJjcmVhdGVkX2F0IjoiMjAyNi0wOS0xN1QxNTowNjozM1oifQ"
}To fetch the next page, repeat the request with the same filters and cursor set to next_cursor. Stop when has_more is false.
| Parameter | Description |
|---|---|
status | queued, processing, succeeded, failed, canceled or expired |
template_id | Renders of one template |
input_type | template, html, url, markdown, pdf_tool or einvoice |
output_format | pdf, png, jpeg, webp, html, xml or zip |
source | Origin of the render, such as api or dashboard |
reference | Your reference value |
batch_id | Renders of one batch |
test | true or false |
created[gte], created[lte] | Creation time range, as RFC 3339 timestamps |
metadata[key] | A metadata value, for example metadata[customer_id]=c_981 |
limit | Page size (default 50) |
cursor | The next_cursor value from the previous page |
Other render endpoints
| Endpoint | Description |
|---|---|
GET /v1/renders/{id} | Retrieve a render, with fresh signed file URLs |
GET /v1/renders/{id}/files/{file_id} | Redirect to a file's signed URL. ?download=true sets an attachment disposition, ?inline=true streams the file through the API. |
POST /v1/renders/{id}/cancel | Cancel a queued or processing render. Canceling is best effort, and nothing is billed if rendering hadn't started. |
DELETE /v1/renders/{id}/files | Delete the hosted files now; the render becomes expired |
GET /v1/renders/{id}/logs | The redacted request and response log, if request logging is enabled for your workspace |
Request logs are kept for 7 days on Starter and Growth, 30 days on Pro and 90 days on Scale, with redaction rules you configure in the dashboard.
Rate limits
Limits depend on your plan:
| Plan | Sustained (requests/s) | Burst (requests/s) | Concurrent sync renders |
|---|---|---|---|
| Free | 2 | 5 | 2 |
| Starter | 10 | 20 | 10 |
| Growth | 20 | 40 | 20 |
| Pro | 50 | 100 | 50 |
| Scale | 150 | 300 | 150 |
| Enterprise | custom | custom | custom |
Responses include the current policy and your remaining quota:
RateLimit-Policy: "burst";q=20;w=1, "sustained";q=600;w=60
RateLimit: "burst";r=17;t=1q is the quota for a window of w seconds, r is the number of requests remaining and t is the number of seconds until the window resets.
When you exceed a limit, the API responds with 429 and a Retry-After header. The code is rate_limited when you send requests too fast and concurrency_limited when too many sync renders are running at once. Wait for Retry-After seconds before retrying. For large volumes, use async mode and spread requests over time. See Plans and limits for all limits.
Billed renders
Every successful live render is billed as one or more renders, reported in the render's billed_renders field and, for binary delivery, in the X-Billed-Renders header.
| Render | Billed renders |
|---|---|
| PDF (template, HTML, URL or Markdown), up to 50 pages | 1 |
| Each additional 50 pages | +1 |
| E-invoice layer on a PDF | included (+0) |
E-invoice XML (POST /v1/einvoices) | 1 |
| Image (PNG, JPEG or WebP) | 1 |
| Test render, failed render, render canceled before it started | 0 |
A PDF over your plan's page limit fails with 422 page_limit_exceeded and isn't billed. See Plans and limits for the full render table.
Engine channels
The renderer is released in engine channels named YYYY.N, such as 2026.4. A channel fixes the Chromium version, the bundled fonts and the image renderer, so output stays identical as long as the template version, the data and the channel don't change.
- Templates use the channel they were created or last upgraded with; the editor shows a visual comparison before you upgrade.
- New templates use the current default channel.
- Set
engineon a request to render with a different channel, for example to test an upgrade. - At most three channels are available at a time. A channel is deprecated with at least 12 months' notice.
Authentication and test mode
Authenticate with API keys, choose scopes, use test mode, roll and revoke keys, and find the API host for your data.
Batches
Render one template for many items with POST /v1/batches, from JSON and CSV items and the combined ZIP or merged PDF to progress, cancel and retry, webhooks, billing and limits.