# PDF tools

> Merge, split, rotate, protect and unlock existing PDFs and read or change their metadata, from sources and page lists to delivery, encrypted files, errors and billing.



The PDF tools work on PDFs you already have: files from earlier renders, uploads, URLs or inline data. Each call creates a render with `input_type: "pdf_tool"`, so it behaves like [`POST /v1/renders`](/docs/renders): sync or async, the same delivery types, webhooks and idempotency keys, and an entry in your render log. Each successful call is 1 billed render.

## Tools [#tools]

| Tool                      | Endpoint                      | What it does                                                                                |
| ------------------------- | ----------------------------- | ------------------------------------------------------------------------------------------- |
| [Merge](#merge)           | `POST /v1/pdf-tools/merge`    | Joins 2 to 200 PDFs into one, with a bookmark per source by default                         |
| [Split](#split)           | `POST /v1/pdf-tools/split`    | Writes page ranges, or runs of a fixed number of pages, as separate PDFs or as one ZIP file |
| [Rotate](#rotate)         | `POST /v1/pdf-tools/rotate`   | Turns pages by 90, 180 or 270 degrees                                                       |
| [Protect](#protect)       | `POST /v1/pdf-tools/protect`  | Encrypts a PDF with AES-256, with passwords and permissions                                 |
| [Unlock](#unlock)         | `POST /v1/pdf-tools/unlock`   | Removes the encryption, given the user or the owner password                                |
| [Metadata](#metadata)     | `POST /v1/pdf-tools/metadata` | Reads the page count, page sizes and document information, or changes the information       |
| [PDF/A](#convert-to-pdfa) | `POST /v1/pdf-tools/pdfa`     | Converts a PDF to PDF/A-2b or PDF/A-3b and validates it                                     |

The tools are available on every plan; the PDF/A conversion needs the Growth plan or higher. Tool calls and uploads need an API key with the `render:write` scope.

## Sources [#sources]

Each tool takes its PDF as a source object: `source`, or `sources` for a merge. A source has exactly one of these fields:

| Field       | Example                                                     | What it is                                                                                                                                                                                  |
| ----------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `url`       | `{"url": "https://example.com/files/contract.pdf"}`         | A public `https` URL. The file is downloaded when the call runs: at most 100 MB, within 30 seconds.                                                                                         |
| `render_id` | `{"render_id": "rnd_01J9ZM1X3F7R8K2C4V6B8N0P2Q"}`           | The PDF of an earlier render in your workspace, at most 100 MB. Without `file_id` it is the render's first PDF file; add `file_id` (`file_…`) to pick another, such as one part of a split. |
| `upload_id` | `{"upload_id": "upl_01J9ZQ4V6X8Z0B2D4F6H8K0M2P"}`           | A PDF uploaded with `POST /v1/uploads`: at most 50 MB, kept for 24 hours                                                                                                                    |
| `data_uri`  | `{"data_uri": "data:application/pdf;base64,JVBERi0xLjcK…"}` | The file itself, base64-encoded, at most 5 MB. The prefix `data:application/octet-stream;base64,` works too.                                                                                |

`render_id` chains tools: pass the render ID of one call to the next, for example to rotate the pages of a file you have just unlocked.

The API checks sources before anything runs:

* A source without exactly one of these fields, a `url` that isn't a public `https` address, and an upload or data URI that isn't a PDF fail with `400 validation_error`.
* A render or upload that isn't in your workspace, or an expired upload, answers `404 render_not_found` or `404 upload_not_found`.
* A render without a PDF file to read answers `410 file_expired`: it hasn't succeeded, its files were deleted, or it used zero-retention delivery.

Data URIs count towards your plan's request body limit, which is 2 MB on Free, so upload larger files first. `POST /v1/uploads` takes one file in the multipart field `file` and recognises PDFs by their content:

```bash
curl https://api-eu.dynamicdocumentapi.com/v1/uploads \
  -H "Authorization: Bearer $DYNAMIC_DOCUMENT_API_KEY" \
  -F "file=@contract.pdf"
```

```json
{
  "id": "upl_01J9ZQ4V6X8Z0B2D4F6H8K0M2P",
  "object": "upload",
  "filename": "contract.pdf",
  "bytes": 482113,
  "mime": "application/pdf",
  "sha256": "5e8f…",
  "created_at": "2026-10-03T09:12:44Z",
  "expires_at": "2026-10-04T09:12:44Z"
}
```

## Page lists [#page-lists]

Split and rotate select pages with page lists. Pages are numbered from 1:

| Page list | Pages                   |
| --------- | ----------------------- |
| `"7"`     | Page 7                  |
| `"1,3-5"` | Pages 1, 3, 4 and 5     |
| `"4-"`    | Page 4 to the last page |
| `"all"`   | Every page              |

Spaces around numbers, dashes and commas are ignored, and every page counts once, in document order: `"5,1-2"` selects pages 1, 2 and 5. A list in any other form is rejected with `400 validation_error`. Page 0, a range that runs backwards such as `"5-3"`, and a page after the last one fail the call with `validation_error`, and the message names the page, for example `page 9 is outside the document, which has 5 pages`.

## Common fields [#common-fields]

Besides its own fields, every tool accepts these, with the same meaning as for [renders](/docs/renders):

| Field                   | Description                                                                                                                                                                                                         |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `filename`              | Name of the output file, which always ends in `.pdf`. Without it, the file is named after the render ID. A split uses it as the stem of the parts' names.                                                           |
| `mode`                  | `sync` (default) or `async`. See [Sync, async and webhooks](#sync-async-and-webhooks).                                                                                                                              |
| `timeout_behavior`      | `cancel` cancels a sync call that reaches your plan's sync timeout                                                                                                                                                  |
| `delivery`              | How you receive the files. See [Delivery](#delivery).                                                                                                                                                               |
| `webhook`               | A per-request webhook with `url` and `events`                                                                                                                                                                       |
| `reference`, `metadata` | Your own identifier and key-value pairs for the render, as for renders. This `metadata` isn't the PDF's document information, which the [metadata tool](#metadata) reads and changes.                               |
| `test`                  | `true` makes a test call, which costs nothing and returns PDFs with the "TEST" watermark. See [Test mode](#test-mode). Test keys always make test calls; `true` with a live key fails with `403 test_key_required`. |

Send an `Idempotency-Key` header with every call, as for renders.

## Merge [#merge]

`POST /v1/pdf-tools/merge` joins the pages of 2 to 200 PDFs into one document, in the order of `sources`:

```bash
curl https://api-eu.dynamicdocumentapi.com/v1/pdf-tools/merge \
  -H "Authorization: Bearer $DYNAMIC_DOCUMENT_API_KEY" \
  -H "Idempotency-Key: contract-4711-merge" \
  -H "Content-Type: application/json" \
  -d '{
    "sources": [
      { "url": "https://example.com/files/cover.pdf" },
      { "render_id": "rnd_01J9ZM1X3F7R8K2C4V6B8N0P2Q" },
      { "upload_id": "upl_01J9ZQ4V6X8Z0B2D4F6H8K0M2P" }
    ],
    "bookmarks": "filenames",
    "filename": "contract-4711.pdf",
    "reference": "contract-4711"
  }'
```

| Field       | Description                                                                                                                                             |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `sources`   | Required. 2 to 200 [sources](#sources), merged in this order                                                                                            |
| `bookmarks` | `filenames` (default): one top-level bookmark per source, pointing at its first page, and the PDF opens with the bookmarks shown. `none`: no bookmarks. |

Bookmarks are named after the files, without `.pdf`: the end of a `url` path (`Quarterly%20Report.pdf` becomes `Quarterly Report`), the name of an uploaded file, or the name of a render's file. Sources without a name, such as data URIs, become `Document 1`, `Document 2` and so on, by their position.

The response is a [render object](/docs/renders#the-render-object) with the merged file. Its `input_type` is `pdf_tool`, and `kind` names the tool: `pdf_merge`, `pdf_split`, `pdf_rotate`, `pdf_protect`, `pdf_unlock`, `pdf_metadata` or `pdf_pdfa`. Every tool answers this way.

```json
{
  "id": "rnd_01JA2B3C4D5E6F7G8H9J0K1M2N",
  "object": "render",
  "status": "succeeded",
  "mode": "sync",
  "test": false,
  "input_type": "pdf_tool",
  "kind": "pdf_merge",
  "output_format": "pdf",
  "files": [
    {
      "id": "file_01JA2B3C4D5E6F7G8H9J0K1M2P",
      "format": "pdf",
      "filename": "contract-4711.pdf",
      "bytes": 912384,
      "pages": 14,
      "content_type": "application/pdf",
      "url": "https://files-eu.dynamicdocumentapi.com/f/…",
      "url_expires_at": "2026-10-03T10:12:45Z",
      "sha256": "3a7b…"
    }
  ],
  "pages": 14,
  "billed_renders": 1,
  "warnings": [],
  "error": null,
  "result": null,
  "reference": "contract-4711",
  "created_at": "2026-10-03T09:12:44Z",
  "completed_at": "2026-10-03T09:12:45Z"
}
```

The response body above is shortened.

The merged PDF keeps the sources' pages with their content and annotations, and links between pages keep working. What belongs to a source document as a whole is not carried over: its bookmarks, form fields (their appearance stays on the page), tags, page labels, embedded files, JavaScript and document information. The result is untagged and has no title; set one with the [metadata tool](#metadata). It may have at most your plan's pages per PDF, or the call fails with `page_limit_exceeded`.

## Split [#split]

`POST /v1/pdf-tools/split` writes parts of a PDF as separate files: one per [page list](#page-lists) in `ranges`, or one per run of `every` pages.

```bash
curl https://api-eu.dynamicdocumentapi.com/v1/pdf-tools/split \
  -H "Authorization: Bearer $DYNAMIC_DOCUMENT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "source": { "upload_id": "upl_01J9ZQ4V6X8Z0B2D4F6H8K0M2P" },
    "ranges": ["1-2", "3-"],
    "filename": "statement.pdf"
  }'
```

| Field    | Description                                                                                                     |
| -------- | --------------------------------------------------------------------------------------------------------------- |
| `source` | Required. The PDF to split                                                                                      |
| `ranges` | Up to 200 page lists, one output file each, such as `["1-2", "3-"]`                                             |
| `every`  | Instead of `ranges`: one file per run of this many pages, from 1 to 10,000. The last file may hold fewer pages. |
| `zip`    | `true` delivers one ZIP file that holds the parts, instead of one file per part. Default `false`.               |

Send exactly one of `ranges` and `every`. Ranges may overlap, so a page can appear in several files. `every: 10` splits a 25-page document into pages 1–10, 11–20 and 21–25, and `every: 1` makes one file per page. A split makes at most 200 files; asking for more fails with `validation_error`.

### Names and files [#names-and-files]

The parts are named `<stem>-1.pdf`, `<stem>-2.pdf` and so on, in the order of `ranges` or of the runs. `<stem>` is `filename` without its extension, or `split` without a `filename`. With `zip: true`, the only output is `<stem>.zip`, which holds the parts under these names.

The render's `files` lists the parts in that order, each with its own `id`, `pages` and signed `url`, and `pages` on the render counts the pages of all parts. `GET /v1/renders/{id}/files/{file_id}` redirects to a single part. For the request above and a 6-page statement:

```json
{
  "id": "rnd_01JA2B3C4D5E6F7G8H9J0K1M2N",
  "object": "render",
  "status": "succeeded",
  "input_type": "pdf_tool",
  "kind": "pdf_split",
  "output_format": "pdf",
  "files": [
    { "id": "file_01JA2B3C4D5E6F7G8H9J0K1M2P", "format": "pdf", "filename": "statement-1.pdf", "pages": 2, "url": "https://files-eu.dynamicdocumentapi.com/f/…" },
    { "id": "file_01JA2B3C4D5E6F7G8H9J0K1M2Q", "format": "pdf", "filename": "statement-2.pdf", "pages": 4, "url": "https://files-eu.dynamicdocumentapi.com/f/…" }
  ],
  "pages": 6,
  "billed_renders": 1
}
```

With `zip: true`, `output_format` is `zip` and `files` holds one `statement.zip` (`application/zip`). Either way, a split is 1 billed render, however many files it makes.

### Delivering several files [#delivering-several-files]

| `delivery.type` | Split result                                                                                                                              |
| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `url`           | A signed `url` for every part                                                                                                             |
| `binary`        | Only for a single output: `zip: true`, or exactly one range. Otherwise the request fails with `400 validation_error` at `/delivery/type`. |
| `base64`        | Every part, each with its own `content_base64`, at most 10 MB together                                                                    |

[Storage destinations](/docs/storage) receive every file, and a path template can tell the parts apart with `file.index`.

### What the parts keep [#what-the-parts-keep]

Each part keeps the source's document information and language. Links to pages in the same part keep working; links to other pages are removed. Like a merge, a part doesn't carry the source's bookmarks, form fields, tags, page labels, embedded files or XMP metadata.

> **PDF/A files and e-invoices**
>
> Merged and split files are plain PDFs, even when a source is a PDF/A file or a Factur-X or ZUGFeRD invoice: the PDF/A identification and the embedded invoice XML belong to the document as a whole and don't carry over. Convert the result with the [PDF/A tool](#convert-to-pdfa) if you need PDF/A again.

## Rotate [#rotate]

`POST /v1/pdf-tools/rotate` turns pages by a multiple of 90 degrees:

```bash
curl https://api-eu.dynamicdocumentapi.com/v1/pdf-tools/rotate \
  -H "Authorization: Bearer $DYNAMIC_DOCUMENT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "source": { "upload_id": "upl_01J9ZQ4V6X8Z0B2D4F6H8K0M2P" }, "pages": "2,4-", "degrees": 90 }'
```

| Field     | Description                                                                                           |
| --------- | ----------------------------------------------------------------------------------------------------- |
| `source`  | Required. The PDF                                                                                     |
| `degrees` | Required. `90`, `180` or `270` turn pages clockwise; `-90`, `-180` or `-270` turn them anticlockwise. |
| `pages`   | The pages to turn, as a [page list](#page-lists). Default `"all"`.                                    |

The angle is added to each selected page's current rotation: a page that is already turned by 90 degrees ends up at 180 with `"degrees": 90`. The other pages keep theirs. Only the rotation changes, not the page content, and the rest of the document stays as it is.

## Protect [#protect]

`POST /v1/pdf-tools/protect` encrypts a PDF with AES-256, like the [`protect` option](/docs/pdf-options#security) of a render:

```bash
curl https://api-eu.dynamicdocumentapi.com/v1/pdf-tools/protect \
  -H "Authorization: Bearer $DYNAMIC_DOCUMENT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "source": { "render_id": "rnd_01J9ZM1X3F7R8K2C4V6B8N0P2Q" },
    "user_password": "…",
    "owner_password": "…",
    "permissions": { "copy": false },
    "filename": "statement-protected.pdf"
  }'
```

| Field            | Description                                                                                                     |
| ---------------- | --------------------------------------------------------------------------------------------------------------- |
| `source`         | Required. An unencrypted PDF                                                                                    |
| `user_password`  | Password needed to open the document, up to 127 characters                                                      |
| `owner_password` | Password that grants full access, up to 127 characters                                                          |
| `permissions`    | What someone who opens the document with the user password may do. Flags you leave out take the defaults below. |

Send at least one password; passwords can't contain NUL characters. Without `user_password`, anyone can open the document, and the permissions apply. Without `owner_password`, a random owner password is set that nobody knows.

| Permission       | Default | Allows                                                                 |
| ---------------- | ------- | ---------------------------------------------------------------------- |
| `print`          | `true`  | Printing, in low resolution unless `print_high_res` is allowed too     |
| `print_high_res` | `true`  | Printing in full quality, together with `print`                        |
| `copy`           | `true`  | Copying text and images                                                |
| `modify`         | `false` | Changing the document in other ways                                    |
| `annotate`       | `false` | Adding and changing annotations such as comments, and filling in forms |
| `fill_forms`     | `true`  | Filling in existing form fields                                        |
| `assemble`       | `false` | Inserting, rotating and deleting pages, and changing bookmarks         |

These are the defaults of the `protect` render option too. Assistive technology can always extract the text. Other permission names fail with `400 validation_error`.

A source that is already encrypted fails with `pdf_password_required`: [unlock](#unlock) it first to change its passwords or permissions. PDF/A forbids encryption, so a protected PDF/A file is no longer PDF/A.

> **Passwords in requests**
>
> Passwords are part of the request body for protect and unlock. Keep them out of your own logs, and use request-log redaction rules if request logging is enabled for your workspace.

## Unlock [#unlock]

`POST /v1/pdf-tools/unlock` removes a PDF's encryption and returns the same document without passwords or restrictions:

```bash
curl https://api-eu.dynamicdocumentapi.com/v1/pdf-tools/unlock \
  -H "Authorization: Bearer $DYNAMIC_DOCUMENT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "source": { "upload_id": "upl_01J9ZQ4V6X8Z0B2D4F6H8K0M2P" }, "password": "…", "filename": "statement.pdf" }'
```

| Field      | Description                                                             |
| ---------- | ----------------------------------------------------------------------- |
| `source`   | Required. The encrypted PDF                                             |
| `password` | Required. The user password or the owner password, up to 127 characters |

* A password that opens the PDF neither as user nor as owner fails with `422 pdf_password_required` and the message `The password doesn't open this PDF.` A failed call costs nothing.
* A PDF that opens without a password but restricts printing, copying or editing is encrypted too. Unlock it with `"password": ""` or with its owner password.
* An unencrypted source comes back unchanged, and the call is 1 billed render like any other. A [test call](#test-mode) adds the "TEST" watermark.

To change an encrypted PDF, unlock it, pass the unlocked render's ID as `render_id` to the next tool, and [protect](#protect) the result again if needed.

## Metadata [#metadata]

`POST /v1/pdf-tools/metadata` reads a PDF's pages, page sizes and document information. With `set`, it changes the document information and returns the edited PDF.

### Read the metadata [#read-the-metadata]

```bash
curl https://api-eu.dynamicdocumentapi.com/v1/pdf-tools/metadata \
  -H "Authorization: Bearer $DYNAMIC_DOCUMENT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "source": { "render_id": "rnd_01J9ZM1X3F7R8K2C4V6B8N0P2Q" } }'
```

A read returns no file. The information is in the render's `result`:

```json
{
  "id": "rnd_01JA2B3C4D5E6F7G8H9J0K1M2N",
  "object": "render",
  "status": "succeeded",
  "input_type": "pdf_tool",
  "kind": "pdf_metadata",
  "files": [],
  "billed_renders": 1,
  "result": {
    "pages": 12,
    "page_sizes": [
      { "width": "210mm", "height": "297mm", "pages": "1-11" },
      { "width": "297mm", "height": "210mm", "pages": "12" }
    ],
    "encrypted": false,
    "pdf_version": "1.7",
    "metadata": {
      "title": "Quarterly report",
      "author": "Example GmbH",
      "subject": null,
      "keywords": ["report", "2026"],
      "creator": null,
      "producer": "Dynamic Document API (engine 2026.4)",
      "created": "2026-09-17T15:06:34Z",
      "modified": "2026-09-17T15:06:34Z",
      "lang": "en-GB"
    }
  }
}
```

| Field         | Description                                                                                                                                                                                              |
| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `pages`       | Number of pages                                                                                                                                                                                          |
| `page_sizes`  | Runs of consecutive pages shown at the same size, in millimetres with at most one decimal. The size is the page's crop box, else its media box, with the page's rotation applied.                        |
| `encrypted`   | `true` for any encryption, also for restrictions without a password                                                                                                                                      |
| `pdf_version` | The PDF version the file declares, such as `1.7`                                                                                                                                                         |
| `metadata`    | `title`, `author`, `subject`, `keywords` (a list, split at commas and semicolons), `creator`, `producer`, `created` and `modified` (UTC timestamps) and `lang`. Values the file doesn't have are `null`. |

A read works on encrypted PDFs. One that needs a password answers `encrypted: true` and `metadata: null`, with `pages` and `page_sizes` if they can be read without the password (otherwise `null` and `[]`). One that only restricts what you may do is read completely.

Because a read has no file, `binary` delivery and storage destinations fail with `400 validation_error`, and your workspace's default storage destinations are skipped. A read is 1 billed render.

### Change the metadata [#change-the-metadata]

```bash
curl https://api-eu.dynamicdocumentapi.com/v1/pdf-tools/metadata \
  -H "Authorization: Bearer $DYNAMIC_DOCUMENT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "source": { "upload_id": "upl_01J9ZQ4V6X8Z0B2D4F6H8K0M2P" },
    "set": { "title": "Invoice 2026-0042", "author": "Example GmbH", "keywords": ["invoice", "2026"], "subject": "" },
    "filename": "invoice-2026-0042.pdf"
  }'
```

| `set` field | Description                                                              |
| ----------- | ------------------------------------------------------------------------ |
| `title`     | Document title, up to 500 characters                                     |
| `author`    | Author, up to 500 characters                                             |
| `subject`   | Subject, up to 500 characters                                            |
| `keywords`  | Up to 50 keywords of up to 100 characters each                           |
| `creator`   | The application that created the original document, up to 500 characters |
| `producer`  | The application that produced the PDF, up to 500 characters              |
| `lang`      | Document language, such as `de-DE`, up to 35 characters                  |

Fields you leave out stay as they are. `""` or `null` removes a field, and `[]` or `null` removes the keywords; the example above removes `subject`. An empty `set` or an unknown field fails with `400 validation_error`. Don't confuse `set` with the top-level `metadata` field, which holds your own key-value pairs for the render.

The values go into the document information and the XMP metadata alike. The modification date becomes the current time, and the creation date stays. In the XMP metadata, only the properties of the changed fields are replaced. Everything else stays, including the PDF/A identification and extension schemas such as Factur-X, so a PDF/A file or an e-invoice keeps its identification, and embedded files such as the invoice XML stay in place. A PDF without XMP metadata doesn't get any.

The response holds the edited PDF and, in `result`, the information of the edited file, as a read returns it. Changing the metadata needs an unencrypted source.

## Convert to PDF/A [#convert-to-pdfa]

`POST /v1/pdf-tools/pdfa` converts an existing PDF to PDF/A-2b or PDF/A-3b and validates the result with veraPDF. Send a `source` and a `level` (`2b` or `3b`); the report comes back in `result.conformance.pdfa`. A source that can't be made conformant, such as an encrypted one or one whose fonts aren't embedded, fails with `pdfa_conversion_failed`. The conversion needs the Growth plan or higher. [Convert an existing PDF](/docs/pdf-options#convert-an-existing-pdf) describes it in full.

## Delivery [#delivery]

Tool calls deliver their files like renders:

| `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. Only for calls with exactly one output file. | 20 MB                        |
| `base64`        | The render object, with each file's content in `content_base64`                                                                                                                  | 10 MB for all files together |
| `none`          | The render object without file URLs, for files that go to your storage destinations                                                                                              | none                         |

* Without `delivery.type`, a call gets `url` when it keeps a hosted copy and `none` when it doesn't, as for [renders](/docs/renders#delivery-types).
* `binary` and `base64` need `mode: "sync"`.
* A split into several files and a metadata read can't use `binary`: the request fails with `400 validation_error` at `/delivery/type`.
* Files over the size limit fail with `413 output_too_large`; use `url` delivery for them.
* `expires_in`, `retention_days`, [zero-retention delivery](/docs/renders#zero-retention-delivery) and [storage destinations](/docs/storage) work as for renders. Your workspace's default storage destinations receive tool outputs too, except metadata reads.
* With zero retention, read metadata with `mode: "sync"` and `base64` delivery. The information comes back in the response, and the stored render keeps no `metadata` in its `result`.

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

Calls run like renders:

* **Sync** (the default): the API waits for the result up to your plan's sync timeout. If the call is still running then, the API answers `202 Accepted` and the call continues. With `timeout_behavior: "cancel"`, the call is canceled instead and the answer is `408 render_timeout`.
* **Async**: with `mode: "async"`, the API answers `202 Accepted` at once, with the render object and a `Location: /v1/renders/{id}` header. Wait for a webhook or poll `GET /v1/renders/{id}`.
* **Webhooks**: tool calls send `render.succeeded` and `render.failed` to a per-request `webhook` and to your [webhook endpoints](/docs/webhooks) that subscribe to these events, except endpoints limited to certain templates with `template_ids`. `data.object` is the render object.
* **Idempotency**: a retry with the same `Idempotency-Key` and body within 24 hours returns the first response and creates no second render. See [Idempotency](/docs/renders#idempotency).
* **Limits**: calls count towards your plan's [rate limits](/docs/renders#rate-limits) and concurrent sync renders. Test calls also count towards your plan's test renders per minute; see [Test mode](#test-mode).
* **Render log**: `GET /v1/renders?input_type=pdf_tool` lists tool calls, together with the renders that build a batch's ZIP file and merged PDF. The [other render endpoints](/docs/renders#other-render-endpoints) work for tool calls too.

An async split into one file per page, packed as a ZIP file, with a per-request webhook:

```bash
curl -i https://api-eu.dynamicdocumentapi.com/v1/pdf-tools/split \
  -H "Authorization: Bearer $DYNAMIC_DOCUMENT_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "source": { "url": "https://example.com/files/scans.pdf" },
    "every": 1,
    "zip": true,
    "mode": "async",
    "webhook": { "url": "https://example.com/webhooks/documents", "events": ["render.succeeded", "render.failed"] }
  }'
```

```http
HTTP/1.1 202 Accepted
Location: /v1/renders/rnd_01JA2B3C4D5E6F7G8H9J0K1M2N
Content-Type: application/json

{"id": "rnd_01JA2B3C4D5E6F7G8H9J0K1M2N", "object": "render", "status": "queued", "mode": "async", "input_type": "pdf_tool", "kind": "pdf_split"}
```

The response body above is shortened.

## Encrypted sources [#encrypted-sources]

What happens with an encrypted source depends on the tool:

| Tool                                               | Encrypted source                                                                            |
| -------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| Merge, split, rotate, protect, metadata with `set` | Fails with `pdf_password_required` and the message `The PDF is encrypted; unlock it first.` |
| Unlock                                             | Decrypted with the `password` you send                                                      |
| Metadata read                                      | Read, with `encrypted: true`                                                                |
| PDF/A conversion                                   | Fails with `pdfa_conversion_failed`                                                         |

Encrypted means any encryption. A PDF that opens without a password but restricts printing, copying or editing counts too, because rewriting it would drop the restrictions. Unlock such a file first, with its owner password or `"password": ""`, then pass the unlocked render to the next tool.

## Errors [#errors]

The API checks the request before anything runs:

| Status | Code                                   | When                                                                                                                                                                                                                                                  |
| ------ | -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 400    | `validation_error`                     | A field is missing or invalid, for example a source without exactly one field, a `url` that isn't a public `https` address, a malformed page list, both `ranges` and `every`, or `binary` delivery for several files. `errors[]` points at the field. |
| 402    | `plan_feature_unavailable`             | The PDF/A conversion, or storage destinations, aren't part of your plan                                                                                                                                                                               |
| 402    | `render_limit_reached`                 | Your workspace has no renders left                                                                                                                                                                                                                    |
| 404    | `render_not_found`, `upload_not_found` | The render or upload isn't in your workspace, or the upload has expired                                                                                                                                                                               |
| 410    | `file_expired`                         | The render has no PDF file to read                                                                                                                                                                                                                    |
| 413    | `payload_too_large`                    | The request body is larger than your plan allows, or a data URI holds more than 5 MB                                                                                                                                                                  |
| 429    | `rate_limited`                         | Too many requests, or more test calls per minute than your plan allows. Wait for the `Retry-After` header.                                                                                                                                            |

Problems found while the tool runs fail the call. A sync call answers `422` with the code, the message and `render_id`; an async call ends with `status: "failed"` and the code in `error`. Failed calls cost nothing.

| Code                     | When                                                                                                                            |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------- |
| `invalid_pdf_source`     | A source isn't a readable PDF, or has no pages. In a merge, the message names the source, such as `source 2`.                   |
| `pdf_password_required`  | A source is encrypted, or the unlock password is wrong. See [Encrypted sources](#encrypted-sources).                            |
| `validation_error`       | A page list doesn't fit the document, or `every` would make more than 200 files. The message says which page or how many files. |
| `page_limit_exceeded`    | A source, or the merged PDF, has more pages than your plan allows per PDF. A metadata read has no page limit.                   |
| `url_not_allowed`        | A source `url` leads to a blocked address                                                                                       |
| `asset_fetch_failed`     | A source `url` couldn't be downloaded: an error status, a download that takes longer than 30 seconds, or a file over 100 MB     |
| `payload_too_large`      | A render's file used as a source is larger than 100 MB                                                                          |
| `pdfa_conversion_failed` | The PDF/A conversion failed. See [Convert an existing PDF](/docs/pdf-options#convert-an-existing-pdf).                          |
| `render_timeout`         | The call ran longer than your plan allows                                                                                       |

See [Errors](/docs/errors) for every other code.

## Test mode [#test-mode]

Calls with a test key, or with `"test": true`, are test calls, as in [test mode](/docs/authentication#test-mode) for renders. They cost nothing, and every PDF they return carries the "TEST" watermark. Use a live key for files you keep or send.

* The watermark is on the merged PDF, on each part of a split and each PDF in its ZIP file, and on the rotated, protected, unlocked, edited or converted PDF.
* Protect adds the watermark before it encrypts the PDF.
* The PDF/A conversion adds it before it converts. It is drawn as vector outlines, so a test PDF/A file is conformant too, and the report in `result.conformance.pdfa` describes the file you receive.
* Unlocking an unencrypted PDF returns it with the watermark, not unchanged.
* A metadata read returns no file, so there is nothing to mark. Its `result` describes your source.
* A source that carries the watermark already, such as a test render, gets it a second time, so the mark looks a little darker.

Test calls count towards your plan's test renders per minute (see [Plans and limits](/docs/plans-and-limits#limits-by-plan)), together with test renders. A call beyond the limit fails with `429 rate_limited` and a `Retry-After` header. Requests the API rejects before the call runs, such as a `400 validation_error`, don't count.

## Billing [#billing]

Each successful call is 1 billed render, whatever the number of pages, sources or output files. Merging 200 PDFs, splitting a document into 50 files, reading metadata and unlocking a PDF that wasn't encrypted are 1 render each. Failed calls, test calls and calls canceled before they started cost nothing. The render's `billed_renders` field and the `X-Billed-Renders` header of binary responses show the number. See [Plans and limits](/docs/plans-and-limits) for the render table.

## Related pages [#related-pages]

* [Renders](/docs/renders) for the render object, sync and async, delivery and idempotency
* [PDF options](/docs/pdf-options) for protection, metadata and PDF/A when you render a new PDF
* [Webhooks](/docs/webhooks) for `render.succeeded` and `render.failed` events
* [Storage](/docs/storage) for delivering files to your own bucket
* [Batches](/docs/batches) for rendering many documents at once, combined into one ZIP file or one merged PDF
* [Plans and limits](/docs/plans-and-limits) for pages per PDF and the render table
* [Errors](/docs/errors) for every error code
* [API reference](/docs/api-reference) for the full request and response schemas
