# 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 [#create-a-render]

<CodeBlockTabs defaultValue="cURL">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="cURL">
      cURL
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="TypeScript">
      TypeScript
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="Python">
      Python
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="cURL">
    ```bash
    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" }
      }'
    ```
  </CodeBlockTab>

  <CodeBlockTab value="TypeScript">
    ```ts
    const response = await fetch("https://api-eu.dynamicdocumentapi.com/v1/renders", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.DYNAMIC_DOCUMENT_API_KEY}`,
        "Content-Type": "application/json",
        "Idempotency-Key": "invoice-2026-0042",
      },
      body: JSON.stringify({
        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" },
      }),
    });
    const render = await response.json();
    ```
  </CodeBlockTab>

  <CodeBlockTab value="Python">
    ```python
    import os

    import requests

    response = requests.post(
        "https://api-eu.dynamicdocumentapi.com/v1/renders",
        headers={
            "Authorization": f"Bearer {os.environ['DYNAMIC_DOCUMENT_API_KEY']}",
            "Idempotency-Key": "invoice-2026-0042",
        },
        json={
            "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"},
        },
        timeout=130,
    )
    render = response.json()
    ```
  </CodeBlockTab>
</CodeBlockTabs>

Possible responses:

| Status                     | When                                                                                               | Body                                                                                              |
| -------------------------- | -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| `200 OK`                   | A sync render succeeded                                                                            | The [render object](#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](/docs/errors)                                                       |

## Request body [#request-body]

| Field              | Type    | Default                                         | Description                                                                                                                                                   |
| ------------------ | ------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `input`            | object  | required                                        | What to render. See [input types](#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](#data).                                                                                         |
| `output`           | object  | PDF                                             | Output format, filename and format options. See [output](#output).                                                                                            |
| `delivery`         | object  | `url` delivery, or `none` without a hosted copy | How you receive the files. See [delivery](#delivery).                                                                                                         |
| `mode`             | string  | `sync`                                          | `sync` waits for the result; `async` returns immediately. See [sync and async](#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](/docs/webhooks#per-request-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](#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 [#input-types]

Set `input.type` and the fields for that type.

#### `template` [#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` [#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` [#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` [#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 [#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 [#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](/docs/pdf-options) such as paper size, margins, headers and footers, PDF/UA, PDF/A and attachments                                                                                                                                                           |
| `image`    | [Image options](/docs/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](/docs/e-invoicing#make-a-template-an-e-invoice-by-default); `null` turns that default off. See [E-invoicing](/docs/e-invoicing). |

The `html` format returns the rendered HTML document. For standalone e-invoice XML, use `POST /v1/einvoices`, described in [E-invoicing](/docs/e-invoicing#export-xrechnung-or-en-16931-xml).

### Delivery [#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](#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](#zero-retention-delivery)                                                                                                                                                                                                                                                                              |
| `hosted`         | `true`, unless your destinations keep no copy | `false` skips the hosted copy and only uploads to your [storage destinations](/docs/storage). 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](/docs/storage#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](/docs/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](/docs/email-delivery).                                                                                                                                                                                                  |

## Sync and async [#sync-and-async]

### Sync mode [#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 [#async-mode]

With `mode: "async"`, the API responds immediately with `202 Accepted`, the render object and a `Location` header. Wait for a [webhook](/docs/webhooks) or poll the render. A render that runs longer than your plan's async maximum duration fails with the code `render_timeout`.

```bash
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
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 [#poll-for-the-result]

Webhooks are the most efficient way to learn that a render finished. If you poll, back off between requests:

<CodeBlockTabs defaultValue="TypeScript">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="TypeScript">
      TypeScript
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="Python">
      Python
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="TypeScript">
    ```ts
    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`);
    }
    ```
  </CodeBlockTab>

  <CodeBlockTab value="Python">
    ```python
    import os
    import time

    import requests


    def wait_for_render(render_id: str, timeout: float = 600) -> dict:
        deadline = time.monotonic() + timeout
        delay = 1.0
        while time.monotonic() < deadline:
            response = requests.get(
                f"https://api-eu.dynamicdocumentapi.com/v1/renders/{render_id}",
                headers={"Authorization": f"Bearer {os.environ['DYNAMIC_DOCUMENT_API_KEY']}"},
                timeout=30,
            )
            response.raise_for_status()
            render = response.json()
            if render["status"] not in ("queued", "processing"):
                return render
            time.sleep(delay)
            delay = min(delay * 2, 5.0)
        raise TimeoutError(f"Render {render_id} did not finish in time")
    ```
  </CodeBlockTab>
</CodeBlockTabs>

## Receiving files [#receiving-files]

### Delivery types [#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](/docs/storage)                   | 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](#zero-retention-delivery), 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](/docs/storage#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 [#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 [#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](/docs/storage) stay.

### Zero-retention delivery [#zero-retention-delivery]

For sensitive documents, set `delivery.retention` to `"none"`, or make it the default in your workspace settings:

```json
{
  "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](/docs/storage). 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](/docs/email-delivery) 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 [#the-render-object]

```json
{
  "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](#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](/docs/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](/docs/storage): `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](/docs/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](/docs/errors#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](/docs/e-invoicing#validation-report) in `result.conformance`, both when they succeed and when they fail. |
| `reference`, `metadata`      | Values from your request                                                                                                                                                                                                 |
| `batch_id`                   | The [batch](/docs/batches) 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-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 [#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:

```bash
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 [#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 [#list-renders]

`GET /v1/renders` returns renders newest first, with cursor pagination:

```bash
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"
```

```json
{
  "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](/docs/batches)                                  |
| `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 [#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 [#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:

```http
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](/docs/plans-and-limits#limits-by-plan) for all limits.

## Billed renders [#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](/docs/e-invoicing) 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](/docs/plans-and-limits#what-counts-as-a-render) for the full render table.

## Engine channels [#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.
