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: 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
| Tool | Endpoint | What it does |
|---|---|---|
| Merge | POST /v1/pdf-tools/merge | Joins 2 to 200 PDFs into one, with a bookmark per source by default |
| 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 | POST /v1/pdf-tools/rotate | Turns pages by 90, 180 or 270 degrees |
| Protect | POST /v1/pdf-tools/protect | Encrypts a PDF with AES-256, with passwords and permissions |
| Unlock | POST /v1/pdf-tools/unlock | Removes the encryption, given the user or the owner password |
| Metadata | POST /v1/pdf-tools/metadata | Reads the page count, page sizes and document information, or changes the information |
| PDF/A | 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
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
urlthat isn't a publichttpsaddress, and an upload or data URI that isn't a PDF fail with400 validation_error. - A render or upload that isn't in your workspace, or an expired upload, answers
404 render_not_foundor404 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:
curl https://api-eu.dynamicdocumentapi.com/v1/uploads \
-H "Authorization: Bearer $DYNAMIC_DOCUMENT_API_KEY" \
-F "file=@contract.pdf"{
"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
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
Besides its own fields, every tool accepts these, with the same meaning as for 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. |
timeout_behavior | cancel cancels a sync call that reaches your plan's sync timeout |
delivery | How you receive the files. See 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 reads and changes. |
test | true makes a test call, which costs nothing and returns PDFs with the "TEST" watermark. See 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
POST /v1/pdf-tools/merge joins the pages of 2 to 200 PDFs into one document, in the order of sources:
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, 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 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.
{
"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. It may have at most your plan's pages per PDF, or the call fails with page_limit_exceeded.
Split
POST /v1/pdf-tools/split writes parts of a PDF as separate files: one per page list in ranges, or one per run of every pages.
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
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:
{
"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
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 receive every file, and a path template can tell the parts apart with file.index.
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 if you need PDF/A again.
Rotate
POST /v1/pdf-tools/rotate turns pages by a multiple of 90 degrees:
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. 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
POST /v1/pdf-tools/protect encrypts a PDF with AES-256, like the protect option of a render:
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 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
POST /v1/pdf-tools/unlock removes a PDF's encryption and returns the same document without passwords or restrictions:
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_requiredand the messageThe 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 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 the result again if needed.
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
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:
{
"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
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
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 describes it in full.
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 getsurlwhen it keeps a hosted copy andnonewhen it doesn't, as for renders. binaryandbase64needmode: "sync".- A split into several files and a metadata read can't use
binary: the request fails with400 validation_errorat/delivery/type. - Files over the size limit fail with
413 output_too_large; useurldelivery for them. expires_in,retention_days, zero-retention delivery and storage destinations 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"andbase64delivery. The information comes back in the response, and the stored render keeps nometadatain itsresult.
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 Acceptedand the call continues. Withtimeout_behavior: "cancel", the call is canceled instead and the answer is408 render_timeout. - Async: with
mode: "async", the API answers202 Acceptedat once, with the render object and aLocation: /v1/renders/{id}header. Wait for a webhook or pollGET /v1/renders/{id}. - Webhooks: tool calls send
render.succeededandrender.failedto a per-requestwebhookand to your webhook endpoints that subscribe to these events, except endpoints limited to certain templates withtemplate_ids.data.objectis the render object. - Idempotency: a retry with the same
Idempotency-Keyand body within 24 hours returns the first response and creates no second render. See Idempotency. - Limits: calls count towards your plan's rate limits and concurrent sync renders. Test calls also count towards your plan's test renders per minute; see Test mode.
- Render log:
GET /v1/renders?input_type=pdf_toollists tool calls, together with the renders that build a batch's ZIP file and merged PDF. The 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:
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/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
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
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. |
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. |
render_timeout | The call ran longer than your plan allows |
See Errors for every other code.
Test mode
Calls with a test key, or with "test": true, are test calls, as in 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.pdfadescribes 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
resultdescribes 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), 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
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 for the render table.
Related pages
- Renders for the render object, sync and async, delivery and idempotency
- PDF options for protection, metadata and PDF/A when you render a new PDF
- Webhooks for
render.succeededandrender.failedevents - Storage for delivering files to your own bucket
- Batches for rendering many documents at once, combined into one ZIP file or one merged PDF
- Plans and limits for pages per PDF and the render table
- Errors for every error code
- API reference for the full request and response schemas
PDF options
Paper sizes, margins, scaling, print behaviour, metadata, accessibility and PDF/UA, PDF/A archiving, attachments, protection and wait strategies for PDF output.
E-invoicing
Factur-X and ZUGFeRD PDFs with embedded EN 16931 XML, XRechnung export in CII or UBL, the invoice model, computed totals and validation reports.