DynamicDocumentAPI

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.

View as Markdown

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:

StatusWhenBody
200 OKA sync render succeededThe render object, or the file itself with delivery.type: "binary"
202 AcceptedAn async render was accepted, or a sync render was still running when the sync timeout was reachedThe render object with status queued or processing, and a Location: /v1/renders/{id} header
408 Request TimeoutA sync render reached the sync timeout and timeout_behavior is cancelProblem details with code render_timeout
422 Unprocessable EntityA sync render failed, for example because of a template error or a page that couldn't be loadedProblem details with render_id and the render error
Other 4xx and 5xxInvalid requests, authentication, limits, outagesProblem details; see Errors

Request body

FieldTypeDefaultDescription
inputobjectrequiredWhat to render. See input types.
dataobjectnoneJSON data for the template engine
data_urlstringnoneURL to fetch the JSON data from instead of data. See data.
outputobjectPDFOutput format, filename and format options. See output.
deliveryobjecturl delivery, or none without a hosted copyHow you receive the files. See delivery.
modestringsyncsync waits for the result; async returns immediately. See sync and async.
timeout_behaviorstringcontinueSet to cancel to cancel a sync render that reaches the sync timeout instead of letting it finish in the background
webhookobjectnonePer-request webhook: url and events, for example ["render.succeeded", "render.failed"]. See Webhooks.
referencestringnoneYour own identifier, such as an invoice number. Returned on the render and usable as a list filter.
metadataobjectnoneUp to 50 key-value pairs, values up to 500 characters. Returned on the render and usable as list filters.
enginestringtemplate or workspace defaultEngine channel to render with, for example "2026.4". See engine channels.
prioritystringnormalnormal 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.
testbooleanfalseMarks 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.

FieldDescription
template_idTemplate 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.

FieldDescription
htmlBody markup. The template language is available, for example <h1>Hello {{ name }}</h1>.
headOptional contents of <head>: <style>, <link>, <script> and <meta> elements
templatingWhether 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.

FieldDescription
urlThe page to render
http.headersExtra request headers, for example {"Authorization": "Bearer …"}
http.cookiesCookies to set, each with name, value and domain
http.basic_authusername and password for HTTP basic authentication
http.user_agentA 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.

FieldDescription
markdownMarkdown source. The template language runs first, so expressions can produce Markdown.
themedefault, github, academic, minimal or none
cssAdditional 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

FieldDescription
formatpdf, png, jpeg, webp or html
filenameFile name, with template expressions allowed, for example invoice-{{ data.number }}.pdf
pdfPDF options such as paper size, margins, headers and footers, PDF/UA, PDF/A and attachments
imageImage options such as viewport, scale and quality
einvoiceTurns 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

FieldDefaultDescription
typeurl, or none without a hosted copyurl, binary, base64 or none. Without a hosted copy, url fails with 400 validation_error. See receiving files.
expires_in3600Lifetime of signed URLs in seconds, from 60 seconds to 7 days
retention_daysworkspace settingHow long the hosted file is kept, up to your plan's maximum
retentionnone"none" for zero-retention delivery
hostedtrue, unless your destinations keep no copyfalse 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.
storageyour default destinationUploads to your own storage buckets: up to 10 entries, each with a destination_id and an optional path. [] uploads nowhere. See Your own storage.
emailthe template's rulesEmail 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:

PlanSync timeoutAsync maximum duration
Free30 s60 s
Starter60 s5 min
Growth60 s10 min
Pro90 s15 min
Scale120 s30 min
Enterprise300 scustom

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.typeResponseSize limit
urlThe render object. Each file has a signed url and url_expires_at.none
binaryThe file as the response body, with Content-Type, Content-Disposition, X-Render-Id, X-Pages and X-Billed-Renders headers20 MB
base64The render object in JSON, with the file content base64-encoded10 MB
noneThe render object without file URLs, for example when the files go to your storage destinationsnone

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=true to get an attachment Content-Disposition, or ?inline=true to 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 binary or base64 delivery, or uploaded to your storage destinations. Uploading is the default without delivery.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.
  • url delivery, 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"
}
FieldDescription
idRender ID (rnd_…)
objectAlways render
statusSee status values
modesync or async
testtrue for test renders
regionRegion that processed the render, such as eu
sourceWhere the request came from, such as api or dashboard
input_typetemplate, html, url, markdown, pdf_tool for the PDF tools, or einvoice for XML exported with POST /v1/einvoices
templateTemplate ID and the version number that was rendered (template renders only)
engineEngine channel used
output_formatThe requested output format
filesOutput files; see the table below
storageUpload result per storage destination: destination_id, status (pending, succeeded, failed or skipped), location and error
emailThe render's email sends, each with id, rule_id, status, to, provider_message_id, error and sent_at. See Email delivery.
billed_rendersRenders billed for this request, a whole number (0 for test, failed and canceled renders)
timingsMilliseconds spent queued, rendering, post-processing, uploading and in total
warningsNon-fatal problems such as slow or missing assets. See render warnings.
errorFor failed renders: code and message, plus line, column and excerpt for template errors
resultMachine-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, metadataValues from your request
batch_idThe batch the render belongs to, otherwise null
created_at, completed_atWhen the render was created and finished
expires_atWhen the files will be deleted

Each entry in files has:

FieldDescription
idFile ID (file_…)
formatFile format, such as pdf, png or xml
variantSize variant of a multi-size image render (planned); otherwise null
filenameFile name
bytesFile size in bytes
pagesNumber of pages (PDF)
width, heightDimensions in pixels (images)
urlSigned download URL. Omitted when no hosted copy exists.
url_expires_atWhen url stops working
sha256SHA-256 checksum of the file

Status values

StatusMeaning
queuedAccepted and waiting for a renderer
processingRendering
succeededFinished; files are available
failedFailed; error explains why. Nothing is billed.
canceledCanceled with POST /v1/renders/{id}/cancel
expiredThe 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.

EndpointBodyEquivalent
POST /v1/pdf/from-templatetemplate_id, version, data, pdfinput.type: "template", output.format: "pdf"
POST /v1/pdf/from-htmlhtml, head, data, pdfinput.type: "html", output.format: "pdf"
POST /v1/pdf/from-urlurl, http, pdfinput.type: "url", output.format: "pdf"
POST /v1/pdf/from-markdownmarkdown, theme, css, data, pdfinput.type: "markdown", output.format: "pdf"
POST /v1/images/from-templatetemplate_id, data, image, formatinput.type: "template", PNG by default
POST /v1/images/from-htmlhtml, head, data, image, formatHTML screenshot
POST /v1/images/from-urlurl, image, formatURL 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.

ParameterDescription
statusqueued, processing, succeeded, failed, canceled or expired
template_idRenders of one template
input_typetemplate, html, url, markdown, pdf_tool or einvoice
output_formatpdf, png, jpeg, webp, html, xml or zip
sourceOrigin of the render, such as api or dashboard
referenceYour reference value
batch_idRenders of one batch
testtrue 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
limitPage size (default 50)
cursorThe next_cursor value from the previous page

Other render endpoints

EndpointDescription
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}/cancelCancel a queued or processing render. Canceling is best effort, and nothing is billed if rendering hadn't started.
DELETE /v1/renders/{id}/filesDelete the hosted files now; the render becomes expired
GET /v1/renders/{id}/logsThe 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:

PlanSustained (requests/s)Burst (requests/s)Concurrent sync renders
Free252
Starter102010
Growth204020
Pro5010050
Scale150300150
Enterprisecustomcustomcustom

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=1

q 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.

RenderBilled renders
PDF (template, HTML, URL or Markdown), up to 50 pages1
Each additional 50 pages+1
E-invoice layer on a PDFincluded (+0)
E-invoice XML (POST /v1/einvoices)1
Image (PNG, JPEG or WebP)1
Test render, failed render, render canceled before it started0

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 engine on 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.

On this page