# Batches

> Render one template for many items with POST /v1/batches, from JSON and CSV items and the combined ZIP or merged PDF to progress, cancel and retry, webhooks, billing and limits.



A batch renders one published template version for many items in a single request. Each item has its own data and becomes an ordinary render with its own files. The batch tracks the items together, reports progress and can package the results as one ZIP file or one merged PDF. Batches are included from the Starter plan.

Batches are asynchronous: `POST /v1/batches` answers `202 Accepted` at once, and the items render in the background. Follow them with `GET /v1/batches/{id}` or the `batch.progress` and `batch.completed` [webhook events](#webhooks).

## When to use a batch [#when-to-use-a-batch]

Use a batch when you render the same template for a list of records, such as certificates for every participant of a course, monthly statements or name badges:

* One request covers up to your plan's [item limit](#limits) instead of one request per document. The items start in turn, as many at a time as your plan's bulk concurrency allows.
* One batch object shows the progress of all items, and two webhook events replace one event per document.
* The results can be packaged as one ZIP file or one merged PDF.
* Failed items can be rendered again with one call.
* The items can come from a CSV file.

Send single renders with `POST /v1/renders` instead when you need the file in the response (sync mode, `binary` or `base64` delivery), when the documents use different templates, or for HTML, URL or Markdown input. A batch renders one stored template, and its items can't use `data_url`.

In the dashboard, **Batches** → **New batch** walks through the same steps: pick a published template, upload a CSV file or paste JSON, map the columns, check a few sample renders and start the batch.

## Create a batch [#create-a-batch]

```bash
curl https://api-eu.dynamicdocumentapi.com/v1/batches \
  -H "Authorization: Bearer $DYNAMIC_DOCUMENT_API_KEY" \
  -H "Idempotency-Key: certificates-course-2026-09" \
  -H "Content-Type: application/json" \
  -d '{
    "template_id": "tpl_01J9ZK3M7Q8V5W2X4Y6Z8A0B1C",
    "items": [
      { "data": { "name": "Ann Example", "course": "Safety 101" }, "reference": "cert_001", "filename": "ann-example.pdf" },
      { "data": { "name": "Bob Example", "course": "Safety 101" }, "reference": "cert_002" }
    ],
    "output": { "format": "pdf", "filename": "certificate-{{ data.name }}.pdf" },
    "combine": { "zip": true, "zip_filename": "certificates.zip" },
    "reference": "course_2026_09",
    "metadata": { "course": "safety-101" }
  }'
```

The API checks the whole batch before anything renders: the template, the options and every item. If something is invalid, no batch is created, and the error's `errors[]` lists each problem with a JSON pointer, such as `/items/1/data` or `/csv/row/5`. A valid batch is answered with `202 Accepted`, a `Location` header and the [batch object](#the-batch-object):

```http
HTTP/1.1 202 Accepted
Location: /v1/batches/bat_01J9ZN4T6V8X0Z2B4D6F8H0K2M
Content-Type: application/json

{"id": "bat_01J9ZN4T6V8X0Z2B4D6F8H0K2M", "object": "batch", "status": "queued", "total": 2, "succeeded": 0, "failed": 0, "pending": 2, "progress": 0.0}
```

The response body above is shortened.

### Request body [#request-body]

| Field           | Type              | Default                                         | Description                                                                                                                         |
| --------------- | ----------------- | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `template_id`   | string            | required                                        | The template to render (`tpl_…`)                                                                                                    |
| `version`       | string or integer | `"live"`                                        | `"live"` for the published version, or a version number such as `7`. Drafts can't be rendered in a batch, not even with a test key. |
| `items`         | array             | —                                               | The items as JSON. See [items](#items).                                                                                             |
| `csv_upload_id` | string            | —                                               | A CSV file uploaded with `POST /v1/uploads` (`upl_…`), instead of `items`. See [CSV input](#csv-input).                             |
| `csv_mapping`   | object            | every column                                    | Which CSV column fills which template variable                                                                                      |
| `output`        | object            | the template's options                          | Format, file name and format options, as in a [render request](/docs/renders). Applies to every item.                               |
| `delivery`      | object            | `url` delivery, or `none` without hosted copies | As in a render request, except `binary` and `base64`. Applies to every item.                                                        |
| `combine`       | object            | none                                            | A ZIP file and a merged PDF of the results. See [combined files](#combined-files).                                                  |
| `webhook`       | object            | none                                            | A webhook for this batch's events. See [webhooks](#webhooks).                                                                       |
| `reference`     | string            | none                                            | Your identifier for the batch, up to 255 characters. Usable as a list filter.                                                       |
| `metadata`      | object            | none                                            | Key-value pairs for the batch. They are copied to every item's render.                                                              |
| `engine`        | string            | template or workspace default                   | Engine channel for every item                                                                                                       |
| `test`          | boolean           | `false`                                         | Marks a test batch. See [test batches](#test-batches).                                                                              |

Send exactly one of `items` and `csv_upload_id`. Fields of a render request that aren't listed here, such as `mode`, `data_url` or `priority`, are rejected with `400 validation_error`. The [API reference](/docs/api-reference) has the full schemas.

### Items [#items]

Each entry in `items` is one document:

| Field       | Description                                                                                                                                                |
| ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `data`      | The item's data for the template, up to 1 MB. Keys become template variables, as in a render request.                                                      |
| `reference` | Your identifier for the item, up to 255 characters. It becomes the `reference` of the item's render.                                                       |
| `filename`  | File name for this item, up to 255 characters. It replaces `output.filename`, and the file gets the extension of the output format.                        |
| `metadata`  | Key-value pairs added to the batch's `metadata` on this item's render. The item's value wins when a key is in both. Batch and item together allow 50 keys. |

`output.filename` is evaluated for each item with that item's data, so `certificate-{{ data.name }}.pdf` gives every file its own name.

### CSV input [#csv-input]

A batch can also read its items from a CSV file. Upload the file with `POST /v1/uploads` first:

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

The response contains the upload's `id` (`upl_…`). Uploads can be up to 50 MB and are kept for 24 hours; the batch reads the file when you create it. Then pass the ID as `csv_upload_id`:

```bash
curl https://api-eu.dynamicdocumentapi.com/v1/batches \
  -H "Authorization: Bearer $DYNAMIC_DOCUMENT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "template_id": "tpl_01J9ZK3M7Q8V5W2X4Y6Z8A0B1C",
    "csv_upload_id": "upl_01J9ZM1X3F7R8K2C4V6B8N0P2Q",
    "csv_mapping": { "name": "Full Name", "course": "Course" },
    "combine": { "merged_pdf": true }
  }'
```

With this file, the first item gets the data `{"name": "Ann Example", "course": "Safety 101"}`, the reference `cert_001` and the file name `ann-example.pdf`:

```csv title="participants.csv"
Full Name,Course,_reference,_filename
Ann Example,Safety 101,cert_001,ann-example
Bob Example,Safety 101,cert_002,bob-example
```

* The file is UTF-8 (a byte order mark is ignored) and comma-separated as in RFC 4180, with the column names in the first row. Give it a `.csv` name when you upload it.
* `csv_mapping` maps template variables to column names, and only the mapped columns are used. A dotted variable such as `customer.name` becomes a nested object.
* Without `csv_mapping`, every column is used under its own name, and a dotted column name such as `customer.name` becomes a nested object.
* Values are always strings, and an empty cell is `""`. For numbers, booleans, lists (such as an e-invoice's `lines`) or per-item `metadata`, send JSON `items`.
* The optional columns `_reference` and `_filename` set each item's `reference` and `filename`, up to 255 characters. They aren't part of the data.
* Rows whose cells are all empty are skipped. Every other row needs as many fields as the header.
* Rows are numbered as in a spreadsheet: the header is row 1 and the first item is row 2. Errors point at rows as `/csv/row/<n>` and at the mapping as `/csv_mapping/<variable>`.

### Options for all items [#options-for-all-items]

`output` and `delivery` work as in a [render request](/docs/renders): the template's stored options apply, and what you send overrides them. Some rules are specific to batches:

* **Async only.** `delivery.type` is `url` or `none`. Without it, the items get `url` when they keep a hosted copy and `none` when they don't, see [Hosted copy](/docs/storage#hosted-copy). `binary` and `base64` need a sync render and are rejected.
* **Data schema.** If the template enforces a JSON Schema for its data, every item is checked when you create the batch. One mismatch rejects the whole batch with `422 data_schema_mismatch`, with pointers such as `/items/3/data/name` or `/csv/row/5/data/name`.
* **E-invoices.** A literal `output.einvoice.invoice` applies to every item. `invoice_path`, and a template's default e-invoice, are resolved in each item's own data. See [E-invoicing](/docs/e-invoicing).
* **Storage.** Each item's files are uploaded to the destinations in `delivery.storage`, or to your workspace's default destinations, like a single render's. In path templates, `render.reference` is the item's reference. See [Storage](/docs/storage).
* **Emails.** Email rules apply to every item. See [emails](#emails).

## Combined files [#combined-files]

Set `combine` to package the results once every item has finished:

| Field          | Default      | Description                                                                                                                                                      |
| -------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `zip`          | `false`      | One ZIP file with the file of every succeeded item                                                                                                               |
| `merged_pdf`   | `false`      | One PDF with the pages of every succeeded item. Needs `output.format: "pdf"`.                                                                                    |
| `zip_filename` | the batch ID | File name of the ZIP, up to 200 characters. The merged PDF gets the same name with `.pdf`. Without it, the files are named after the batch, such as `bat_….zip`. |

* The combined files are built when every item has a final status and at least one item succeeded. They use the hosted files of the succeeded items, in item order; failed and canceled items are left out.
* ZIP entries are named after the items' files. A file without a name of its own is named after its render ID and stored as `<index>-rnd_….<ext>`. Repeated names get ` (2)`, ` (3)` and so on before the extension, ignoring case.
* The merged PDF has one bookmark per item, titled with the item's file name. It counts against your plan's pages per PDF: a batch with more items than that limit can't request a merged PDF, and a merged PDF that comes out longer fails.
* The files appear in the batch's `files` with a signed `url` that lasts `delivery.expires_in` seconds. Every `GET /v1/batches/{id}` returns fresh URLs. The files are kept for the same retention period as the items.
* The combined files hold every succeeded item or none. If a combined file can't be built, the batch ends as `failed`, and its `error` says why. The item files stay available on their renders as long as their retention lasts.
* An item's files can be gone by the time the last item finishes: their retention ended while the batch waited for renders or ran for a long time (test batches keep files for a day), or `DELETE /v1/renders/{id}/files` purged them. Then neither the ZIP nor the merged PDF is built, and `error` is `{"code": "item_files_expired", "message": "…", "items": […]}`, where `items` lists the indexes of those items (at most 100).
* A canceled batch isn't combined.
* `combine` needs items that keep their hosted copy. It is rejected with `400 validation_error` at `/combine` for [zero-retention](#zero-retention) batches (code `zero_retention`) and for batches whose items keep no hosted copy (code `not_hosted`): `delivery.hosted: false`, or no `hosted` while every storage destination of the batch has `keep_hosted_copy: false`, see [Hosted copy](/docs/storage#hosted-copy). To combine with such destinations, send `delivery.hosted: true`. In the dashboard, tick **Keep hosted copies** in the **Options** step of **New batch**; until you do, the ZIP file and the merged PDF can't be selected.

## Track a batch [#track-a-batch]

`GET /v1/batches/{id}` returns the batch object with its counters, its progress and, once they are built, signed URLs for the ZIP file and the merged PDF. [Webhooks](#webhooks) tell you when a batch has finished, without polling.

### The batch object [#the-batch-object]

```json
{
  "id": "bat_01J9ZN4T6V8X0Z2B4D6F8H0K2M",
  "object": "batch",
  "status": "completed",
  "test": false,
  "template_id": "tpl_01J9ZK3M7Q8V5W2X4Y6Z8A0B1C",
  "template_version": 7,
  "total": 1200,
  "succeeded": 1198,
  "failed": 2,
  "canceled": 0,
  "pending": 0,
  "progress": 1.0,
  "paused": null,
  "error": null,
  "files": [
    {
      "kind": "zip",
      "file_id": "file_01J9ZP0A2C4E6G8J0K2N4Q6S8V",
      "filename": "certificates.zip",
      "content_type": "application/zip",
      "bytes": 41873920,
      "url": "https://files-eu.dynamicdocumentapi.com/f/…",
      "url_expires_at": "2026-09-17T16:21:08Z",
      "expires_at": "2026-10-17T15:21:08Z"
    }
  ],
  "combine": { "zip": true, "merged_pdf": false, "zip_filename": "certificates.zip" },
  "emails": { "sent": 0, "failed": 0, "unknown": 0, "pending": 0, "skipped": 0, "canceled": 0 },
  "reference": "course_2026_09",
  "metadata": { "course": "safety-101" },
  "created_at": "2026-09-17T15:06:33Z",
  "completed_at": "2026-09-17T15:21:08Z"
}
```

| Field                             | Description                                                                                                                                                                                                                              |
| --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`                              | Batch ID (`bat_…`)                                                                                                                                                                                                                       |
| `object`                          | Always `batch`                                                                                                                                                                                                                           |
| `status`                          | See [status values](#status-values)                                                                                                                                                                                                      |
| `test`                            | `true` for test batches                                                                                                                                                                                                                  |
| `template_id`, `template_version` | The template and the version number every item renders                                                                                                                                                                                   |
| `total`                           | Number of items                                                                                                                                                                                                                          |
| `succeeded`, `failed`, `canceled` | Items that ended with that status                                                                                                                                                                                                        |
| `pending`                         | Items without a final status yet, started or not                                                                                                                                                                                         |
| `progress`                        | Finished items divided by `total`, from `0` to `1`                                                                                                                                                                                       |
| `paused`                          | `null`, or the reason and start time while a live batch waits for renders. See [paused batches](#paused-batches).                                                                                                                        |
| `error`                           | `null`, unless the batch `failed` because a requested combined file couldn't be built: then that file's error (the ZIP's first), with `code` and `message`, and `items` for `item_files_expired`. See [combined files](#combined-files). |
| `files`                           | The ZIP file and the merged PDF, once built                                                                                                                                                                                              |
| `combine`                         | The `combine` options of the request                                                                                                                                                                                                     |
| `emails`                          | The items' emails by status. See [emails](#emails).                                                                                                                                                                                      |
| `reference`, `metadata`           | Values from your request                                                                                                                                                                                                                 |
| `created_at`, `completed_at`      | When the batch was created, and when it reached a final status                                                                                                                                                                           |

Each entry in `files` has:

| Field                               | Description                                                                              |
| ----------------------------------- | ---------------------------------------------------------------------------------------- |
| `kind`                              | `zip` or `merged_pdf`                                                                    |
| `file_id`                           | File ID (`file_…`)                                                                       |
| `filename`, `content_type`, `bytes` | File name, media type and size in bytes                                                  |
| `pages`                             | Number of pages, for the merged PDF                                                      |
| `url`, `url_expires_at`             | Signed download URL and when it stops working. Omitted when no hosted copy is available. |
| `expires_at`                        | When the file will be deleted, or `null` if it's kept                                    |

### Status values [#status-values]

| Status       | Meaning                                                                                                                                                               |
| ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `queued`     | Accepted; no item has started yet                                                                                                                                     |
| `processing` | Items are rendering, or the combined files are being built                                                                                                            |
| `completed`  | Every item has a final status, at least one succeeded, and every requested combined file was built. Items that didn't succeed are counted in `failed` and `canceled`. |
| `failed`     | No item succeeded, or a requested combined file couldn't be built; `error` then says why                                                                              |
| `canceled`   | Canceled with `POST /v1/batches/{id}/cancel`                                                                                                                          |

### Paused batches [#paused-batches]

If your workspace runs out of renders while a live batch is running, the batch pauses instead of failing:

* Items that haven't started wait. Items that are already rendering finish normally.
* The batch stays `processing`, `paused` shows `{"reason": "render_limit_reached", "since": "…"}`, and one `batch.progress` event is sent.
* About once a minute the batch checks again. It resumes on its own once renders are available, for example after you raise the monthly top-up limit or change your plan, or when the next billing cycle starts.
* Canceling works while a batch is paused.

A live batch created while no renders are left at all fails at once with `402 render_limit_reached`. See [Plans and limits](/docs/plans-and-limits) for top-ups and the monthly top-up limit.

### Items [#items-1]

`GET /v1/batches/{id}/items` lists the items in their original order, with cursor pagination. Filter by `status`, for example to see what `retry-failed` would render again:

```bash
curl -G https://api-eu.dynamicdocumentapi.com/v1/batches/bat_01J9ZN4T6V8X0Z2B4D6F8H0K2M/items \
  -H "Authorization: Bearer $DYNAMIC_DOCUMENT_API_KEY" \
  --data-urlencode "status=failed"
```

```json
{
  "object": "list",
  "data": [
    {
      "index": 17,
      "status": "failed",
      "render_id": "rnd_01J9ZN5B7D9F1H3K5M7P9R1T3V",
      "reference": "cert_018",
      "filename": null,
      "error": { "code": "template_runtime_error", "message": "'dict object' has no attribute 'course'" }
    }
  ],
  "has_more": false,
  "next_cursor": null
}
```

| Field                   | Description                                                                                                                                                                                   |
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `index`                 | The item's position, counting from 0: its place in `items`, or among the CSV's data rows                                                                                                      |
| `status`                | See the table below                                                                                                                                                                           |
| `render_id`             | The item's render (`rnd_…`) once it has started, otherwise `null`                                                                                                                             |
| `reference`, `filename` | The item's values                                                                                                                                                                             |
| `error`                 | Why the item failed: `code` and `message`, plus `line`, `column` and `excerpt` for template errors. While a retried item waits for its new render, `previous_render_id` names the failed one. |

| Item status | Meaning                                    |
| ----------- | ------------------------------------------ |
| `pending`   | Not started yet                            |
| `queued`    | Started: its render is queued or rendering |
| `succeeded` | Its render succeeded                       |
| `failed`    | Its render failed                          |
| `canceled`  | Canceled before it finished                |

### Renders of a batch [#renders-of-a-batch]

Every item is an ordinary render with `source: "batch"`, `mode: "async"` and the batch's ID in `batch_id`. Its `reference` is the item's, not the batch's, and its `metadata` combines the batch's and the item's.

Get an item's files with `GET /v1/renders/{id}`, using the `render_id` from the items list, or list every render of the batch with `GET /v1/renders?batch_id=bat_…`. That list also contains the renders that built the ZIP file and the merged PDF (`input_type: "pdf_tool"`) and the earlier attempts of retried items; add `input_type=template` to leave out the combined files. See [Renders](/docs/renders) for the render object and the other list filters.

## Cancel a batch [#cancel-a-batch]

`POST /v1/batches/{id}/cancel` stops a batch that hasn't finished:

```bash
curl -X POST https://api-eu.dynamicdocumentapi.com/v1/batches/bat_01J9ZN4T6V8X0Z2B4D6F8H0K2M/cancel \
  -H "Authorization: Bearer $DYNAMIC_DOCUMENT_API_KEY"
```

* Items that haven't started are canceled at once and never billed.
* Items that have started are canceled when a renderer picks them up. An item that is already rendering may still finish; it then counts as succeeded or failed, and is billed if it succeeds.
* The batch becomes `canceled` immediately, `batch.completed` is sent, and no combined files are built.

The response is the batch object. Canceling a batch that has already finished fails with `409 conflict`, and the problem details include its `batch_status`.

## Retry failed items [#retry-failed-items]

`POST /v1/batches/{id}/retry-failed` renders the failed items of a finished batch again:

```bash
curl -X POST https://api-eu.dynamicdocumentapi.com/v1/batches/bat_01J9ZN4T6V8X0Z2B4D6F8H0K2M/retry-failed \
  -H "Authorization: Bearer $DYNAMIC_DOCUMENT_API_KEY"
```

* Each failed item goes back to `pending` and renders as a new render, with the template version, data and options the batch was created with. Retrying helps when items failed for a temporary reason. To fix a template or data error, create a new batch with the failed items.
* The batch returns to `processing`, `completed_at` is cleared, `error` goes back to `null` and `files` is emptied. The ZIP file and the merged PDF are built again, and `batch.completed` is sent again, when the batch finishes.
* Retrying works for `completed`, `failed` and `canceled` batches with at least one failed item. Canceled items stay canceled.

The response is the batch object with `202 Accepted`. A batch that is still running or has no failed items answers `409 conflict`. A [zero-retention](#zero-retention) batch answers `409 zero_retention`, because its items no longer have their data. A batch with `combine` whose succeeded items' files are already gone answers `409 item_files_expired`, with their indexes in `items`, before anything is rendered again: the new combined files couldn't hold those items. Submit the failed items in a new batch instead.

## Endpoints [#endpoints]

| Endpoint                              | Scope          | Description                                                                |
| ------------------------------------- | -------------- | -------------------------------------------------------------------------- |
| `POST /v1/batches`                    | `render:write` | Create a batch                                                             |
| `GET /v1/batches`                     | `renders:read` | List batches, newest first. Filters: `status`, `template_id`, `reference`. |
| `GET /v1/batches/{id}`                | `renders:read` | Retrieve a batch, with fresh signed URLs for its combined files            |
| `GET /v1/batches/{id}/items`          | `renders:read` | List the items in order. Filter: `status`.                                 |
| `POST /v1/batches/{id}/cancel`        | `render:write` | Cancel a batch that hasn't finished                                        |
| `POST /v1/batches/{id}/retry-failed`  | `render:write` | Render the failed items again                                              |
| `POST /v1/batches/{id}/resend-emails` | `render:write` | Resend the items' failed emails. See [emails](#emails).                    |

Both lists use cursor pagination like `GET /v1/renders`: `limit` (up to 100, 50 by default) and `cursor`. Test keys only see test batches.

## Webhooks [#webhooks]

A batch sends two events instead of one event per item:

| Event             | Sent when                                                                                                                                                                      |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `batch.progress`  | Another 10 % of the items has finished (at 10 %, 20 % and so on up to 90 %, each at most once), or a live batch [paused](#paused-batches) because the render limit was reached |
| `batch.completed` | The batch reached a final status: `completed`, `failed` or `canceled`                                                                                                          |

The item that finishes a batch sends no progress event: the batch builds its combined files, if any, and then sends `batch.completed`. `data.object` is the full [batch object](#the-batch-object), including signed URLs for the combined files unless your workspace has file URLs in webhooks turned off.

There are two ways to receive the events:

* **Webhook endpoints** subscribed to `batch.progress` or `batch.completed`. An endpoint with a `template_ids` filter receives a batch's events only when the filter lists the batch's template.
* **A webhook for one batch**, set in the request:

```json
{
  "template_id": "tpl_01J9ZK3M7Q8V5W2X4Y6Z8A0B1C",
  "items": [{ "data": { "name": "Ann Example", "course": "Safety 101" } }],
  "webhook": { "url": "https://example.com/webhooks/batches", "events": ["batch.completed"] }
}
```

| Field    | Description                                                                                                                           |
| -------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `url`    | Where the events go. HTTPS is required, except for test batches. Private and local addresses are rejected with `422 url_not_allowed`. |
| `events` | `batch.progress`, `batch.completed` or both (the default)                                                                             |

Like per-request render webhooks, these deliveries are signed with the secret of your workspace's default webhook endpoint, so the workspace needs one. Without it, the batch is rejected with `400 validation_error` at `/webhook`.

A delivery looks like this (the object is shortened):

```json
{
  "id": "evt_01J9ZP2C4E6G8J0K2M4P6R8T0V",
  "type": "batch.completed",
  "created_at": "2026-09-17T15:21:08Z",
  "workspace_id": "ws_01J9ZK0P2R4T6V8X0Z2B4D6F8H",
  "region": "eu",
  "data": {
    "object": {
      "id": "bat_01J9ZN4T6V8X0Z2B4D6F8H0K2M",
      "object": "batch",
      "status": "completed",
      "total": 1200,
      "succeeded": 1198,
      "failed": 2,
      "files": [{ "kind": "zip", "filename": "certificates.zip", "url": "https://files-eu.dynamicdocumentapi.com/f/…" }]
    }
  }
}
```

Batch events are signed, retried and logged like every other event; see [Webhooks](/docs/webhooks) to verify them. Test batches send them too, with `"test": true` on the batch. The endpoint test, `POST /v1/webhook-endpoints/{id}/test`, only sends render and template events, so run a small test batch to try batch events.

> **No render events for batch items**
>
> Item renders don't send `render.succeeded`, `render.failed` or `render.expired`, not even to endpoints subscribed to them: the batch events cover them. Read the outcome of each item from `GET /v1/batches/{id}/items`. Email events of items are sent as usual.

## Emails [#emails]

With [email delivery](/docs/email-delivery), on Growth and higher plans, every item sends the template's email rules like a single render does. `delivery.email` works as in a render request: leave it out for the template's enabled rules, set `false` for none, or list the rules to use. The rules are read once, when the batch is created.

The batch object's `emails` counts the newest email of each item and rule by status. To send the failed ones again:

```bash
curl https://api-eu.dynamicdocumentapi.com/v1/batches/bat_01J9ZN4T6V8X0Z2B4D6F8H0K2M/resend-emails \
  -H "Authorization: Bearer $DYNAMIC_DOCUMENT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"statuses": ["failed"]}'
```

```json
{ "resent": 12, "skipped": [{ "send_id": "ems_01J9ZM1X3F7R8K2C4V6B8N0P2T", "code": "email_file_expired" }] }
```

`statuses` defaults to `["failed"]`. Add `"unknown"` to resend emails whose outcome is unknown as well, but check your provider's logs first: the provider may already have delivered them. `skipped` lists up to 100 emails that couldn't be resent, with the reason.

## Zero retention [#zero-retention]

A batch can use [zero-retention delivery](/docs/renders) when the files go to your own [storage](/docs/storage). Set `delivery.retention` to `"none"` (or turn zero retention on in your workspace settings), and send the files to a storage destination, in `delivery.storage` or through your workspace's default destinations:

```json
{
  "template_id": "tpl_01J9ZK3M7Q8V5W2X4Y6Z8A0B1C",
  "items": [{ "data": { "employee": { "name": "Alex Example" }, "salary": 5400 } }],
  "delivery": {
    "retention": "none",
    "storage": [{ "destination_id": "dst_01J9ZK7R9T1V3X5Z7B9D1F3H5K" }]
  }
}
```

In a zero-retention batch:

* each item's files go to your storage only, so there are no hosted copies and no download URLs
* `delivery.type` is `none`, the only type such a batch can use, so you can leave it out
* `combine` is rejected, because there are no hosted files to combine
* an item's data is deleted as soon as the item finishes, so `retry-failed` answers `409 zero_retention`; submit the failed items in a new batch
* email rules don't apply

## Test batches [#test-batches]

A batch created with a test key is a test batch, with `"test": true`. Sending `"test": true` with a live key fails with `403 test_key_required`.

The items of a test batch are test renders: free and watermarked "TEST". A test batch counts as one render towards your plan's test renders per minute, whatever its size, and sends webhook events like a live batch.

## Billing [#billing]

Every item is a render and is billed like one, following the [render table](/docs/plans-and-limits): a PDF of up to 50 pages is 1 render, each further 50 pages add 1, and an image is 1 render. Each item's render shows its cost in `billed_renders`.

| Part of a batch                          | Billed renders                                    |
| ---------------------------------------- | ------------------------------------------------- |
| Item that succeeds                       | The same as a single render of that output        |
| Item that fails or is canceled           | 0                                                 |
| ZIP file                                 | 0 (the renders inside are already billed)         |
| Merged PDF                               | 1                                                 |
| Retried item                             | Billed like any item when its new render succeeds |
| Test batch, including its combined files | 0                                                 |

For example, a batch of 1,000 two-page invoices with a ZIP file and a merged PDF costs 1,001 renders when every item succeeds.

Items are billed as they finish. A live batch that runs out of renders [pauses](#paused-batches) until renders are available again.

## Limits [#limits]

Batches are available from the Starter plan. On Free, `POST /v1/batches` answers `402 plan_feature_unavailable`.

| Limit            | Free | Starter | Growth | Pro    | Scale  | Enterprise |
| ---------------- | ---- | ------- | ------ | ------ | ------ | ---------- |
| Items per batch  | —    | 500     | 2,000  | 10,000 | 50,000 | custom     |
| Bulk concurrency | —    | 2       | 10     | 20     | 50     | custom     |

**Bulk concurrency** is the number of batch items of your workspace that render at the same time, across all its batches. Running batches take turns, oldest first, and a new item starts when one finishes. Your current value is `limits.bulk_concurrency` in `GET /v1/account`.

A batch with more items than your plan allows is rejected with `400 validation_error` (`too_many_items`); split it into several batches.

| Limit on every plan                | Value                                                            |
| ---------------------------------- | ---------------------------------------------------------------- |
| Request body of `POST /v1/batches` | 25 MB                                                            |
| Data per item                      | 1 MB                                                             |
| CSV upload                         | 50 MB, kept for 24 hours                                         |
| `metadata`                         | 50 keys for batch and item together, values up to 500 characters |
| `reference` and item `filename`    | 255 characters                                                   |
| `combine.zip_filename`             | 200 characters                                                   |
| `csv_mapping`                      | 500 entries                                                      |

Each item also renders within your plan's limits for a single render, such as the async maximum duration and the pages per PDF. See [Plans and limits](/docs/plans-and-limits).

## Idempotency [#idempotency]

`POST /v1/batches` accepts an `Idempotency-Key` header, as `POST /v1/renders` does. Reuse the key when you retry after a network error or a timeout:

* The same key with the same body within 24 hours returns the original `202` response, and no second batch is created.
* While the first request is still being processed, a retry fails with `409 idempotency_in_progress`.
* The same key with a different body fails with `422 idempotency_key_reused`.

The cancel, retry-failed and resend-emails endpoints accept the header too.

## Errors [#errors]

| Status | Code                                      | When                                                                                                                                                                                                                                                 |
| ------ | ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | `validation_error`                        | The request or an item is invalid. `errors[]` points at each problem, such as `/items/3/metadata`, `/csv/row/5` or `/combine/merged_pdf`. `/combine` with the code `zero_retention` or `not_hosted` means the items keep no hosted files to combine. |
| `402`  | `plan_feature_unavailable`                | Your plan doesn't include batches, or a feature the batch uses, such as e-invoices below Growth                                                                                                                                                      |
| `402`  | `render_limit_reached`                    | A live batch was created while your workspace had no renders left                                                                                                                                                                                    |
| `403`  | `test_key_required`                       | `"test": true` with a live key                                                                                                                                                                                                                       |
| `404`  | `template_not_found`, `version_not_found` | The template or the version doesn't exist, or the template has no published version                                                                                                                                                                  |
| `404`  | `upload_not_found`                        | The CSV upload doesn't exist or has expired                                                                                                                                                                                                          |
| `404`  | `not_found`                               | The batch doesn't exist. Test keys only see test batches.                                                                                                                                                                                            |
| `409`  | `conflict`                                | Cancel on a finished batch, or retry-failed on a running batch or one without failed items                                                                                                                                                           |
| `409`  | `zero_retention`                          | Retry-failed on a zero-retention batch                                                                                                                                                                                                               |
| `409`  | `item_files_expired`                      | Retry-failed on a batch with `combine` whose succeeded items' files are gone; `items` lists their indexes                                                                                                                                            |
| `410`  | `template_deleted`                        | The template was deleted                                                                                                                                                                                                                             |
| `413`  | `payload_too_large`                       | The request body is over 25 MB, or an item's data is over 1 MB                                                                                                                                                                                       |
| `422`  | `data_schema_mismatch`                    | An item's data doesn't match the template's JSON Schema                                                                                                                                                                                              |
| `422`  | `url_not_allowed`                         | The batch's `webhook.url` isn't allowed                                                                                                                                                                                                              |

See [Errors](/docs/errors) for the problem details format and every other code.

## Related pages [#related-pages]

* [Renders](/docs/renders) for the render request, delivery options and the render object
* [Webhooks](/docs/webhooks) for endpoints, signatures and retries
* [Storage](/docs/storage) for uploading files to your own buckets
* [Email delivery](/docs/email-delivery) for email rules and sends
* [E-invoicing](/docs/e-invoicing) for e-invoices in batches
* [PDF tools](/docs/pdf-tools) for merging, splitting and converting existing PDFs
* [Plans and limits](/docs/plans-and-limits) for the render table and every limit
* [Errors](/docs/errors) for every error code
