# Dynamic Document API

Version 1.0.0. Machine-readable OpenAPI document: https://dynamicdocumentapi.com/openapi/openapi-public.json

The Dynamic Document API turns templates and JSON into PDFs and images.

* Authenticate with `Authorization: Bearer dda_live_…` (test keys: `dda_test_…`).
* Errors are RFC 9457 problem details (`application/problem+json`).
* All POST endpoints accept an `Idempotency-Key` header.
* Rate-limit headers: `RateLimit-Policy`, `RateLimit`, and `Retry-After` on 429.

## Servers

- http://127.0.0.1:8001 — Region eu

## GET /l/{link_id}/{filename}

**Render a signed link**

The public image URL of a signed link (proto/README.md §22), served from `https://img.dynamicdocumentapi.com` without an API key. Checks, in order: unknown, disabled or deleted link, or a format the link does not enable → 404 `not_found`; expired link or a signed `exp` in the past → 410 `link_expired`; `access: signed` needs `sig` = base64url(HMAC-SHA256(secret, "GET\n/l/{link_id}/{name}.{ext}\n" + canonical query without `sig`)) → 403 `invalid_signature` (open links accept only allowed parameters that have `param_limits`); unknown parameters or a value over `max_length` → 400 `validation_error`; a `Referer` whose host is not in `allowed_referrers` → 403 `referrer_not_allowed`; the per-IP hourly limit → 429 `rate_limited`; the plan feature → 402 `plan_feature_unavailable`; the link quota → 429 `quota_exceeded`.

 A cached result is returned with `X-Billed-Renders: 0` and creates no render; a cache miss renders the template's live version (`X-Billed-Renders: 1`, `X-Render-Id`). Responses carry `ETag` (304 on `If-None-Match`), `Cache-Control: public, max-age=…` and `Access-Control-Allow-Origin: *`. `HEAD` runs the same checks and never renders. When the render limit is reached, a link with a fallback image returns it instead of 402 `render_limit_reached`.

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `If-None-Match` | header | string | A previous `ETag`. |
| `exp` | query | integer | Expiry (Unix seconds), signed. |
| `filename` (required) | path | string | `{name}.{ext}`: any name of 1 to 80 characters `[A-Za-z0-9_-]` and a format enabled for the link (`png`, `jpeg` or `jpg`, `webp`, `pdf`). The name only sets the download filename; both are signed. |
| `link_id` (required) | path | string | Signed link id (`lnk_…`). |
| `params` | query | object | The template parameters (`allowed_params`), e.g. `title.text=Summer%20Sale`. |
| `sig` | query | string | base64url HMAC-SHA256 signature (`access: signed`). |

Responses:

- `200` The rendered file (PNG, JPEG, WebP or PDF per the extension).
- `304` Not modified (`If-None-Match`).
- `400` Parameter not allowed, over `max_length`, malformed or over 8 KB of query.
- `402` `plan_feature_unavailable` or `render_limit_reached` (no fallback image).
- `403` `invalid_signature` or `referrer_not_allowed`.
- `404` Unknown, disabled or deleted link, or a format the link does not enable.
- `410` `link_expired` (or the template was deleted).
- `422` The render failed (template error).
- `429` `rate_limited` (per IP and hour, `Retry-After`) or `quota_exceeded`.

## GET /v1/account

**Plan, renders and limits**

Responses:

- `200` 
- `401` Missing, invalid, expired or revoked credentials.
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified.
- `429` Rate limited (`Retry-After`).
- `503` Control plane unavailable.

## GET /v1/batches

**List batches**

Newest first. Test keys see test batches only.

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `cursor` | query | string |  |
| `limit` | query | integer | 1 to 100 (default 50). |
| `reference` | query | string |  |
| `status` | query | "canceled" \| "completed" \| "failed" \| "processing" \| "queued" |  |
| `template_id` | query | string |  |

Responses:

- `200` 
- `400` Unknown `status` or malformed `template_id`.
- `401` Missing, invalid, expired or revoked credentials.
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified.
- `429` Rate limited (`Retry-After`).

## POST /v1/batches

**Create a batch**

Renders one template version for many items — a JSON `items` array (≤ 25 MB body) or a CSV from `POST /v1/uploads` with an optional `csv_mapping` — sharing `output`, `delivery` and `webhook`. Items become ordinary renders (`source: batch`), released under your plan's bulk concurrency; `combine` adds a ZIP of all outputs and/or one merged PDF. Answers `202` right away; follow progress with `GET /v1/batches/{id}` or the `batch.progress` / `batch.completed` webhooks. Live items are billed as they are released: if the render limit is reached mid-batch, the batch pauses (`paused`, one `batch.progress`) and resumes on its own once renders are available again.

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `Idempotency-Key` | header | string | Replays the stored response for 24 h; a different body with the same key → 422. |

Request body (`application/json`): BatchRequest

| Field | Type | Description |
|---|---|---|
| `combine` | CombineSpec or null |  |
| `csv_mapping` | object or null |  |
| `csv_upload_id` | string or null |  |
| `delivery` | DeliverySpec or null |  |
| `engine` | string or null |  |
| `items` | array of BatchItemSpec or null |  |
| `metadata` | object or null |  |
| `output` | OutputSpec or null |  |
| `reference` | string or null |  |
| `template_id` (required) | string |  |
| `test` | boolean or null |  |
| `version` | string or integer |  |
| `webhook` | BatchWebhookSpec or null |  |

Responses:

- `202` Batch accepted (`Location: /v1/batches/{id}`).
- `400` Invalid request, items or CSV (`errors[]` with `/items/<i>/…` or `/csv/row/<n>`).
- `401` Missing, invalid, expired or revoked credentials.
- `402` Batches not on your plan, or render limit reached (`render_limit_reached`).
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified; or `region_not_enabled`: the workspace's enabled regions leave out this region.
- `404` Template, version or upload not found.
- `409` A request with this Idempotency-Key is still in progress.
- `410` Template deleted.
- `413` Body over 25 MB or an item's data over 1 MB.
- `422` Item data does not match the template schema, or the webhook URL is not allowed.
- `429` Rate limited (`Retry-After`).
- `503` Region degraded (`Retry-After`).

## GET /v1/batches/{batch_id}

**Retrieve a batch**

Counters, progress and — once combined — the ZIP / merged PDF with signed URLs.

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `batch_id` (required) | path | string | Batch id (`bat_…`). |

Responses:

- `200` 
- `401` Missing, invalid, expired or revoked credentials.
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified.
- `404` Batch not found.
- `429` Rate limited (`Retry-After`).

## POST /v1/batches/{batch_id}/cancel

**Cancel a batch**

Items not released yet are canceled; released renders are canceled when a worker picks them up (items already rendering may still finish and are counted). The batch is `canceled` at once and `batch.completed` fires. Canceled items are not billed.

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `Idempotency-Key` | header | string | Replays the stored response for 24 h; a different body with the same key → 422. |
| `batch_id` (required) | path | string | Batch id (`bat_…`). |

Responses:

- `200` 
- `401` Missing, invalid, expired or revoked credentials.
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified.
- `404` Batch not found.
- `409` `conflict`: the batch already finished.
- `429` Rate limited (`Retry-After`).

## GET /v1/batches/{batch_id}/items

**List batch items**

Items in `index` order; `?status=failed` lists what `retry-failed` renders again.

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `batch_id` (required) | path | string | Batch id (`bat_…`). |
| `cursor` | query | string |  |
| `limit` | query | integer | 1 to 100 (default 50). |
| `status` | query | "canceled" \| "failed" \| "pending" \| "queued" \| "succeeded" |  |

Responses:

- `200` 
- `400` Unknown `status`.
- `401` Missing, invalid, expired or revoked credentials.
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified.
- `404` Batch not found.
- `429` Rate limited (`Retry-After`).

## POST /v1/batches/{batch_id}/resend-emails

**Resend a batch's failed emails**

For each item and rule, the newest email is resent when its status is listed (`failed` by default; `unknown` only when listed). Emails that can't be resent are listed in `skipped` with the reason (at most 100).

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `Idempotency-Key` | header | string | Replays the stored response for 24 h; a different body with the same key → 422. |
| `batch_id` (required) | path | string | Batch id (`bat_…`). |

Request body (`application/json`): BatchResendEmailsRequest

| Field | Type | Description |
|---|---|---|
| `statuses` | array of "failed" \| "unknown" | Resend the newest email of each render and rule when its status is listed. |

Responses:

- `200` 
- `400` Unknown status.
- `401` Missing, invalid, expired or revoked credentials.
- `402` Email delivery needs the Growth plan or higher (`plan_feature_unavailable`).
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified.
- `404` Batch not found.
- `429` Rate limited (`Retry-After`).

## POST /v1/batches/{batch_id}/retry-failed

**Retry the failed items**

For a finished batch with failed items: those items are rendered again as new renders (the old render id stays in `error.previous_render_id`), earlier combined files are dropped and the batch is `processing` until it finishes again. Zero-retention batches can't be retried: their items keep no data once they finish. Nor can a batch with `combine` whose succeeded items' files expired or were deleted: its new combined files couldn't hold those items.

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `Idempotency-Key` | header | string | Replays the stored response for 24 h; a different body with the same key → 422. |
| `batch_id` (required) | path | string | Batch id (`bat_…`). |

Responses:

- `202` 
- `401` Missing, invalid, expired or revoked credentials.
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified.
- `404` Batch not found.
- `409` `conflict`: the batch is still running or has no failed items. `zero_retention`: a zero-retention batch keeps no item data; submit the failed items in a new batch. `item_files_expired`: the batch has `combine` and the files of succeeded items expired or were deleted (`items` lists their indexes, at most 100); submit the failed items in a new batch.
- `429` Rate limited (`Retry-After`).

## POST /v1/einvoices

**Create an e-invoice XML**

Generates a standalone e-invoice from the invoice model: UN/CEFACT CII (every profile) or UBL 2.1 (`EN16931`, `XRECHNUNG`), e.g. an XRechnung for German public buyers. The XML is validated with the KoSIT validator (report in `result.conformance.einvoice`) and delivered as one `application/xml` file; a model that breaks a profile rule fails with `einvoice_invalid` and the list of issues. Answers a Render object with `input_type: "einvoice"` and `kind: "einvoice_xml"`; `delivery`, `mode`, `webhook`, `reference`, `metadata` and `test` behave as on `POST /v1/renders`. 1 billed render. Test renders are free, and their XML has a TEST note as its first invoice note (BT-22; every profile but `MINIMUM`, which has no notes; `EXTENDED` also sets `TestIndicator`). To embed the invoice in a PDF instead, use `output.einvoice` on a render.

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `Idempotency-Key` | header | string | Replays the stored response for 24 h; a different body with the same key → 422. |

Request body (`application/json`): EInvoiceRequest

| Field | Type | Description |
|---|---|---|
| `delivery` | DeliverySpec or null |  |
| `filename` | string or null | Output filename (`.xml` is enforced). Default: the invoice number, sanitised, + `.xml`. |
| `invoice` (required) | object | The invoice model (EN 16931 business terms). |
| `metadata` | object or null |  |
| `mode` | "sync" \| "async" |  |
| `priority` | "normal" \| "low" |  |
| `profile` (required) | "MINIMUM" \| "BASIC_WL" \| "BASIC" \| "EN16931" \| "EXTENDED" \| "XRECHNUNG" | EN 16931 profile of the XML: `XRECHNUNG` (German public sector), `EN16931`, or a Factur-X / ZUGFeRD profile (`MINIMUM`, `BASIC_WL`, `BASIC`, `EXTENDED`; CII only). |
| `reference` | string or null |  |
| `syntax` | "cii" \| "ubl" | `cii` (UN/CEFACT Cross Industry Invoice D16B) or `ubl` (UBL 2.1 Invoice / CreditNote; `EN16931` and `XRECHNUNG` only). |
| `test` | boolean or null |  |
| `timeout_behavior` | "continue" \| "cancel" |  |
| `webhook` | WebhookSpec or null |  |

Responses:

- `200` Sync render finished (`status=succeeded`). With `delivery.type=binary` the body is the file.
- `202` Async render accepted, or the sync timeout was exceeded (`Location: /v1/renders/{id}`).
- `400` Malformed or invalid request (`errors[]` with JSON pointers), e.g. an unknown, switched-off or another template's rule at `/delivery/email/<i>/rule_id`.
- `401` Missing, invalid, expired or revoked credentials.
- `402` Render limit reached (`render_limit_reached`) or feature not on plan (`plan_feature_unavailable`, e.g. a `delivery.email` list below the Growth plan).
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified; or `region_not_enabled`: the workspace's enabled regions leave out this region.
- `404` Template or version not found.
- `408` Sync timeout exceeded and the render was canceled (`timeout_behavior=cancel`).
- `409` A request with this Idempotency-Key is still in progress.
- `410` Template deleted.
- `413` Payload or output too large.
- `422` Render failed (`render_id`, `render_error`; `render_failed` when the worker reports no more specific code), schema mismatch, URL not allowed or idempotency key reused. `einvoice_invalid`, `einvoice_validation_failed`, `pdfa_conversion_failed` and `pdfua_validation_failed` also carry the conformance report as `result`. `email_needs_hosted_file`: `delivery.email` lists rules, but a zero-retention render keeps no file to email. `render_canceled`: the sync render was canceled, or its job expired, before it finished. `scene_invalid`, `data_invalid`: a canvas template's scene can't be rendered (e.g. an output over 16,384 px per side), or its `data` isn't an object or lacks a required dynamic property.
- `429` Rate limited (`Retry-After`).
- `503` Region degraded (`Retry-After`).

## GET /v1/email-connections

**List email connections**

The workspace's email provider accounts. Credentials are never returned; `egress_ips` are the addresses to authorize at the provider.

Responses:

- `200` 
- `401` Missing, invalid, expired or revoked credentials.
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified.
- `429` Rate limited (`Retry-After`).
- `503` The control plane is unreachable (`Retry-After`).

## POST /v1/email-connections

**Create an email connection**

Connects your own provider account. `provider` and what it takes:

* `brevo`: `credentials.api_key`, an API key (Brevo's SMTP key is not accepted).
* `smtp`: `config` `host`, `port` (587 or 2525, with STARTTLS) and `username`; `credentials.password`.
* `postmark`: `config.message_stream` (default `outbound`); `credentials.server_token`.
* `resend`: `credentials.api_key`.
* `ses` (Amazon SES): `config.region` and optionally `config.configuration_set`; `credentials` `access_key_id` and `secret_access_key`.
* `graph` (Microsoft 365): `config` `tenant_id`, `client_id` and `sender`, the mailbox that sends; `credentials.client_secret` of an app with the `Mail.Send` application permission.

Every provider takes `config.attachment_budget_bytes` and `config.max_per_second`; reads show them with the provider's defaults filled in. At most 10 connections per workspace; `provider` can't change later. `credentials` are encrypted and never returned. Authorize `egress_ips` at providers that restrict calling IPs (Brevo: Security → Authorized IPs).

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `Idempotency-Key` | header | string | Replays the stored response for 24 h; a different body with the same key → 422. |

Request body (`application/json`, required): EmailConnectionWrite

| Field | Type | Description |
|---|---|---|
| `config` | object | Per provider: SMTP `{host, port, username}` (port 587 or 2525); Postmark `{message_stream}` (default `outbound`, also when empty or null); Amazon SES `{region, configuration_set?}`; Microsoft 365 `{tenant_id, client_id, sender}` (`sender`: the mailbox that sends); Brevo and Resend none. Every provider: `attachment_budget_bytes`, the bytes of files attached per email (default / maximum: Brevo 10 / 13 MiB, SMTP 6 / 25 MiB, Postmark 6 / 6 MiB, Resend 10 / 25 MiB, Amazon SES 10 / 25 MiB, Microsoft 365 2 / 2 MiB), and `max_per_second` (1 to 100, default 10). |
| `credentials` | object | Write-only: Brevo `{api_key}` (an API key, not its SMTP key), SMTP `{password}`, Postmark `{server_token}`, Resend `{api_key}`, Amazon SES `{access_key_id, secret_access_key}`, Microsoft 365 `{client_secret}`. A PATCH without them keeps them. |
| `default_from` (required) | EmailDefaultFromWrite |  |
| `name` (required) | string |  |
| `provider` (required) | "brevo" \| "smtp" \| "postmark" \| "resend" \| "ses" \| "graph" | `brevo`, `smtp`, `postmark`, `resend`, `ses` (Amazon SES) or `graph` (Microsoft 365); it can't change after creation. * `brevo` - Brevo * `smtp` - SMTP * `postmark` - Postmark * `resend` - Resend * `ses` - Amazon SES * `graph` - Microsoft 365 (Graph) |
| `reply_to` | string \| null |  |
| `test_mode` | "sandbox" \| "redirect" \| "skip" | Test renders: `sandbox` (Brevo only, and its default: Brevo checks the email without delivering it), `redirect` (to `test_recipient`, subject prefixed `[Test] `), `skip` (no provider call; the default for every other provider). * `sandbox` - Sandbox * `redirect` - Redirect * `skip` - Skip |
| `test_recipient` | string \| null | The verified address of a workspace member; required with `test_mode: redirect`. |

Responses:

- `201` 
- `400` Invalid request body (`errors[]` with JSON pointers).
- `401` Missing, invalid, expired or revoked credentials.
- `402` Email delivery needs the Growth plan or higher (`plan_feature_unavailable`).
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified; or `feature_not_enabled`: email delivery isn't enabled for this workspace yet.
- `429` Rate limited (`Retry-After`).
- `503` The control plane is unreachable (`Retry-After`).

## GET /v1/email-connections/{connection_id}

**Retrieve an email connection**

Common base: API key or OAuth only, never a dashboard region token.

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `connection_id` (required) | path | string | Email connection id (`emc_…`). |

Responses:

- `200` 
- `401` Missing, invalid, expired or revoked credentials.
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified.
- `404` Email connection not found.
- `429` Rate limited (`Retry-After`).
- `503` The control plane is unreachable (`Retry-After`).

## PATCH /v1/email-connections/{connection_id}

**Update an email connection**

Partial update; a body without `credentials` keeps them. A `config` replaces the stored one as a whole. `provider` is immutable.

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `connection_id` (required) | path | string | Email connection id (`emc_…`). |

Request body (`application/json`): PatchedEmailConnectionWrite

| Field | Type | Description |
|---|---|---|
| `config` | object | Per provider: SMTP `{host, port, username}` (port 587 or 2525); Postmark `{message_stream}` (default `outbound`, also when empty or null); Amazon SES `{region, configuration_set?}`; Microsoft 365 `{tenant_id, client_id, sender}` (`sender`: the mailbox that sends); Brevo and Resend none. Every provider: `attachment_budget_bytes`, the bytes of files attached per email (default / maximum: Brevo 10 / 13 MiB, SMTP 6 / 25 MiB, Postmark 6 / 6 MiB, Resend 10 / 25 MiB, Amazon SES 10 / 25 MiB, Microsoft 365 2 / 2 MiB), and `max_per_second` (1 to 100, default 10). |
| `credentials` | object | Write-only: Brevo `{api_key}` (an API key, not its SMTP key), SMTP `{password}`, Postmark `{server_token}`, Resend `{api_key}`, Amazon SES `{access_key_id, secret_access_key}`, Microsoft 365 `{client_secret}`. A PATCH without them keeps them. |
| `default_from` | EmailDefaultFromWrite |  |
| `name` | string |  |
| `provider` | "brevo" \| "smtp" \| "postmark" \| "resend" \| "ses" \| "graph" | `brevo`, `smtp`, `postmark`, `resend`, `ses` (Amazon SES) or `graph` (Microsoft 365); it can't change after creation. * `brevo` - Brevo * `smtp` - SMTP * `postmark` - Postmark * `resend` - Resend * `ses` - Amazon SES * `graph` - Microsoft 365 (Graph) |
| `reply_to` | string \| null |  |
| `test_mode` | "sandbox" \| "redirect" \| "skip" | Test renders: `sandbox` (Brevo only, and its default: Brevo checks the email without delivering it), `redirect` (to `test_recipient`, subject prefixed `[Test] `), `skip` (no provider call; the default for every other provider). * `sandbox` - Sandbox * `redirect` - Redirect * `skip` - Skip |
| `test_recipient` | string \| null | The verified address of a workspace member; required with `test_mode: redirect`. |

Responses:

- `200` 
- `400` Invalid request body (`errors[]` with JSON pointers).
- `401` Missing, invalid, expired or revoked credentials.
- `402` Email delivery needs the Growth plan or higher (`plan_feature_unavailable`).
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified; or `feature_not_enabled`: email delivery isn't enabled for this workspace yet.
- `404` Email connection not found.
- `429` Rate limited (`Retry-After`).
- `503` The control plane is unreachable (`Retry-After`).

## DELETE /v1/email-connections/{connection_id}

**Delete an email connection**

A connection that rules use answers `409 email_connection_in_use`: delete or move those rules first. Works on every plan.

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `connection_id` (required) | path | string | Email connection id (`emc_…`). |

Responses:

- `204` Deleted.
- `401` Missing, invalid, expired or revoked credentials.
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified.
- `404` Email connection not found.
- `409` Rules use the connection (`email_connection_in_use`).
- `429` Rate limited (`Retry-After`).
- `503` The control plane is unreachable (`Retry-After`).

## POST /v1/email-connections/{connection_id}/test

**Test an email connection**

Checks the saved settings from the region that sends, step by step:

* `brevo`: `credentials`, `senders` (the verified senders and domains) and `sandbox_send` (Brevo checks an email from the default sender without delivering it).
* `smtp`: `connect`, `starttls`, `smtp_auth`.
* `postmark`: `credentials`.
* `resend`: `credentials`, which also lists the verified domains.
* `ses`: `credentials`, `senders` (the verified identities; it fails when there are none).
* `graph`: `credentials` (Microsoft Entra ID issues a token).

The senders found are stored as the connection's `senders`. `send_to` adds a real test email (`send`) to a verified member address. `200` with `ok: false` means a step failed; the test stops there. A failed `credentials` or `smtp_auth` step marks the connection `failing`.

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `Idempotency-Key` | header | string | Replays the stored response for 24 h; a different body with the same key → 422. |
| `connection_id` (required) | path | string | Email connection id (`emc_…`). |

Request body (`application/json`): EmailConnectionTestRequest

| Field | Type | Description |
|---|---|---|
| `send_to` | string \| null | Also send a real test email to this address: the verified address of a workspace member. |

Responses:

- `200` The test ran; check `ok`.
- `400` Invalid request body (`errors[]` with JSON pointers).
- `401` Missing, invalid, expired or revoked credentials.
- `402` Email delivery needs the Growth plan or higher (`plan_feature_unavailable`).
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified; or `feature_not_enabled`: email delivery isn't enabled for this workspace yet.
- `404` Email connection not found.
- `429` Rate limited (`Retry-After`).
- `503` The control plane is unreachable (`Retry-After`).

## POST /v1/email-rules/preview

**Preview a rule that is not saved yet**

A watermarked preview render of the template (its live version, else its draft) with the rule's email rendered in the document's context: the fields, the recipients as parsed and the attachments as they would be planned. Nothing is sent and nothing is billed. The data is `data`, else the dataset `dataset_id`, else the template's default dataset, else `{}`. `rule.connection_id` names the connection.

Request body (`application/json`): EmailRuleDraftPreviewRequest

| Field | Type | Description |
|---|---|---|
| `data` | object or null |  |
| `dataset_id` | string or null |  |
| `rule` (required) | EmailRuleSpec | The editable fields of an email rule, as the rule editor holds them (unsaved state, §24.17). Used instead of the saved rule; fields the editor does not know (`name`, `enabled`, …) are ignored. `null` counts as empty. Syntax errors come back as the preview's `email_preview.error`. |
| `template_id` (required) | string |  |

Responses:

- `200` 
- `202` The preview did not finish within the sync wait (`email_preview: null`).
- `400` Invalid request (`errors[]`), e.g. an unknown `rule.connection_id`.
- `401` Missing, invalid, expired or revoked credentials.
- `402` Email delivery needs the Growth plan or higher (`plan_feature_unavailable`).
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified.
- `404` Rule, template, connection or dataset not found.
- `410` Template deleted.
- `422` The document failed to render (`render_error`).
- `429` Rate limited (`Retry-After`).
- `503` Region degraded or the email checks are unavailable (`Retry-After`).

## GET /v1/email-rules/{rule_id}

**Retrieve an email rule**

Common base: API key or OAuth only, never a dashboard region token.

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `rule_id` (required) | path | string | Email rule id (`emr_…`). |

Responses:

- `200` 
- `401` Missing, invalid, expired or revoked credentials.
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified.
- `404` Email rule not found.
- `429` Rate limited (`Retry-After`).
- `503` The control plane is unreachable (`Retry-After`).

## PATCH /v1/email-rules/{rule_id}

**Update an email rule**

Partial update; changing `position` moves the others. Switching a rule off (`enabled: false`) also cancels its emails that were not sent yet. Below the Growth plan, or while email delivery isn't enabled for the workspace, an update may only switch the rule off.

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `rule_id` (required) | path | string | Email rule id (`emr_…`). |

Request body (`application/json`): PatchedEmailRuleWrite

| Field | Type | Description |
|---|---|---|
| `attach_formats` | array of "pdf" \| "png" \| "jpeg" \| "webp" \| "html" \| "xml" \| "zip" | Only files of these formats; empty = every file. |
| `attachments` | "attach_or_link" \| "attach" \| "link" \| "none" | * `attach_or_link` - Attach Or Link * `attach` - Attach * `link` - Link * `none` - None |
| `bcc` | string \| null |  |
| `cc` | string \| null |  |
| `condition` | string \| null | An expression; the rule sends when it is true. Empty = always. |
| `connection_id` | string | `emc_…` of this workspace. |
| `enabled` | boolean |  |
| `from_email` | string \| null | Literal; empty = the connection's default sender. One the provider hasn't verified is saved with the warning `email_sender_unverified`. |
| `from_name` | string \| null | Template source; one line after rendering. |
| `html` | string \| null | Template source, values HTML-escaped (≤ 256 KiB). |
| `name` | string |  |
| `position` | integer | 0 to 4; the others move. Default: the end. |
| `provider_template` | EmailProviderTemplate or null |  |
| `reply_to` | string \| null | Template source; one line after rendering. |
| `subject` | string \| null | Template source; required unless a provider template gives it. |
| `text` | string \| null | Template source; empty = derived from `html`. |
| `to` | string \| null | Template source rendering to addresses separated by `,` `;` or line breaks. |

Responses:

- `200` 
- `400` Invalid request body (`errors[]` with JSON pointers).
- `401` Missing, invalid, expired or revoked credentials.
- `402` Email delivery needs the Growth plan or higher (`plan_feature_unavailable`).
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified; or `feature_not_enabled`: email delivery isn't enabled for this workspace yet.
- `404` Email rule not found.
- `422` A field does not compile (`email_template_error`).
- `429` Rate limited (`Retry-After`).
- `503` The control plane is unreachable (`Retry-After`).

## DELETE /v1/email-rules/{rule_id}

**Delete an email rule**

Its emails that were not sent yet are canceled. Works on every plan.

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `rule_id` (required) | path | string | Email rule id (`emr_…`). |

Responses:

- `204` Deleted.
- `401` Missing, invalid, expired or revoked credentials.
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified.
- `404` Email rule not found.
- `429` Rate limited (`Retry-After`).
- `503` The control plane is unreachable (`Retry-After`).

## POST /v1/email-rules/{rule_id}/preview

**Preview an email rule**

A watermarked preview render of the template (its live version, else its draft) with the rule's email rendered in the document's context: the fields, the recipients as parsed and the attachments as they would be planned. Nothing is sent and nothing is billed. The data is `data`, else the dataset `dataset_id`, else the template's default dataset, else `{}`. Works for switched-off rules; `rule` previews unsaved fields instead of the saved ones.

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `rule_id` (required) | path | string | Email rule id (`emr_…`). |

Request body (`application/json`): EmailRulePreviewRequest

| Field | Type | Description |
|---|---|---|
| `data` | object or null |  |
| `dataset_id` | string or null | A dataset (`ds_…`) of the template. |
| `rule` | EmailRuleSpec or null | Unsaved fields instead of the saved rule. |

Responses:

- `200` 
- `202` The preview did not finish within the sync wait (`email_preview: null`).
- `400` Invalid request (`errors[]`), e.g. an unknown `rule.connection_id`.
- `401` Missing, invalid, expired or revoked credentials.
- `402` Email delivery needs the Growth plan or higher (`plan_feature_unavailable`).
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified.
- `404` Rule, template, connection or dataset not found.
- `410` Template deleted.
- `422` The document failed to render (`render_error`).
- `429` Rate limited (`Retry-After`).
- `503` Region degraded or the email checks are unavailable (`Retry-After`).

## POST /v1/email-rules/{rule_id}/test

**Send a test email**

Renders the template (its live version, else its draft) as a test render (free, watermarked) whose one email is this rule's, redirected to `to` with the subject prefixed `[Test] `. `to` must be the verified address of an active member of the workspace. Works for switched-off rules; `rule` tests unsaved fields. Follow the render: its `email[]` shows the send.

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `Idempotency-Key` | header | string | Replays the stored response for 24 h; a different body with the same key → 422. |
| `rule_id` (required) | path | string | Email rule id (`emr_…`). |

Request body (`application/json`): EmailRuleTestRequest

| Field | Type | Description |
|---|---|---|
| `data` | object or null |  |
| `dataset_id` | string or null |  |
| `rule` | EmailRuleSpec or null | Unsaved fields instead of the saved rule. |
| `to` (required) | string | The verified email address of an active member of the workspace. |

Responses:

- `202` The test render was queued (`Location: /v1/renders/{id}`).
- `400` Invalid request, or `to` is not a verified member address (`/to`).
- `401` Missing, invalid, expired or revoked credentials.
- `402` Email delivery needs the Growth plan or higher (`plan_feature_unavailable`).
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified.
- `404` Rule, template, connection or dataset not found.
- `410` Template deleted.
- `422` Zero-retention workspaces keep no file to email (`email_needs_hosted_file`).
- `429` Rate limited (`Retry-After`).
- `503` Region degraded (`Retry-After`).

## GET /v1/email-sends

**List email sends**

Newest first; sends are kept for 30 days. Test keys see test sends only.

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `batch_id` | query | string |  |
| `connection_id` | query | string |  |
| `created_after` | query | string |  |
| `cursor` | query | string |  |
| `limit` | query | integer | 1 to 100 (default 50). |
| `render_id` | query | string |  |
| `rule_id` | query | string |  |
| `status` | query | "canceled" \| "failed" \| "pending" \| "retrying" \| "sandboxed" \| "sending" \| "sent" \| "skipped" \| "unknown" |  |
| `template_id` | query | string |  |

Responses:

- `200` 
- `400` Malformed id, unknown `status` or invalid `created_after`.
- `401` Missing, invalid, expired or revoked credentials.
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified.
- `429` Rate limited (`Retry-After`).

## GET /v1/email-sends/stats

**Email statistics**

Today's recipients of live emails in this region against the daily cap (it resets at 00:00 UTC), and the sends of the last 30 days by status.

Responses:

- `200` 
- `401` Missing, invalid, expired or revoked credentials.
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified.
- `429` Rate limited (`Retry-After`).
- `503` Region degraded (`Retry-After`).

## GET /v1/email-sends/{send_id}

**Retrieve an email send**

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `send_id` (required) | path | string | Email send id (`ems_…`). |

Responses:

- `200` 
- `401` Missing, invalid, expired or revoked credentials.
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified.
- `404` Email send not found.
- `429` Rate limited (`Retry-After`).

## GET /v1/email-sends/{send_id}/events

**List a send's delivery events**

What Brevo reported after it accepted the email (delivered, deferred, bounces, blocks, spam complaints, invalid addresses), oldest first. Only Brevo connections have delivery events: other providers' sends list none. Also sent as the webhooks `email.delivered`, `email.bounced` and `email.complained`.

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `send_id` (required) | path | string | Email send id (`ems_…`). |

Responses:

- `200` 
- `401` Missing, invalid, expired or revoked credentials.
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified.
- `404` Email send not found.
- `429` Rate limited (`Retry-After`).

## POST /v1/email-sends/{send_id}/resend

**Resend an email**

Sends a `failed`, `unknown` or `sent` email again as a new send (`resend_of` set) with the same content and files; it counts toward today's cap. An email that failed before it was sent (template, recipients or attachments) or whose content was not kept (request logging off) answers `409 email_not_resendable`; a file that is no longer hosted `409 email_file_expired`.

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `Idempotency-Key` | header | string | Replays the stored response for 24 h; a different body with the same key → 422. |
| `send_id` (required) | path | string | Email send id (`ems_…`). |

Responses:

- `201` 
- `401` Missing, invalid, expired or revoked credentials.
- `402` Email delivery needs the Growth plan or higher (`plan_feature_unavailable`).
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified.
- `404` Email send not found.
- `409` `email_not_resendable` or `email_file_expired`.
- `429` Rate limited (`Retry-After`).
- `503` Region degraded (`Retry-After`).

## POST /v1/images/from-html

**Screenshot of HTML**

Convenience wrapper over `POST /v1/renders` with flat, no-code friendly fields.

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `Idempotency-Key` | header | string | Replays the stored response for 24 h; a different body with the same key → 422. |

Request body (`application/json`): object

| Field | Type | Description |
|---|---|---|
| `data` | object or null |  |
| `data_url` | string or null |  |
| `delivery` | DeliverySpec or "url" \| "binary" \| "base64" \| "none" |  |
| `engine` | string or null |  |
| `expires_in` | integer |  |
| `filename` | string | Jinja-enabled filename. |
| `format` | "png" \| "jpeg" \| "webp" |  |
| `head` | string or null |  |
| `hosted` | boolean or null | Keep the hosted copy; `false` deletes it once the storage uploads and email sends have succeeded. Absent or `null`: `false` when every storage destination of the render has `keep_hosted_copy: false`, else `true`. Zero-retention renders never keep one. |
| `html` (required) | string |  |
| `image` | ImageOptions or null |  |
| `metadata` | object or null |  |
| `mode` | "sync" \| "async" |  |
| `priority` | "normal" \| "low" |  |
| `reference` | string or null |  |
| `retention_days` | integer or null |  |
| `templating` | boolean or null |  |
| `test` | boolean or null |  |
| `timeout_behavior` | "continue" \| "cancel" |  |
| `webhook` | WebhookSpec or null |  |
| `webhook_url` | string |  |

Responses:

- `200` Sync render finished (`status=succeeded`). With `delivery.type=binary` the body is the file.
- `202` Async render accepted, or the sync timeout was exceeded (`Location: /v1/renders/{id}`).
- `400` Malformed or invalid request (`errors[]` with JSON pointers), e.g. an unknown, switched-off or another template's rule at `/delivery/email/<i>/rule_id`.
- `401` Missing, invalid, expired or revoked credentials.
- `402` Render limit reached (`render_limit_reached`) or feature not on plan (`plan_feature_unavailable`, e.g. a `delivery.email` list below the Growth plan).
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified; or `region_not_enabled`: the workspace's enabled regions leave out this region.
- `404` Template or version not found.
- `408` Sync timeout exceeded and the render was canceled (`timeout_behavior=cancel`).
- `409` A request with this Idempotency-Key is still in progress.
- `410` Template deleted.
- `413` Payload or output too large.
- `422` Render failed (`render_id`, `render_error`; `render_failed` when the worker reports no more specific code), schema mismatch, URL not allowed or idempotency key reused. `einvoice_invalid`, `einvoice_validation_failed`, `pdfa_conversion_failed` and `pdfua_validation_failed` also carry the conformance report as `result`. `email_needs_hosted_file`: `delivery.email` lists rules, but a zero-retention render keeps no file to email. `render_canceled`: the sync render was canceled, or its job expired, before it finished. `scene_invalid`, `data_invalid`: a canvas template's scene can't be rendered (e.g. an output over 16,384 px per side), or its `data` isn't an object or lacks a required dynamic property.
- `429` Rate limited (`Retry-After`).
- `503` Region degraded (`Retry-After`).

## POST /v1/images/from-template

**Image from a template**

Convenience wrapper over `POST /v1/renders` with flat, no-code friendly fields.

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `Idempotency-Key` | header | string | Replays the stored response for 24 h; a different body with the same key → 422. |

Request body (`application/json`): object

| Field | Type | Description |
|---|---|---|
| `data` | object or null |  |
| `data_url` | string or null |  |
| `delivery` | DeliverySpec or "url" \| "binary" \| "base64" \| "none" |  |
| `engine` | string or null |  |
| `expires_in` | integer |  |
| `filename` | string | Jinja-enabled filename. |
| `format` | "png" \| "jpeg" \| "webp" |  |
| `hosted` | boolean or null | Keep the hosted copy; `false` deletes it once the storage uploads and email sends have succeeded. Absent or `null`: `false` when every storage destination of the render has `keep_hosted_copy: false`, else `true`. Zero-retention renders never keep one. |
| `image` | ImageOptions or null |  |
| `metadata` | object or null |  |
| `mode` | "sync" \| "async" |  |
| `overrides` | array of object or null |  |
| `priority` | "normal" \| "low" |  |
| `reference` | string or null |  |
| `retention_days` | integer or null |  |
| `template_id` (required) | string |  |
| `test` | boolean or null |  |
| `timeout_behavior` | "continue" \| "cancel" |  |
| `version` | "live" \| "draft" or integer |  |
| `webhook` | WebhookSpec or null |  |
| `webhook_url` | string |  |

Responses:

- `200` Sync render finished (`status=succeeded`). With `delivery.type=binary` the body is the file.
- `202` Async render accepted, or the sync timeout was exceeded (`Location: /v1/renders/{id}`).
- `400` Malformed or invalid request (`errors[]` with JSON pointers), e.g. an unknown, switched-off or another template's rule at `/delivery/email/<i>/rule_id`.
- `401` Missing, invalid, expired or revoked credentials.
- `402` Render limit reached (`render_limit_reached`) or feature not on plan (`plan_feature_unavailable`, e.g. a `delivery.email` list below the Growth plan).
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified; or `region_not_enabled`: the workspace's enabled regions leave out this region.
- `404` Template or version not found.
- `408` Sync timeout exceeded and the render was canceled (`timeout_behavior=cancel`).
- `409` A request with this Idempotency-Key is still in progress.
- `410` Template deleted.
- `413` Payload or output too large.
- `422` Render failed (`render_id`, `render_error`; `render_failed` when the worker reports no more specific code), schema mismatch, URL not allowed or idempotency key reused. `einvoice_invalid`, `einvoice_validation_failed`, `pdfa_conversion_failed` and `pdfua_validation_failed` also carry the conformance report as `result`. `email_needs_hosted_file`: `delivery.email` lists rules, but a zero-retention render keeps no file to email. `render_canceled`: the sync render was canceled, or its job expired, before it finished. `scene_invalid`, `data_invalid`: a canvas template's scene can't be rendered (e.g. an output over 16,384 px per side), or its `data` isn't an object or lacks a required dynamic property.
- `429` Rate limited (`Retry-After`).
- `503` Region degraded (`Retry-After`).

## POST /v1/images/from-url

**Screenshot of a URL**

Convenience wrapper over `POST /v1/renders` with flat, no-code friendly fields.

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `Idempotency-Key` | header | string | Replays the stored response for 24 h; a different body with the same key → 422. |

Request body (`application/json`): object

| Field | Type | Description |
|---|---|---|
| `data` | object or null |  |
| `data_url` | string or null |  |
| `delivery` | DeliverySpec or "url" \| "binary" \| "base64" \| "none" |  |
| `engine` | string or null |  |
| `expires_in` | integer |  |
| `filename` | string | Jinja-enabled filename. |
| `format` | "png" \| "jpeg" \| "webp" |  |
| `hosted` | boolean or null | Keep the hosted copy; `false` deletes it once the storage uploads and email sends have succeeded. Absent or `null`: `false` when every storage destination of the render has `keep_hosted_copy: false`, else `true`. Zero-retention renders never keep one. |
| `http` | HttpOptions or null |  |
| `image` | ImageOptions or null |  |
| `metadata` | object or null |  |
| `mode` | "sync" \| "async" |  |
| `priority` | "normal" \| "low" |  |
| `reference` | string or null |  |
| `retention_days` | integer or null |  |
| `test` | boolean or null |  |
| `timeout_behavior` | "continue" \| "cancel" |  |
| `url` (required) | string |  |
| `webhook` | WebhookSpec or null |  |
| `webhook_url` | string |  |

Responses:

- `200` Sync render finished (`status=succeeded`). With `delivery.type=binary` the body is the file.
- `202` Async render accepted, or the sync timeout was exceeded (`Location: /v1/renders/{id}`).
- `400` Malformed or invalid request (`errors[]` with JSON pointers), e.g. an unknown, switched-off or another template's rule at `/delivery/email/<i>/rule_id`.
- `401` Missing, invalid, expired or revoked credentials.
- `402` Render limit reached (`render_limit_reached`) or feature not on plan (`plan_feature_unavailable`, e.g. a `delivery.email` list below the Growth plan).
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified; or `region_not_enabled`: the workspace's enabled regions leave out this region.
- `404` Template or version not found.
- `408` Sync timeout exceeded and the render was canceled (`timeout_behavior=cancel`).
- `409` A request with this Idempotency-Key is still in progress.
- `410` Template deleted.
- `413` Payload or output too large.
- `422` Render failed (`render_id`, `render_error`; `render_failed` when the worker reports no more specific code), schema mismatch, URL not allowed or idempotency key reused. `einvoice_invalid`, `einvoice_validation_failed`, `pdfa_conversion_failed` and `pdfua_validation_failed` also carry the conformance report as `result`. `email_needs_hosted_file`: `delivery.email` lists rules, but a zero-retention render keeps no file to email. `render_canceled`: the sync render was canceled, or its job expired, before it finished. `scene_invalid`, `data_invalid`: a canvas template's scene can't be rendered (e.g. an output over 16,384 px per side), or its `data` isn't an object or lacks a required dynamic property.
- `429` Rate limited (`Retry-After`).
- `503` Region degraded (`Retry-After`).

## POST /v1/pdf-tools/merge

**Merge PDFs**

Merge 2-200 PDFs into one document (docs/05 §5). Answers a Render object with `input_type: "pdf_tool"`; `delivery`, `mode`, `webhook`, `reference`, `metadata` and `test` behave as on `POST /v1/renders`.

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `Idempotency-Key` | header | string | Replays the stored response for 24 h; a different body with the same key → 422. |

Request body (`application/json`): MergeRequest

| Field | Type | Description |
|---|---|---|
| `bookmarks` | "filenames" \| "none" |  |
| `delivery` | DeliverySpec or null |  |
| `filename` | string or null |  |
| `metadata` | object or null |  |
| `mode` | "sync" \| "async" |  |
| `priority` | "normal" \| "low" |  |
| `reference` | string or null |  |
| `sources` (required) | array of PdfSourceSpec |  |
| `test` | boolean or null |  |
| `timeout_behavior` | "continue" \| "cancel" |  |
| `webhook` | WebhookSpec or null |  |

Responses:

- `200` Sync render finished (`status=succeeded`). With `delivery.type=binary` the body is the file.
- `202` Async render accepted, or the sync timeout was exceeded (`Location: /v1/renders/{id}`).
- `400` Malformed or invalid request (`errors[]` with JSON pointers), e.g. an unknown, switched-off or another template's rule at `/delivery/email/<i>/rule_id`.
- `401` Missing, invalid, expired or revoked credentials.
- `402` Render limit reached (`render_limit_reached`) or feature not on plan (`plan_feature_unavailable`, e.g. a `delivery.email` list below the Growth plan).
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified; or `region_not_enabled`: the workspace's enabled regions leave out this region.
- `404` Template or version not found.
- `408` Sync timeout exceeded and the render was canceled (`timeout_behavior=cancel`).
- `409` A request with this Idempotency-Key is still in progress.
- `410` Template deleted.
- `413` Payload or output too large.
- `422` Render failed (`render_id`, `render_error`; `render_failed` when the worker reports no more specific code), schema mismatch, URL not allowed or idempotency key reused. `einvoice_invalid`, `einvoice_validation_failed`, `pdfa_conversion_failed` and `pdfua_validation_failed` also carry the conformance report as `result`. `email_needs_hosted_file`: `delivery.email` lists rules, but a zero-retention render keeps no file to email. `render_canceled`: the sync render was canceled, or its job expired, before it finished. `scene_invalid`, `data_invalid`: a canvas template's scene can't be rendered (e.g. an output over 16,384 px per side), or its `data` isn't an object or lacks a required dynamic property.
- `429` Rate limited (`Retry-After`).
- `503` Region degraded (`Retry-After`).

## POST /v1/pdf-tools/metadata

**Read or set PDF metadata**

Without `set`: no file, and the render's `result` is `{pages, page_sizes, encrypted, pdf_version, metadata}`. `page_sizes` lists runs of pages shown at one size (`{"width": "210mm", "height": "297mm", "pages": "1-10"}`); `metadata` holds `title`, `author`, `subject`, `keywords` (a list), `creator`, `producer`, `created`, `modified` and `lang`, null when absent. A PDF that needs a password still answers `pages` and `encrypted: true`, with `metadata` null. With `set` (`title`, `author`, `subject`, `keywords`, `creator`, `producer`, `lang`): the PDF with only those fields changed (`""`, `null` or `[]` clears one) and the same `result` for it; an encrypted source fails with `pdf_password_required` (422). A read cannot use `delivery.type: "binary"`. 1 billed render. Answers a Render object with `input_type: "pdf_tool"`; `delivery`, `mode`, `webhook`, `reference`, `metadata` and `test` behave as on `POST /v1/renders`.

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `Idempotency-Key` | header | string | Replays the stored response for 24 h; a different body with the same key → 422. |

Request body (`application/json`): PdfMetadataRequest

| Field | Type | Description |
|---|---|---|
| `delivery` | DeliverySpec or null |  |
| `filename` | string or null |  |
| `metadata` | object or null |  |
| `mode` | "sync" \| "async" |  |
| `priority` | "normal" \| "low" |  |
| `reference` | string or null |  |
| `set` | object or null |  |
| `source` (required) | PdfSourceSpec | Exactly one of `url`, `render_id` (+ optional `file_id`), `upload_id` or `data_uri`. |
| `test` | boolean or null |  |
| `timeout_behavior` | "continue" \| "cancel" |  |
| `webhook` | WebhookSpec or null |  |

Responses:

- `200` Sync render finished (`status=succeeded`). With `delivery.type=binary` the body is the file.
- `202` Async render accepted, or the sync timeout was exceeded (`Location: /v1/renders/{id}`).
- `400` Malformed or invalid request (`errors[]` with JSON pointers), e.g. an unknown, switched-off or another template's rule at `/delivery/email/<i>/rule_id`.
- `401` Missing, invalid, expired or revoked credentials.
- `402` Render limit reached (`render_limit_reached`) or feature not on plan (`plan_feature_unavailable`, e.g. a `delivery.email` list below the Growth plan).
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified; or `region_not_enabled`: the workspace's enabled regions leave out this region.
- `404` Template or version not found.
- `408` Sync timeout exceeded and the render was canceled (`timeout_behavior=cancel`).
- `409` A request with this Idempotency-Key is still in progress.
- `410` Template deleted.
- `413` Payload or output too large.
- `422` Render failed (`render_id`, `render_error`; `render_failed` when the worker reports no more specific code), schema mismatch, URL not allowed or idempotency key reused. `einvoice_invalid`, `einvoice_validation_failed`, `pdfa_conversion_failed` and `pdfua_validation_failed` also carry the conformance report as `result`. `email_needs_hosted_file`: `delivery.email` lists rules, but a zero-retention render keeps no file to email. `render_canceled`: the sync render was canceled, or its job expired, before it finished. `scene_invalid`, `data_invalid`: a canvas template's scene can't be rendered (e.g. an output over 16,384 px per side), or its `data` isn't an object or lacks a required dynamic property.
- `429` Rate limited (`Retry-After`).
- `503` Region degraded (`Retry-After`).

## POST /v1/pdf-tools/pdfa

**Convert a PDF to PDF/A**

Converts any PDF to PDF/A-2b or PDF/A-3b (best effort: fonts must be embedded and the source must not be encrypted) and validates the result with veraPDF; the report is `result.conformance.pdfa`. A PDF that cannot be made conformant fails with `pdfa_conversion_failed`. 1 billed render. Answers a Render object with `input_type: "pdf_tool"`; `delivery`, `mode`, `webhook`, `reference`, `metadata` and `test` behave as on `POST /v1/renders`.

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `Idempotency-Key` | header | string | Replays the stored response for 24 h; a different body with the same key → 422. |

Request body (`application/json`): PdfaRequest

| Field | Type | Description |
|---|---|---|
| `delivery` | DeliverySpec or null |  |
| `filename` | string or null |  |
| `level` (required) | "2b" \| "3b" | PDF/A conformance level: `2b` (ISO 19005-2) or `3b` (ISO 19005-3, allows attachments). |
| `metadata` | object or null |  |
| `mode` | "sync" \| "async" |  |
| `priority` | "normal" \| "low" |  |
| `reference` | string or null |  |
| `source` (required) | PdfSourceSpec | Exactly one of `url`, `render_id` (+ optional `file_id`), `upload_id` or `data_uri`. |
| `test` | boolean or null |  |
| `timeout_behavior` | "continue" \| "cancel" |  |
| `webhook` | WebhookSpec or null |  |

Responses:

- `200` Sync render finished (`status=succeeded`). With `delivery.type=binary` the body is the file.
- `202` Async render accepted, or the sync timeout was exceeded (`Location: /v1/renders/{id}`).
- `400` Malformed or invalid request (`errors[]` with JSON pointers), e.g. an unknown, switched-off or another template's rule at `/delivery/email/<i>/rule_id`.
- `401` Missing, invalid, expired or revoked credentials.
- `402` Render limit reached (`render_limit_reached`) or feature not on plan (`plan_feature_unavailable`, e.g. a `delivery.email` list below the Growth plan).
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified; or `region_not_enabled`: the workspace's enabled regions leave out this region.
- `404` Template or version not found.
- `408` Sync timeout exceeded and the render was canceled (`timeout_behavior=cancel`).
- `409` A request with this Idempotency-Key is still in progress.
- `410` Template deleted.
- `413` Payload or output too large.
- `422` Render failed (`render_id`, `render_error`; `render_failed` when the worker reports no more specific code), schema mismatch, URL not allowed or idempotency key reused. `einvoice_invalid`, `einvoice_validation_failed`, `pdfa_conversion_failed` and `pdfua_validation_failed` also carry the conformance report as `result`. `email_needs_hosted_file`: `delivery.email` lists rules, but a zero-retention render keeps no file to email. `render_canceled`: the sync render was canceled, or its job expired, before it finished. `scene_invalid`, `data_invalid`: a canvas template's scene can't be rendered (e.g. an output over 16,384 px per side), or its `data` isn't an object or lacks a required dynamic property.
- `429` Rate limited (`Retry-After`).
- `503` Region degraded (`Retry-After`).

## POST /v1/pdf-tools/protect

**Password-protect a PDF**

AES-256 encryption with a user and/or owner password, as the `pdf.protect` render option does. Unset `permissions` flags take the same defaults: `print`, `print_high_res`, `copy` and `fill_forms` allowed, `modify`, `annotate` and `assemble` denied. An encrypted source fails with `pdf_password_required` (422). 1 billed render. Answers a Render object with `input_type: "pdf_tool"`; `delivery`, `mode`, `webhook`, `reference`, `metadata` and `test` behave as on `POST /v1/renders`.

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `Idempotency-Key` | header | string | Replays the stored response for 24 h; a different body with the same key → 422. |

Request body (`application/json`): ProtectRequest

| Field | Type | Description |
|---|---|---|
| `delivery` | DeliverySpec or null |  |
| `filename` | string or null |  |
| `metadata` | object or null |  |
| `mode` | "sync" \| "async" |  |
| `owner_password` | string or null |  |
| `permissions` | object or null |  |
| `priority` | "normal" \| "low" |  |
| `reference` | string or null |  |
| `source` (required) | PdfSourceSpec | Exactly one of `url`, `render_id` (+ optional `file_id`), `upload_id` or `data_uri`. |
| `test` | boolean or null |  |
| `timeout_behavior` | "continue" \| "cancel" |  |
| `user_password` | string or null |  |
| `webhook` | WebhookSpec or null |  |

Responses:

- `200` Sync render finished (`status=succeeded`). With `delivery.type=binary` the body is the file.
- `202` Async render accepted, or the sync timeout was exceeded (`Location: /v1/renders/{id}`).
- `400` Malformed or invalid request (`errors[]` with JSON pointers), e.g. an unknown, switched-off or another template's rule at `/delivery/email/<i>/rule_id`.
- `401` Missing, invalid, expired or revoked credentials.
- `402` Render limit reached (`render_limit_reached`) or feature not on plan (`plan_feature_unavailable`, e.g. a `delivery.email` list below the Growth plan).
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified; or `region_not_enabled`: the workspace's enabled regions leave out this region.
- `404` Template or version not found.
- `408` Sync timeout exceeded and the render was canceled (`timeout_behavior=cancel`).
- `409` A request with this Idempotency-Key is still in progress.
- `410` Template deleted.
- `413` Payload or output too large.
- `422` Render failed (`render_id`, `render_error`; `render_failed` when the worker reports no more specific code), schema mismatch, URL not allowed or idempotency key reused. `einvoice_invalid`, `einvoice_validation_failed`, `pdfa_conversion_failed` and `pdfua_validation_failed` also carry the conformance report as `result`. `email_needs_hosted_file`: `delivery.email` lists rules, but a zero-retention render keeps no file to email. `render_canceled`: the sync render was canceled, or its job expired, before it finished. `scene_invalid`, `data_invalid`: a canvas template's scene can't be rendered (e.g. an output over 16,384 px per side), or its `data` isn't an object or lacks a required dynamic property.
- `429` Rate limited (`Retry-After`).
- `503` Region degraded (`Retry-After`).

## POST /v1/pdf-tools/rotate

**Rotate PDF pages**

Adds `degrees` (90, 180 or 270; negative turns anticlockwise) to the rotation of the pages in `pages` (`"all"`, `"1,3-5"`, `"4-"`); other pages keep theirs. A page outside the document fails with `validation_error` (422), naming it; an encrypted source with `pdf_password_required` (unlock it first). 1 billed render. Answers a Render object with `input_type: "pdf_tool"`; `delivery`, `mode`, `webhook`, `reference`, `metadata` and `test` behave as on `POST /v1/renders`.

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `Idempotency-Key` | header | string | Replays the stored response for 24 h; a different body with the same key → 422. |

Request body (`application/json`): RotateRequest

| Field | Type | Description |
|---|---|---|
| `degrees` (required) | 90 \| 180 \| 270 \| -90 \| -180 \| -270 |  |
| `delivery` | DeliverySpec or null |  |
| `filename` | string or null |  |
| `metadata` | object or null |  |
| `mode` | "sync" \| "async" |  |
| `pages` | string |  |
| `priority` | "normal" \| "low" |  |
| `reference` | string or null |  |
| `source` (required) | PdfSourceSpec | Exactly one of `url`, `render_id` (+ optional `file_id`), `upload_id` or `data_uri`. |
| `test` | boolean or null |  |
| `timeout_behavior` | "continue" \| "cancel" |  |
| `webhook` | WebhookSpec or null |  |

Responses:

- `200` Sync render finished (`status=succeeded`). With `delivery.type=binary` the body is the file.
- `202` Async render accepted, or the sync timeout was exceeded (`Location: /v1/renders/{id}`).
- `400` Malformed or invalid request (`errors[]` with JSON pointers), e.g. an unknown, switched-off or another template's rule at `/delivery/email/<i>/rule_id`.
- `401` Missing, invalid, expired or revoked credentials.
- `402` Render limit reached (`render_limit_reached`) or feature not on plan (`plan_feature_unavailable`, e.g. a `delivery.email` list below the Growth plan).
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified; or `region_not_enabled`: the workspace's enabled regions leave out this region.
- `404` Template or version not found.
- `408` Sync timeout exceeded and the render was canceled (`timeout_behavior=cancel`).
- `409` A request with this Idempotency-Key is still in progress.
- `410` Template deleted.
- `413` Payload or output too large.
- `422` Render failed (`render_id`, `render_error`; `render_failed` when the worker reports no more specific code), schema mismatch, URL not allowed or idempotency key reused. `einvoice_invalid`, `einvoice_validation_failed`, `pdfa_conversion_failed` and `pdfua_validation_failed` also carry the conformance report as `result`. `email_needs_hosted_file`: `delivery.email` lists rules, but a zero-retention render keeps no file to email. `render_canceled`: the sync render was canceled, or its job expired, before it finished. `scene_invalid`, `data_invalid`: a canvas template's scene can't be rendered (e.g. an output over 16,384 px per side), or its `data` isn't an object or lacks a required dynamic property.
- `429` Rate limited (`Retry-After`).
- `503` Region degraded (`Retry-After`).

## POST /v1/pdf-tools/split

**Split a PDF**

One file per entry of `ranges` (`"1-3"`, `"4-"` to the last page, `"1,3-5"`) or per run of `every` pages, at most 200, named `<stem>-<n>.pdf` (n from 1; `<stem>` is `filename` without `.pdf`, else `split`) and listed in that order in `files`. With `zip: true` a single `<stem>.zip` holds them instead. `delivery.type: "binary"` needs a single output (`zip: true` or exactly one range); `base64` carries every file, and storage destinations get every file (`file.index` in path templates). A page outside the document fails with `validation_error` (422), naming it; an encrypted source with `pdf_password_required` (unlock it first). 1 billed render. Answers a Render object with `input_type: "pdf_tool"`; `delivery`, `mode`, `webhook`, `reference`, `metadata` and `test` behave as on `POST /v1/renders`.

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `Idempotency-Key` | header | string | Replays the stored response for 24 h; a different body with the same key → 422. |

Request body (`application/json`): SplitRequest

| Field | Type | Description |
|---|---|---|
| `delivery` | DeliverySpec or null |  |
| `every` | integer or null | One output file per run of this many pages instead. |
| `filename` | string or null |  |
| `metadata` | object or null |  |
| `mode` | "sync" \| "async" |  |
| `priority` | "normal" \| "low" |  |
| `ranges` | array of string or null | Page lists, one output file each: `"1-3"`, `"4-"` (to the last page), `"7"`, `"1,3-5"`. Pages are numbered from 1. |
| `reference` | string or null |  |
| `source` (required) | PdfSourceSpec | Exactly one of `url`, `render_id` (+ optional `file_id`), `upload_id` or `data_uri`. |
| `test` | boolean or null |  |
| `timeout_behavior` | "continue" \| "cancel" |  |
| `webhook` | WebhookSpec or null |  |
| `zip` | boolean | Deliver one ZIP (`<stem>.zip`) holding the parts instead of one file per part. |

Responses:

- `200` Sync render finished (`status=succeeded`). With `delivery.type=binary` the body is the file.
- `202` Async render accepted, or the sync timeout was exceeded (`Location: /v1/renders/{id}`).
- `400` Malformed or invalid request (`errors[]` with JSON pointers), e.g. an unknown, switched-off or another template's rule at `/delivery/email/<i>/rule_id`.
- `401` Missing, invalid, expired or revoked credentials.
- `402` Render limit reached (`render_limit_reached`) or feature not on plan (`plan_feature_unavailable`, e.g. a `delivery.email` list below the Growth plan).
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified; or `region_not_enabled`: the workspace's enabled regions leave out this region.
- `404` Template or version not found.
- `408` Sync timeout exceeded and the render was canceled (`timeout_behavior=cancel`).
- `409` A request with this Idempotency-Key is still in progress.
- `410` Template deleted.
- `413` Payload or output too large.
- `422` Render failed (`render_id`, `render_error`; `render_failed` when the worker reports no more specific code), schema mismatch, URL not allowed or idempotency key reused. `einvoice_invalid`, `einvoice_validation_failed`, `pdfa_conversion_failed` and `pdfua_validation_failed` also carry the conformance report as `result`. `email_needs_hosted_file`: `delivery.email` lists rules, but a zero-retention render keeps no file to email. `render_canceled`: the sync render was canceled, or its job expired, before it finished. `scene_invalid`, `data_invalid`: a canvas template's scene can't be rendered (e.g. an output over 16,384 px per side), or its `data` isn't an object or lacks a required dynamic property.
- `429` Rate limited (`Retry-After`).
- `503` Region degraded (`Retry-After`).

## POST /v1/pdf-tools/unlock

**Remove a PDF password**

Decrypts a PDF with its user or owner password and removes its restrictions. A wrong password fails with `pdf_password_required` (422); an unencrypted PDF comes back unchanged. 1 billed render. Answers a Render object with `input_type: "pdf_tool"`; `delivery`, `mode`, `webhook`, `reference`, `metadata` and `test` behave as on `POST /v1/renders`.

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `Idempotency-Key` | header | string | Replays the stored response for 24 h; a different body with the same key → 422. |

Request body (`application/json`): UnlockRequest

| Field | Type | Description |
|---|---|---|
| `delivery` | DeliverySpec or null |  |
| `filename` | string or null |  |
| `metadata` | object or null |  |
| `mode` | "sync" \| "async" |  |
| `password` (required) | string |  |
| `priority` | "normal" \| "low" |  |
| `reference` | string or null |  |
| `source` (required) | PdfSourceSpec | Exactly one of `url`, `render_id` (+ optional `file_id`), `upload_id` or `data_uri`. |
| `test` | boolean or null |  |
| `timeout_behavior` | "continue" \| "cancel" |  |
| `webhook` | WebhookSpec or null |  |

Responses:

- `200` Sync render finished (`status=succeeded`). With `delivery.type=binary` the body is the file.
- `202` Async render accepted, or the sync timeout was exceeded (`Location: /v1/renders/{id}`).
- `400` Malformed or invalid request (`errors[]` with JSON pointers), e.g. an unknown, switched-off or another template's rule at `/delivery/email/<i>/rule_id`.
- `401` Missing, invalid, expired or revoked credentials.
- `402` Render limit reached (`render_limit_reached`) or feature not on plan (`plan_feature_unavailable`, e.g. a `delivery.email` list below the Growth plan).
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified; or `region_not_enabled`: the workspace's enabled regions leave out this region.
- `404` Template or version not found.
- `408` Sync timeout exceeded and the render was canceled (`timeout_behavior=cancel`).
- `409` A request with this Idempotency-Key is still in progress.
- `410` Template deleted.
- `413` Payload or output too large.
- `422` Render failed (`render_id`, `render_error`; `render_failed` when the worker reports no more specific code), schema mismatch, URL not allowed or idempotency key reused. `einvoice_invalid`, `einvoice_validation_failed`, `pdfa_conversion_failed` and `pdfua_validation_failed` also carry the conformance report as `result`. `email_needs_hosted_file`: `delivery.email` lists rules, but a zero-retention render keeps no file to email. `render_canceled`: the sync render was canceled, or its job expired, before it finished. `scene_invalid`, `data_invalid`: a canvas template's scene can't be rendered (e.g. an output over 16,384 px per side), or its `data` isn't an object or lacks a required dynamic property.
- `429` Rate limited (`Retry-After`).
- `503` Region degraded (`Retry-After`).

## POST /v1/pdf/from-html

**PDF from HTML**

Convenience wrapper over `POST /v1/renders` with flat, no-code friendly fields.

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `Idempotency-Key` | header | string | Replays the stored response for 24 h; a different body with the same key → 422. |

Request body (`application/json`): object

| Field | Type | Description |
|---|---|---|
| `data` | object or null |  |
| `data_url` | string or null |  |
| `delivery` | DeliverySpec or "url" \| "binary" \| "base64" \| "none" |  |
| `einvoice` | EInvoiceSpec or null |  |
| `engine` | string or null |  |
| `expires_in` | integer |  |
| `filename` | string | Jinja-enabled filename. |
| `head` | string or null |  |
| `hosted` | boolean or null | Keep the hosted copy; `false` deletes it once the storage uploads and email sends have succeeded. Absent or `null`: `false` when every storage destination of the render has `keep_hosted_copy: false`, else `true`. Zero-retention renders never keep one. |
| `html` (required) | string |  |
| `metadata` | object or null |  |
| `mode` | "sync" \| "async" |  |
| `pdf` | PdfOptions or null |  |
| `priority` | "normal" \| "low" |  |
| `reference` | string or null |  |
| `retention_days` | integer or null |  |
| `templating` | boolean or null |  |
| `test` | boolean or null |  |
| `timeout_behavior` | "continue" \| "cancel" |  |
| `webhook` | WebhookSpec or null |  |
| `webhook_url` | string |  |

Responses:

- `200` Sync render finished (`status=succeeded`). With `delivery.type=binary` the body is the file.
- `202` Async render accepted, or the sync timeout was exceeded (`Location: /v1/renders/{id}`).
- `400` Malformed or invalid request (`errors[]` with JSON pointers), e.g. an unknown, switched-off or another template's rule at `/delivery/email/<i>/rule_id`.
- `401` Missing, invalid, expired or revoked credentials.
- `402` Render limit reached (`render_limit_reached`) or feature not on plan (`plan_feature_unavailable`, e.g. a `delivery.email` list below the Growth plan).
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified; or `region_not_enabled`: the workspace's enabled regions leave out this region.
- `404` Template or version not found.
- `408` Sync timeout exceeded and the render was canceled (`timeout_behavior=cancel`).
- `409` A request with this Idempotency-Key is still in progress.
- `410` Template deleted.
- `413` Payload or output too large.
- `422` Render failed (`render_id`, `render_error`; `render_failed` when the worker reports no more specific code), schema mismatch, URL not allowed or idempotency key reused. `einvoice_invalid`, `einvoice_validation_failed`, `pdfa_conversion_failed` and `pdfua_validation_failed` also carry the conformance report as `result`. `email_needs_hosted_file`: `delivery.email` lists rules, but a zero-retention render keeps no file to email. `render_canceled`: the sync render was canceled, or its job expired, before it finished. `scene_invalid`, `data_invalid`: a canvas template's scene can't be rendered (e.g. an output over 16,384 px per side), or its `data` isn't an object or lacks a required dynamic property.
- `429` Rate limited (`Retry-After`).
- `503` Region degraded (`Retry-After`).

## POST /v1/pdf/from-markdown

**PDF from Markdown**

Convenience wrapper over `POST /v1/renders` with flat, no-code friendly fields.

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `Idempotency-Key` | header | string | Replays the stored response for 24 h; a different body with the same key → 422. |

Request body (`application/json`): object

| Field | Type | Description |
|---|---|---|
| `allow_raw_html` | boolean or null |  |
| `css` | string or null |  |
| `data` | object or null |  |
| `data_url` | string or null |  |
| `delivery` | DeliverySpec or "url" \| "binary" \| "base64" \| "none" |  |
| `einvoice` | EInvoiceSpec or null |  |
| `engine` | string or null |  |
| `expires_in` | integer |  |
| `filename` | string | Jinja-enabled filename. |
| `hosted` | boolean or null | Keep the hosted copy; `false` deletes it once the storage uploads and email sends have succeeded. Absent or `null`: `false` when every storage destination of the render has `keep_hosted_copy: false`, else `true`. Zero-retention renders never keep one. |
| `markdown` (required) | string |  |
| `metadata` | object or null |  |
| `mode` | "sync" \| "async" |  |
| `pdf` | PdfOptions or null |  |
| `priority` | "normal" \| "low" |  |
| `reference` | string or null |  |
| `retention_days` | integer or null |  |
| `templating` | boolean or null |  |
| `test` | boolean or null |  |
| `theme` | "default" \| "github" \| "academic" \| "minimal" \| "none" |  |
| `timeout_behavior` | "continue" \| "cancel" |  |
| `webhook` | WebhookSpec or null |  |
| `webhook_url` | string |  |

Responses:

- `200` Sync render finished (`status=succeeded`). With `delivery.type=binary` the body is the file.
- `202` Async render accepted, or the sync timeout was exceeded (`Location: /v1/renders/{id}`).
- `400` Malformed or invalid request (`errors[]` with JSON pointers), e.g. an unknown, switched-off or another template's rule at `/delivery/email/<i>/rule_id`.
- `401` Missing, invalid, expired or revoked credentials.
- `402` Render limit reached (`render_limit_reached`) or feature not on plan (`plan_feature_unavailable`, e.g. a `delivery.email` list below the Growth plan).
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified; or `region_not_enabled`: the workspace's enabled regions leave out this region.
- `404` Template or version not found.
- `408` Sync timeout exceeded and the render was canceled (`timeout_behavior=cancel`).
- `409` A request with this Idempotency-Key is still in progress.
- `410` Template deleted.
- `413` Payload or output too large.
- `422` Render failed (`render_id`, `render_error`; `render_failed` when the worker reports no more specific code), schema mismatch, URL not allowed or idempotency key reused. `einvoice_invalid`, `einvoice_validation_failed`, `pdfa_conversion_failed` and `pdfua_validation_failed` also carry the conformance report as `result`. `email_needs_hosted_file`: `delivery.email` lists rules, but a zero-retention render keeps no file to email. `render_canceled`: the sync render was canceled, or its job expired, before it finished. `scene_invalid`, `data_invalid`: a canvas template's scene can't be rendered (e.g. an output over 16,384 px per side), or its `data` isn't an object or lacks a required dynamic property.
- `429` Rate limited (`Retry-After`).
- `503` Region degraded (`Retry-After`).

## POST /v1/pdf/from-template

**PDF from a template**

Convenience wrapper over `POST /v1/renders` with flat, no-code friendly fields.

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `Idempotency-Key` | header | string | Replays the stored response for 24 h; a different body with the same key → 422. |

Request body (`application/json`): object

| Field | Type | Description |
|---|---|---|
| `data` | object or null |  |
| `data_url` | string or null |  |
| `delivery` | DeliverySpec or "url" \| "binary" \| "base64" \| "none" |  |
| `einvoice` | EInvoiceSpec or null |  |
| `engine` | string or null |  |
| `expires_in` | integer |  |
| `filename` | string | Jinja-enabled filename. |
| `hosted` | boolean or null | Keep the hosted copy; `false` deletes it once the storage uploads and email sends have succeeded. Absent or `null`: `false` when every storage destination of the render has `keep_hosted_copy: false`, else `true`. Zero-retention renders never keep one. |
| `metadata` | object or null |  |
| `mode` | "sync" \| "async" |  |
| `overrides` | array of object or null |  |
| `pdf` | PdfOptions or null |  |
| `priority` | "normal" \| "low" |  |
| `reference` | string or null |  |
| `retention_days` | integer or null |  |
| `template_id` (required) | string |  |
| `test` | boolean or null |  |
| `timeout_behavior` | "continue" \| "cancel" |  |
| `version` | "live" \| "draft" or integer |  |
| `webhook` | WebhookSpec or null |  |
| `webhook_url` | string |  |

Responses:

- `200` Sync render finished (`status=succeeded`). With `delivery.type=binary` the body is the file.
- `202` Async render accepted, or the sync timeout was exceeded (`Location: /v1/renders/{id}`).
- `400` Malformed or invalid request (`errors[]` with JSON pointers), e.g. an unknown, switched-off or another template's rule at `/delivery/email/<i>/rule_id`.
- `401` Missing, invalid, expired or revoked credentials.
- `402` Render limit reached (`render_limit_reached`) or feature not on plan (`plan_feature_unavailable`, e.g. a `delivery.email` list below the Growth plan).
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified; or `region_not_enabled`: the workspace's enabled regions leave out this region.
- `404` Template or version not found.
- `408` Sync timeout exceeded and the render was canceled (`timeout_behavior=cancel`).
- `409` A request with this Idempotency-Key is still in progress.
- `410` Template deleted.
- `413` Payload or output too large.
- `422` Render failed (`render_id`, `render_error`; `render_failed` when the worker reports no more specific code), schema mismatch, URL not allowed or idempotency key reused. `einvoice_invalid`, `einvoice_validation_failed`, `pdfa_conversion_failed` and `pdfua_validation_failed` also carry the conformance report as `result`. `email_needs_hosted_file`: `delivery.email` lists rules, but a zero-retention render keeps no file to email. `render_canceled`: the sync render was canceled, or its job expired, before it finished. `scene_invalid`, `data_invalid`: a canvas template's scene can't be rendered (e.g. an output over 16,384 px per side), or its `data` isn't an object or lacks a required dynamic property.
- `429` Rate limited (`Retry-After`).
- `503` Region degraded (`Retry-After`).

## POST /v1/pdf/from-url

**PDF from a URL**

Convenience wrapper over `POST /v1/renders` with flat, no-code friendly fields.

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `Idempotency-Key` | header | string | Replays the stored response for 24 h; a different body with the same key → 422. |

Request body (`application/json`): object

| Field | Type | Description |
|---|---|---|
| `data` | object or null |  |
| `data_url` | string or null |  |
| `delivery` | DeliverySpec or "url" \| "binary" \| "base64" \| "none" |  |
| `einvoice` | EInvoiceSpec or null |  |
| `engine` | string or null |  |
| `expires_in` | integer |  |
| `filename` | string | Jinja-enabled filename. |
| `hosted` | boolean or null | Keep the hosted copy; `false` deletes it once the storage uploads and email sends have succeeded. Absent or `null`: `false` when every storage destination of the render has `keep_hosted_copy: false`, else `true`. Zero-retention renders never keep one. |
| `http` | HttpOptions or null |  |
| `metadata` | object or null |  |
| `mode` | "sync" \| "async" |  |
| `pdf` | PdfOptions or null |  |
| `priority` | "normal" \| "low" |  |
| `reference` | string or null |  |
| `retention_days` | integer or null |  |
| `test` | boolean or null |  |
| `timeout_behavior` | "continue" \| "cancel" |  |
| `url` (required) | string |  |
| `webhook` | WebhookSpec or null |  |
| `webhook_url` | string |  |

Responses:

- `200` Sync render finished (`status=succeeded`). With `delivery.type=binary` the body is the file.
- `202` Async render accepted, or the sync timeout was exceeded (`Location: /v1/renders/{id}`).
- `400` Malformed or invalid request (`errors[]` with JSON pointers), e.g. an unknown, switched-off or another template's rule at `/delivery/email/<i>/rule_id`.
- `401` Missing, invalid, expired or revoked credentials.
- `402` Render limit reached (`render_limit_reached`) or feature not on plan (`plan_feature_unavailable`, e.g. a `delivery.email` list below the Growth plan).
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified; or `region_not_enabled`: the workspace's enabled regions leave out this region.
- `404` Template or version not found.
- `408` Sync timeout exceeded and the render was canceled (`timeout_behavior=cancel`).
- `409` A request with this Idempotency-Key is still in progress.
- `410` Template deleted.
- `413` Payload or output too large.
- `422` Render failed (`render_id`, `render_error`; `render_failed` when the worker reports no more specific code), schema mismatch, URL not allowed or idempotency key reused. `einvoice_invalid`, `einvoice_validation_failed`, `pdfa_conversion_failed` and `pdfua_validation_failed` also carry the conformance report as `result`. `email_needs_hosted_file`: `delivery.email` lists rules, but a zero-retention render keeps no file to email. `render_canceled`: the sync render was canceled, or its job expired, before it finished. `scene_invalid`, `data_invalid`: a canvas template's scene can't be rendered (e.g. an output over 16,384 px per side), or its `data` isn't an object or lacks a required dynamic property.
- `429` Rate limited (`Retry-After`).
- `503` Region degraded (`Retry-After`).

## POST /v1/previews/html

**Live HTML preview (dashboard)**

Request body (`application/json`): HtmlPreviewRequest

| Field | Type | Description |
|---|---|---|
| `content` | PreviewContent or null |  |
| `data` | object or null |  |
| `head` | string or null |  |
| `html` | string or null |  |
| `markdown` | string or null |  |
| `partials` | object or null |  |
| `strict_undefined` | boolean or null |  |
| `template_id` | string or null |  |
| `template_type` | "pdf_code" \| "pdf_markdown" |  |

Responses:

- `200` 
- `401` Missing, invalid, expired or revoked credentials.
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified.
- `422` Template syntax/runtime error or fuel exhausted.
- `429` Rate limited (`Retry-After`).
- `503` Preview engine unavailable.

## POST /v1/previews/image

**Image preview (dashboard, watermarked, free)**

Request body (`application/json`): RenderPreviewRequest

| Field | Type | Description |
|---|---|---|
| `content` | PreviewContent or null |  |
| `data` | object or null |  |
| `engine` | string or null |  |
| `output` | OutputSpec or null |  |
| `template_id` | string or null |  |
| `template_type` | "pdf_code" \| "pdf_markdown" \| "image_canvas" or null |  |
| `version` | "live" \| "draft" or integer or null |  |

Responses:

- `200` 
- `202` 
- `401` Missing, invalid, expired or revoked credentials.
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified; or `region_not_enabled`: the workspace's enabled regions leave out this region.
- `422` Render failed.
- `429` Rate limited (`Retry-After`).

## POST /v1/previews/pdf

**PDF preview (dashboard, watermarked, free)**

With `pdf.pdfua` (or `pdf.tagged`) the preview is a tagged PDF with the PDF/UA-1 report in `result.conformance.pdfua`, but it never fails on the report, so the editor can show what to fix.

Request body (`application/json`): RenderPreviewRequest

| Field | Type | Description |
|---|---|---|
| `content` | PreviewContent or null |  |
| `data` | object or null |  |
| `engine` | string or null |  |
| `output` | OutputSpec or null |  |
| `template_id` | string or null |  |
| `template_type` | "pdf_code" \| "pdf_markdown" \| "image_canvas" or null |  |
| `version` | "live" \| "draft" or integer or null |  |

Responses:

- `200` 
- `202` 
- `401` Missing, invalid, expired or revoked credentials.
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified; or `region_not_enabled`: the workspace's enabled regions leave out this region.
- `422` Render failed.
- `429` Rate limited (`Retry-After`).

## GET /v1/renders

**List renders**

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `batch_id` | query | string |  |
| `created[gte]` | query | string |  |
| `created[lte]` | query | string |  |
| `cursor` | query | string |  |
| `input_type` | query | "einvoice" \| "html" \| "markdown" \| "pdf_tool" \| "template" \| "url" |  |
| `limit` | query | integer | 1 to 100 (default 50). |
| `metadata[key]` | query | string | Filter by a metadata key/value. |
| `output_format` | query | "html" \| "jpeg" \| "pdf" \| "png" \| "webp" \| "xml" \| "zip" |  |
| `reference` | query | string |  |
| `source` | query | string |  |
| `status` | query | "queued" \| "processing" \| "succeeded" \| "failed" \| "canceled" \| "expired" or array of "queued" \| "processing" \| "succeeded" \| "failed" \| "canceled" \| "expired" | Repeat it (or comma-separate the values) to match any of several statuses. |
| `template_id` | query | string |  |
| `test` | query | boolean |  |

Responses:

- `200` 
- `401` Missing, invalid, expired or revoked credentials.
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified.
- `429` Rate limited (`Retry-After`).

## POST /v1/renders

**Create a render**

Unified endpoint for template, HTML, URL and Markdown inputs → PDF, images or HTML. `output.pdf.pdfa` makes the PDF PDF/A-2b or -3b; `output.pdf.pdfua` makes it PDF/UA-1 (accessible, Growth plan and up); `output.einvoice` embeds a Factur-X / ZUGFeRD / XRechnung invoice (a hybrid PDF/A-3b, no extra billed render). All are validated (veraPDF, KoSIT); the report is the render's `result`.

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `Idempotency-Key` | header | string | Replays the stored response for 24 h; a different body with the same key → 422. |

Request body (`application/json`): RenderRequest

| Field | Type | Description |
|---|---|---|
| `data` | object or null |  |
| `data_url` | string or null |  |
| `delivery` | DeliverySpec or null |  |
| `engine` | string or null |  |
| `input` (required) | TemplateInputSpec or HtmlInputSpec or UrlInputSpec or MarkdownInputSpec |  |
| `metadata` | object or null |  |
| `mode` | "sync" \| "async" |  |
| `output` | OutputSpec or null |  |
| `overrides` | array of object or null |  |
| `priority` | "normal" \| "low" |  |
| `reference` | string or null |  |
| `test` | boolean or null |  |
| `timeout_behavior` | "continue" \| "cancel" |  |
| `webhook` | WebhookSpec or null |  |

Responses:

- `200` Sync render finished (`status=succeeded`). With `delivery.type=binary` the body is the file.
- `202` Async render accepted, or the sync timeout was exceeded (`Location: /v1/renders/{id}`).
- `400` Malformed or invalid request (`errors[]` with JSON pointers), e.g. an unknown, switched-off or another template's rule at `/delivery/email/<i>/rule_id`.
- `401` Missing, invalid, expired or revoked credentials.
- `402` Render limit reached (`render_limit_reached`) or feature not on plan (`plan_feature_unavailable`, e.g. a `delivery.email` list below the Growth plan).
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified; or `region_not_enabled`: the workspace's enabled regions leave out this region.
- `404` Template or version not found.
- `408` Sync timeout exceeded and the render was canceled (`timeout_behavior=cancel`).
- `409` A request with this Idempotency-Key is still in progress.
- `410` Template deleted.
- `413` Payload or output too large.
- `422` Render failed (`render_id`, `render_error`; `render_failed` when the worker reports no more specific code), schema mismatch, URL not allowed or idempotency key reused. `einvoice_invalid`, `einvoice_validation_failed`, `pdfa_conversion_failed` and `pdfua_validation_failed` also carry the conformance report as `result`. `email_needs_hosted_file`: `delivery.email` lists rules, but a zero-retention render keeps no file to email. `render_canceled`: the sync render was canceled, or its job expired, before it finished. `scene_invalid`, `data_invalid`: a canvas template's scene can't be rendered (e.g. an output over 16,384 px per side), or its `data` isn't an object or lacks a required dynamic property.
- `429` Rate limited (`Retry-After`).
- `503` Region degraded (`Retry-After`).

## GET /v1/renders/{render_id}

**Retrieve a render**

Fresh signed file URLs are issued on every retrieve while files are hosted.

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `render_id` (required) | path | string |  |

Responses:

- `200` 
- `401` Missing, invalid, expired or revoked credentials.
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified.
- `404` Render not found.
- `429` Rate limited (`Retry-After`).

## POST /v1/renders/{render_id}/cancel

**Cancel a render**

Best effort: queued renders are canceled immediately; processing renders finish.

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `Idempotency-Key` | header | string | Replays the stored response for 24 h; a different body with the same key → 422. |
| `render_id` (required) | path | string |  |

Responses:

- `200` 
- `401` Missing, invalid, expired or revoked credentials.
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified.
- `404` Render not found.
- `409` `conflict`: the render already finished.
- `429` Rate limited (`Retry-After`).

## DELETE /v1/renders/{render_id}/files

**Purge stored files**

Deletes hosted copies now; the render becomes `expired`. BYOS copies are untouched. For a zero-retention render it deletes the transient copies its storage uploads read (uploads still pending then fail), and the status stays.

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `render_id` (required) | path | string |  |

Responses:

- `200` 
- `401` Missing, invalid, expired or revoked credentials.
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified.
- `404` Render not found.
- `409` `conflict`: the render is still running; cancel it first.
- `429` Rate limited (`Retry-After`).

## GET /v1/renders/{render_id}/files/{file_id}

**Download a render file**

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `download` | query | boolean | Attachment disposition. |
| `file_id` (required) | path | string |  |
| `inline` | query | boolean | Stream through the API instead of 302. |
| `render_id` (required) | path | string |  |

Responses:

- `200` File (`?inline=true`).
- `302` Redirect to a signed URL.
- `401` Missing, invalid, expired or revoked credentials.
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified.
- `404` Render or file not found. Every file of a zero-retention render answers 404: its copies for storage destinations can't be opened.
- `410` File expired or deleted.
- `429` Rate limited (`Retry-After`).

## GET /v1/renders/{render_id}/logs

**Request/response log**

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `render_id` (required) | path | string |  |

Responses:

- `200` 
- `401` Missing, invalid, expired or revoked credentials.
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified.
- `404` Logging disabled or log expired.
- `429` Rate limited (`Retry-After`).

## GET /v1/signed-links

**List signed links**

Image links that render a template straight from a URL. Secrets are never returned by a read.

Responses:

- `200` Every object in the workspace (a single page today).
- `401` Missing, invalid, expired or revoked credentials.
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified.
- `429` Rate limited (`Retry-After`).
- `503` The control plane is unreachable (`Retry-After`).

## POST /v1/signed-links

**Create a signed link**

Returns `base_url` and the HMAC `secret` **once**; SDKs sign URLs locally with it (docs/05 §7). Use `access: "open"` for tools that cannot compute an HMAC.

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `Idempotency-Key` | header | string | Replays the stored response for 24 h; a different body with the same key → 422. |

Request body (`application/json`, required): SignedLinkWrite

| Field | Type | Description |
|---|---|---|
| `access` | "signed" \| "open" | `open` accepts unsigned requests within `allowed_params` and `param_limits`. * `signed` - signed * `open` - open |
| `allowed_params` | array of string | Dynamic properties callers may set, e.g. `title.text`. |
| `allowed_referrers` | array of string |  |
| `cache_ttl_seconds` | integer \| null |  |
| `defaults` | object |  |
| `enabled` | boolean |  |
| `expires_at` | string \| null |  |
| `fallback_image_asset_id` | string \| null | Ready image asset (`ast_…`) returned instead of 402 `render_limit_reached`; null clears. |
| `formats` (required) | array of "png" \| "jpeg" \| "webp" \| "pdf" |  |
| `name` (required) | string |  |
| `param_limits` | object |  |
| `quota_total` | integer \| null |  |
| `rate_limit_per_ip_per_hour` | integer \| null |  |
| `template_id` (required) | string |  |

Responses:

- `201` Created; `secret` is shown once.
- `400` Invalid request body (`errors[]` with JSON pointers).
- `401` Missing, invalid, expired or revoked credentials.
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified.
- `404` Template not found.
- `429` Rate limited (`Retry-After`).
- `503` The control plane is unreachable (`Retry-After`).

## GET /v1/signed-links/{link_id}

**Retrieve a signed link**

Common base: API key or OAuth only, never a dashboard region token.

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `link_id` (required) | path | string | Signed link id (`lnk_…`). |

Responses:

- `200` 
- `401` Missing, invalid, expired or revoked credentials.
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified.
- `404` Signed link not found.
- `429` Rate limited (`Retry-After`).
- `503` The control plane is unreachable (`Retry-After`).

## PATCH /v1/signed-links/{link_id}

**Update a signed link**

Partial update. `template_id` is immutable and the secret is unchanged.

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `link_id` (required) | path | string | Signed link id (`lnk_…`). |

Request body (`application/json`): PatchedSignedLinkWrite

| Field | Type | Description |
|---|---|---|
| `access` | "signed" \| "open" | `open` accepts unsigned requests within `allowed_params` and `param_limits`. * `signed` - signed * `open` - open |
| `allowed_params` | array of string | Dynamic properties callers may set, e.g. `title.text`. |
| `allowed_referrers` | array of string |  |
| `cache_ttl_seconds` | integer \| null |  |
| `defaults` | object |  |
| `enabled` | boolean |  |
| `expires_at` | string \| null |  |
| `fallback_image_asset_id` | string \| null | Ready image asset (`ast_…`) returned instead of 402 `render_limit_reached`; null clears. |
| `formats` | array of "png" \| "jpeg" \| "webp" \| "pdf" |  |
| `name` | string |  |
| `param_limits` | object |  |
| `quota_total` | integer \| null |  |
| `rate_limit_per_ip_per_hour` | integer \| null |  |
| `template_id` | string |  |

Responses:

- `200` 
- `400` Invalid request body (`errors[]` with JSON pointers).
- `401` Missing, invalid, expired or revoked credentials.
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified.
- `404` Signed link not found.
- `429` Rate limited (`Retry-After`).
- `503` The control plane is unreachable (`Retry-After`).

## DELETE /v1/signed-links/{link_id}

**Delete a signed link**

Existing URLs stop working immediately (cached CDN copies expire on their own).

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `link_id` (required) | path | string | Signed link id (`lnk_…`). |

Responses:

- `204` Deleted.
- `401` Missing, invalid, expired or revoked credentials.
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified.
- `404` Signed link not found.
- `429` Rate limited (`Retry-After`).
- `503` The control plane is unreachable (`Retry-After`).

## POST /v1/signed-links/{link_id}/sign

**Sign a link URL**

Convenience for languages without an SDK: signs `params` with the link secret. Signing locally avoids the round trip (docs/05 §7).

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `Idempotency-Key` | header | string | Replays the stored response for 24 h; a different body with the same key → 422. |
| `link_id` (required) | path | string | Signed link id (`lnk_…`). |

Request body (`application/json`, required): SignUrl

| Field | Type | Description |
|---|---|---|
| `expires_at` | integer | Unix seconds; omit for a link that does not expire. |
| `format` (required) | "png" \| "jpeg" \| "webp" \| "pdf" | * `png` - png * `jpeg` - jpeg * `webp` - webp * `pdf` - pdf |
| `name` | string | File name in the URL (cosmetic, but signed). |
| `params` | object |  |

Responses:

- `200` 
- `400` Invalid request body (`errors[]` with JSON pointers).
- `401` Missing, invalid, expired or revoked credentials.
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified.
- `404` Signed link not found.
- `429` Rate limited (`Retry-After`).
- `503` The control plane is unreachable (`Retry-After`).

## GET /v1/storage-destinations

**List storage destinations**

Your own buckets that renders can be uploaded to. Credentials are never returned.

Responses:

- `200` Every object in the workspace (a single page today).
- `401` Missing, invalid, expired or revoked credentials.
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified.
- `429` Rate limited (`Retry-After`).
- `503` The control plane is unreachable (`Retry-After`).

## POST /v1/storage-destinations

**Create a storage destination**

`credentials` are encrypted at rest and never returned; reads answer `{"configured": true}`. Reference the destination from `delivery.storage[]` on a render. On Amazon S3, `config.storage_class` sets the class of every uploaded object and of the connection test's probe: `STANDARD`, `STANDARD_IA`, `ONEZONE_IA`, `INTELLIGENT_TIERING` or `GLACIER_IR`; leave it out for the bucket's default. Archive classes (`GLACIER`, `DEEP_ARCHIVE`) and classes on other providers answer `400`. `config.force_path_style` (S3 and S3-compatible) puts the bucket in the path (`true`, `<endpoint>/<bucket>/<key>`) or in the host name (`false`, `<bucket>.<endpoint host>`); left out, it is `false` for Amazon S3 and `true` for S3-compatible services. Bucket names with dots, underscores or capitals and IP-address endpoints always use the path. Uploaded objects get the file's content type.

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `Idempotency-Key` | header | string | Replays the stored response for 24 h; a different body with the same key → 422. |

Request body (`application/json`, required): StorageDestinationWrite

| Field | Type | Description |
|---|---|---|
| `config` (required) | object |  |
| `credentials` | object | Never returned again; send it only when it changes. |
| `is_default` | boolean |  |
| `keep_hosted_copy` | boolean |  |
| `name` (required) | string |  |
| `path_template` | string |  |
| `provider` (required) | "s3" \| "s3_compatible" \| "azure_blob" \| "gcs" \| "sftp" | * `s3` - Amazon S3 * `s3_compatible` - S3-compatible * `azure_blob` - Azure Blob Storage * `gcs` - Google Cloud Storage * `sftp` - SFTP |

Responses:

- `201` 
- `400` Invalid request body (`errors[]` with JSON pointers).
- `401` Missing, invalid, expired or revoked credentials.
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified.
- `429` Rate limited (`Retry-After`).
- `503` The control plane is unreachable (`Retry-After`).

## GET /v1/storage-destinations/{destination_id}

**Retrieve a storage destination**

Common base: API key or OAuth only, never a dashboard region token.

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `destination_id` (required) | path | string | Storage destination id (`dst_…`). |

Responses:

- `200` 
- `401` Missing, invalid, expired or revoked credentials.
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified.
- `404` Storage destination not found.
- `429` Rate limited (`Retry-After`).
- `503` The control plane is unreachable (`Retry-After`).

## PATCH /v1/storage-destinations/{destination_id}

**Update a storage destination**

Partial update. `provider` is immutable — create a new destination to change it.

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `destination_id` (required) | path | string | Storage destination id (`dst_…`). |

Request body (`application/json`): PatchedStorageDestinationWrite

| Field | Type | Description |
|---|---|---|
| `config` | object |  |
| `credentials` | object | Never returned again; send it only when it changes. |
| `is_default` | boolean |  |
| `keep_hosted_copy` | boolean |  |
| `name` | string |  |
| `path_template` | string |  |
| `provider` | "s3" \| "s3_compatible" \| "azure_blob" \| "gcs" \| "sftp" | * `s3` - Amazon S3 * `s3_compatible` - S3-compatible * `azure_blob` - Azure Blob Storage * `gcs` - Google Cloud Storage * `sftp` - SFTP |

Responses:

- `200` 
- `400` Invalid request body (`errors[]` with JSON pointers).
- `401` Missing, invalid, expired or revoked credentials.
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified.
- `404` Storage destination not found.
- `429` Rate limited (`Retry-After`).
- `503` The control plane is unreachable (`Retry-After`).

## DELETE /v1/storage-destinations/{destination_id}

**Delete a storage destination**

Files already uploaded to your bucket are untouched. Uploads that haven't succeeded yet stop: each fails at its next attempt, without connecting, with `storage.upload_failed`.

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `destination_id` (required) | path | string | Storage destination id (`dst_…`). |

Responses:

- `204` Deleted.
- `401` Missing, invalid, expired or revoked credentials.
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified.
- `404` Storage destination not found.
- `429` Rate limited (`Retry-After`).
- `503` The control plane is unreachable (`Retry-After`).

## POST /v1/storage-destinations/{destination_id}/test

**Test a storage destination**

Writes, reads back and deletes a small probe object from the region that owns the workspace. `200` with `ok: false` means the credentials or the bucket policy are wrong — `steps[]` says which check failed. The result is also stored as `last_test`.

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `Idempotency-Key` | header | string | Replays the stored response for 24 h; a different body with the same key → 422. |
| `destination_id` (required) | path | string | Storage destination id (`dst_…`). |

Responses:

- `200` The probe ran; check `ok`.
- `400` Invalid request body (`errors[]` with JSON pointers).
- `401` Missing, invalid, expired or revoked credentials.
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified.
- `404` Storage destination not found.
- `429` Rate limited (`Retry-After`).
- `503` The control plane is unreachable (`Retry-After`).

## GET /v1/templates

**List templates**

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `cursor` | query | string |  |
| `limit` | query | integer |  |
| `q` | query | string |  |
| `type` | query | string |  |

Responses:

- `200` 
- `401` Missing, invalid, expired or revoked credentials.
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified.
- `429` Rate limited (`Retry-After`).

## POST /v1/templates

**Create a template**

Creates a `pdf_code`, `pdf_markdown` or `image_canvas` template. The content fields depend on `type` (any type may send one `content` object instead); they land in the draft — publish it to get a live version (docs/05 §6).

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `Idempotency-Key` | header | string | Replays the stored response for 24 h; a different body with the same key → 422. |

Request body (`application/json`): TemplateCreateRequest

Responses:

- `201` 
- `400` Invalid request body (`errors[]` with JSON pointers).
- `401` Missing, invalid, expired or revoked credentials.
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified; or `template_not_allowed_for_key`: the key is restricted to specific templates.
- `429` Rate limited (`Retry-After`).
- `503` The control plane is unreachable (`Retry-After`).

## GET /v1/templates/{template_id}

**Retrieve a template**

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `template_id` (required) | path | string |  |

Responses:

- `200` 
- `401` Missing, invalid, expired or revoked credentials.
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified.
- `404` Template not found.
- `410` Template deleted.
- `429` Rate limited (`Retry-After`).

## DELETE /v1/templates/{template_id}

**Delete a template**

Soft delete: the template moves to the trash and is purged after 30 days. Renders that reference it keep working until then; new renders answer `410 template_deleted`.

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `template_id` (required) | path | string | Template id (`tpl_…`). |

Responses:

- `200` 
- `400` Invalid request body (`errors[]` with JSON pointers).
- `401` Missing, invalid, expired or revoked credentials.
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified.
- `404` Template not found.
- `410` Template already deleted.
- `423` The template is locked (`template_locked`); Owners and Admins unlock it in the dashboard.
- `429` Rate limited (`Retry-After`).
- `503` The control plane is unreachable (`Retry-After`).

## PUT /v1/templates/{template_id}/draft

**Replace the draft sources**

Replaces the draft with the content fields of the template's type (see `POST /v1/templates`) or one `content` object. Send `If-Match: <revision>` (the `revision` of the draft you edited) to detect concurrent edits; a stale revision answers `409 draft_revision_conflict`. The response carries the new revision in `ETag`. `image_canvas` drafts are compiled when published — and by `version: draft` renders, which answer `422` with JSON pointers under `/template/draft` when the draft does not compile.

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `If-Match` | header | string | Draft revision you based the edit on. |
| `template_id` (required) | path | string | Template id (`tpl_…`). |

Request body (`application/json`): TemplateDraftWriteRequest

Responses:

- `200` Saved. `ETag` holds the new revision.
- `400` Invalid request body (`errors[]` with JSON pointers).
- `401` Missing, invalid, expired or revoked credentials.
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified.
- `404` Template not found.
- `409` The draft changed since the revision in `If-Match` (`draft_revision_conflict`).
- `410` Template deleted.
- `423` The template is locked (`template_locked`); Owners and Admins unlock it in the dashboard.
- `429` Rate limited (`Retry-After`).
- `503` The control plane is unreachable (`Retry-After`).

## GET /v1/templates/{template_id}/email-rules

**List a template's email rules**

0 to 5 rules in `position` order. Rules are live configuration, not versioned with the template: an edit applies to renders submitted afterwards.

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `template_id` (required) | path | string | Template id (`tpl_…`). |

Responses:

- `200` 
- `401` Missing, invalid, expired or revoked credentials.
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified.
- `404` Template not found.
- `429` Rate limited (`Retry-After`).
- `503` The control plane is unreachable (`Retry-After`).

## POST /v1/templates/{template_id}/email-rules

**Create an email rule**

`from_name`, `reply_to`, `to`, `cc`, `bcc`, `subject`, `html` and `text` use the template language with the document's context (data keys, `data`, `render` including `render.invoice`) plus `files` and `download_links`; `condition` is an expression. Every field must compile, else `422 email_template_error` with `errors[].path`, line and column. A `from_email` the provider hasn't verified is saved with the warning `email_sender_unverified`.

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `Idempotency-Key` | header | string | Replays the stored response for 24 h; a different body with the same key → 422. |
| `template_id` (required) | path | string | Template id (`tpl_…`). |

Request body (`application/json`, required): EmailRuleWrite

| Field | Type | Description |
|---|---|---|
| `attach_formats` | array of "pdf" \| "png" \| "jpeg" \| "webp" \| "html" \| "xml" \| "zip" | Only files of these formats; empty = every file. |
| `attachments` | "attach_or_link" \| "attach" \| "link" \| "none" | * `attach_or_link` - Attach Or Link * `attach` - Attach * `link` - Link * `none` - None |
| `bcc` | string \| null |  |
| `cc` | string \| null |  |
| `condition` | string \| null | An expression; the rule sends when it is true. Empty = always. |
| `connection_id` (required) | string | `emc_…` of this workspace. |
| `enabled` | boolean |  |
| `from_email` | string \| null | Literal; empty = the connection's default sender. One the provider hasn't verified is saved with the warning `email_sender_unverified`. |
| `from_name` | string \| null | Template source; one line after rendering. |
| `html` | string \| null | Template source, values HTML-escaped (≤ 256 KiB). |
| `name` (required) | string |  |
| `position` | integer | 0 to 4; the others move. Default: the end. |
| `provider_template` | EmailProviderTemplate or null |  |
| `reply_to` | string \| null | Template source; one line after rendering. |
| `subject` | string \| null | Template source; required unless a provider template gives it. |
| `text` | string \| null | Template source; empty = derived from `html`. |
| `to` | string \| null | Template source rendering to addresses separated by `,` `;` or line breaks. |

Responses:

- `201` 
- `400` Invalid request body (`errors[]` with JSON pointers).
- `401` Missing, invalid, expired or revoked credentials.
- `402` Email delivery needs the Growth plan or higher (`plan_feature_unavailable`).
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified; or `feature_not_enabled`: email delivery isn't enabled for this workspace yet.
- `404` Template or connection not found.
- `422` A field does not compile (`email_template_error`).
- `429` Rate limited (`Retry-After`).
- `503` The control plane is unreachable (`Retry-After`).

## POST /v1/templates/{template_id}/publish

**Publish the draft**

Validates the draft (compile, options, syntax, datasets) and publishes a new live version. `201` when the run finished, `202` when validation is still running, `422` when a step failed — the problem details then carry the run in `publish_run`.

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `Idempotency-Key` | header | string | Replays the stored response for 24 h; a different body with the same key → 422. |
| `template_id` (required) | path | string | Template id (`tpl_…`). |

Request body (`application/json`): TemplatePublish

| Field | Type | Description |
|---|---|---|
| `note` | string |  |

Responses:

- `201` Published.
- `202` Validation is still running.
- `400` Invalid request body (`errors[]` with JSON pointers).
- `401` Missing, invalid, expired or revoked credentials.
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified.
- `404` Template not found.
- `410` Template deleted.
- `422` Validation failed; `publish_run` holds the failing steps.
- `429` Rate limited (`Retry-After`).
- `503` The control plane is unreachable (`Retry-After`).

## POST /v1/templates/{template_id}/rollback

**Roll back to a version**

Points `live_version` at an existing version. Nothing is re-rendered and no new version is created.

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `Idempotency-Key` | header | string | Replays the stored response for 24 h; a different body with the same key → 422. |
| `template_id` (required) | path | string | Template id (`tpl_…`). |

Request body (`application/json`, required): TemplateRollback

| Field | Type | Description |
|---|---|---|
| `version` (required) | integer or string | Version number (`7`) or a `tv_…` id. |

Responses:

- `200` 
- `400` Invalid request body (`errors[]` with JSON pointers).
- `401` Missing, invalid, expired or revoked credentials.
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified.
- `404` Template or version not found.
- `410` Template deleted.
- `429` Rate limited (`Retry-After`).
- `503` The control plane is unreachable (`Retry-After`).

## GET /v1/templates/{template_id}/schema

**Data schema of a template**

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `template_id` (required) | path | string |  |

Responses:

- `200` 
- `401` Missing, invalid, expired or revoked credentials.
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified.
- `404` Template not found.
- `429` Rate limited (`Retry-After`).

## GET /v1/templates/{template_id}/versions

**List template versions**

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `template_id` (required) | path | string |  |

Responses:

- `200` 
- `401` Missing, invalid, expired or revoked credentials.
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified.
- `429` Rate limited (`Retry-After`).

## GET /v1/templates/{template_id}/versions/{number}

**Retrieve a template version**

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `number` (required) | path | string |  |
| `template_id` (required) | path | string |  |

Responses:

- `200` 
- `401` Missing, invalid, expired or revoked credentials.
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified.
- `404` Version not found.
- `429` Rate limited (`Retry-After`).

## POST /v1/uploads

**Upload a file (≤ 50 MB, kept 24 h)**

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `Idempotency-Key` | header | string | Replays the stored response for 24 h; a different body with the same key → 422. |

Request body (`multipart/form-data`): object

| Field | Type | Description |
|---|---|---|
| `file` (required) | string |  |

Responses:

- `201` 
- `401` Missing, invalid, expired or revoked credentials.
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified.
- `413` File too large.
- `415` Unsupported file type.
- `429` Rate limited (`Retry-After`).

## GET /v1/usage

**Usage time series**

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `from` | query | string | `YYYY-MM-DD` or ISO-8601; default: cycle start. |
| `granularity` | query | "day" \| "hour" |  |
| `group_by` | query | "key_id" \| "kind" \| "region" \| "source" \| "template_id" |  |
| `to` | query | string | `YYYY-MM-DD` (inclusive) or ISO-8601 (exclusive). |

Responses:

- `200` 
- `400` Invalid `from`/`to`, `granularity` or `group_by`.
- `401` Missing, invalid, expired or revoked credentials.
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified.
- `429` Rate limited (`Retry-After`).
- `503` Control plane unavailable.

## GET /v1/webhook-deliveries

**List webhook deliveries**

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `cursor` | query | string |  |
| `endpoint_id` | query | string |  |
| `limit` | query | integer |  |
| `render_id` | query | string |  |
| `status` | query | "exhausted" \| "failed" \| "pending" \| "succeeded" |  |

Responses:

- `200` 
- `401` Missing, invalid, expired or revoked credentials.
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified.
- `429` Rate limited (`Retry-After`).

## GET /v1/webhook-deliveries/{delivery_id}

**Retrieve a webhook delivery**

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `delivery_id` (required) | path | string |  |

Responses:

- `200` 
- `401` Missing, invalid, expired or revoked credentials.
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified.
- `404` Not found.
- `429` Rate limited (`Retry-After`).

## POST /v1/webhook-deliveries/{delivery_id}/replay

**Replay a webhook delivery**

Sends the stored event again as a new delivery (`replay_of` set, same event id). An endpoint's delivery goes to the endpoint's current URL. A delivery to a render's or batch's own `webhook.url` goes to that URL again, signed with the secret of the endpoint that signed the original (the workspace's default endpoint).

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `Idempotency-Key` | header | string | Replays the stored response for 24 h; a different body with the same key → 422. |
| `delivery_id` (required) | path | string |  |

Responses:

- `202` 
- `401` Missing, invalid, expired or revoked credentials.
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified.
- `404` Not found.
- `409` `conflict`: the endpoint is deleted, or disabled; for a delivery to a per-request `webhook.url`, the endpoint whose secret signs it. Test events can still be replayed to a disabled endpoint.
- `429` Rate limited (`Retry-After`).

## GET /v1/webhook-endpoints

**List webhook endpoints**

Endpoints registered for this workspace. Secrets are never returned by a read.

Responses:

- `200` Every object in the workspace (a single page today).
- `401` Missing, invalid, expired or revoked credentials.
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified.
- `429` Rate limited (`Retry-After`).
- `503` The control plane is unreachable (`Retry-After`).

## POST /v1/webhook-endpoints

**Create a webhook endpoint**

The response carries the signing `secret` **once** — store it now. Deliveries are signed per [Standard Webhooks](https://www.standardwebhooks.com/) (docs/05 §8.1).

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `Idempotency-Key` | header | string | Replays the stored response for 24 h; a different body with the same key → 422. |

Request body (`application/json`, required): WebhookEndpointWrite

| Field | Type | Description |
|---|---|---|
| `description` | string |  |
| `enabled` | boolean |  |
| `events` (required) | array of "render.succeeded" \| "render.failed" \| "render.expired" \| "batch.progress" \| "batch.completed" \| "storage.upload_failed" \| "email.sent" \| "email.send_failed" \| "email.delivered" \| "email.bounced" \| "email.complained" \| "usage.threshold_reached" \| "template.published" \| "api_key.expiring" |  |
| `headers` | object | Extra request headers. |
| `is_default` | boolean | Receive events from renders without an inline `webhook`. |
| `template_ids` | array of string | Only deliver events for these templates. |
| `url` (required) | string | Absolute https URL; no credentials. |

Responses:

- `201` Created; `secret` is shown once.
- `400` Invalid request body (`errors[]` with JSON pointers).
- `401` Missing, invalid, expired or revoked credentials.
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified.
- `429` Rate limited (`Retry-After`).
- `503` The control plane is unreachable (`Retry-After`).

## GET /v1/webhook-endpoints/{endpoint_id}

**Retrieve a webhook endpoint**

Common base: API key or OAuth only, never a dashboard region token.

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `endpoint_id` (required) | path | string | Webhook endpoint id (`whe_…`). |

Responses:

- `200` 
- `401` Missing, invalid, expired or revoked credentials.
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified.
- `404` Webhook endpoint not found.
- `429` Rate limited (`Retry-After`).
- `503` The control plane is unreachable (`Retry-After`).

## PATCH /v1/webhook-endpoints/{endpoint_id}

**Update a webhook endpoint**

Partial update; omitted fields keep their value. The secret is not changed — use `POST /v1/webhook-endpoints/{id}/roll-secret`.

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `endpoint_id` (required) | path | string | Webhook endpoint id (`whe_…`). |

Request body (`application/json`): PatchedWebhookEndpointWrite

| Field | Type | Description |
|---|---|---|
| `description` | string |  |
| `enabled` | boolean |  |
| `events` | array of "render.succeeded" \| "render.failed" \| "render.expired" \| "batch.progress" \| "batch.completed" \| "storage.upload_failed" \| "email.sent" \| "email.send_failed" \| "email.delivered" \| "email.bounced" \| "email.complained" \| "usage.threshold_reached" \| "template.published" \| "api_key.expiring" |  |
| `headers` | object | Extra request headers. |
| `is_default` | boolean | Receive events from renders without an inline `webhook`. |
| `template_ids` | array of string | Only deliver events for these templates. |
| `url` | string | Absolute https URL; no credentials. |

Responses:

- `200` 
- `400` Invalid request body (`errors[]` with JSON pointers).
- `401` Missing, invalid, expired or revoked credentials.
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified.
- `404` Webhook endpoint not found.
- `429` Rate limited (`Retry-After`).
- `503` The control plane is unreachable (`Retry-After`).

## DELETE /v1/webhook-endpoints/{endpoint_id}

**Delete a webhook endpoint**

Pending deliveries for this endpoint stop; the delivery log is kept.

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `endpoint_id` (required) | path | string | Webhook endpoint id (`whe_…`). |

Responses:

- `204` Deleted.
- `401` Missing, invalid, expired or revoked credentials.
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified.
- `404` Webhook endpoint not found.
- `429` Rate limited (`Retry-After`).
- `503` The control plane is unreachable (`Retry-After`).

## POST /v1/webhook-endpoints/{endpoint_id}/roll-secret

**Roll the signing secret**

Returns a new `secret` once. Deliveries carry both signatures until the previous secret expires, so receivers can be updated without downtime.

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `Idempotency-Key` | header | string | Replays the stored response for 24 h; a different body with the same key → 422. |
| `endpoint_id` (required) | path | string | Webhook endpoint id (`whe_…`). |

Request body (`application/json`): RollSecret

| Field | Type | Description |
|---|---|---|
| `expire_previous_in` | integer | Seconds the previous secret keeps signing deliveries (0 = immediately). |

Responses:

- `200` The new secret, shown once.
- `400` Invalid request body (`errors[]` with JSON pointers).
- `401` Missing, invalid, expired or revoked credentials.
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified.
- `404` Webhook endpoint not found.
- `429` Rate limited (`Retry-After`).
- `503` The control plane is unreachable (`Retry-After`).

## POST /v1/webhook-endpoints/{endpoint_id}/test

**Send a test event**

Queues a signed sample event (`test: true`) to the endpoint and returns the delivery id; follow it with `GET /v1/webhook-deliveries/{id}`. Every event type that fires today can be tested (all but `api_key.expiring`); the sample `data` has the real event's shape, including `data.event` for `email.delivered`, `email.bounced` and `email.complained`. Ids are made up and files have no URL.

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `Idempotency-Key` | header | string | Replays the stored response for 24 h; a different body with the same key → 422. |
| `endpoint_id` (required) | path | string | Webhook endpoint id (`whe_…`). |

Request body (`application/json`): TestEvent

| Field | Type | Description |
|---|---|---|
| `event` | "render.succeeded" \| "render.failed" \| "render.expired" \| "batch.progress" \| "batch.completed" \| "storage.upload_failed" \| "email.sent" \| "email.send_failed" \| "email.delivered" \| "email.bounced" \| "email.complained" \| "usage.threshold_reached" \| "template.published" | * `render.succeeded` - render.succeeded * `render.failed` - render.failed * `render.expired` - render.expired * `batch.progress` - batch.progress * `batch.completed` - batch.completed * `storage.upload_failed` - storage.upload_failed * `email.sent` - email.sent * `email.send_failed` - email.send_failed * `email.delivered` - email.delivered * `email.bounced` - email.bounced * `email.complained` - email.complained * `usage.threshold_reached` - usage.threshold_reached * `template.published` - template.published |

Responses:

- `202` Queued.
- `400` Invalid request body (`errors[]` with JSON pointers).
- `401` Missing, invalid, expired or revoked credentials.
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified.
- `404` Webhook endpoint not found, or not replicated to this region yet.
- `429` Rate limited (`Retry-After`).
- `503` The control plane is unreachable (`Retry-After`).
