> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dairo.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Send a batch

> Send one letter template to up to 1,000 recipients in a single call, each letter rendered with its own data and tracked together.

Send the same letter to many recipients in one call. A batch takes a stored
letter template and a recipient list, renders one letter per recipient, and
groups them under a single `letter_batch` you can track together.

<Note>
  Batches use the same scopes as single letters: creating a batch needs
  `letters:send`; reading one needs `letters:read`.
</Note>

## Create a batch

`POST /v1/letters/batches` takes a `templateId` and a `recipients` array — one
entry per letter, up to 1,000. Each recipient is a postal `to` address plus
the optional `templateData` that fills the [template](/letters/templates)'s
placeholders for that one letter. Every other option is set once and applies
to every letter in the batch.

<CodeGroup>
  ```bash title="cURL" theme={null}
  curl -X POST https://api.dairo.app/v1/letters/batches \
    -H "Authorization: Bearer $DAIRO_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "templateId": "ltpl_price_change",
      "recipients": [
        {
          "to": { "name": "Jane Doe", "street": "Hauptstrasse", "houseNumber": "12", "postalCode": "8001", "city": "Zürich", "country": "CH" },
          "templateData": { "firstName": "Jane", "newPrice": "CHF 49" }
        },
        {
          "to": { "name": "Hans Müller", "street": "Bahnhofstrasse", "houseNumber": "3", "postalCode": "3011", "city": "Bern", "country": "CH" },
          "templateData": { "firstName": "Hans", "newPrice": "CHF 49" }
        }
      ],
      "print": { "mode": "grayscale", "sides": "duplex" },
      "delivery": "economy",
      "notifications": false
    }'
  ```

  ```bash title="CLI" theme={null}
  dairo letter batch create \
    --template-id ltpl_price_change \
    --recipients '[
      { "to": { "name": "Jane Doe", "street": "Hauptstrasse", "houseNumber": "12", "postalCode": "8001", "city": "Zürich", "country": "CH" }, "templateData": { "firstName": "Jane", "newPrice": "CHF 49" } },
      { "to": { "name": "Hans Müller", "street": "Bahnhofstrasse", "houseNumber": "3", "postalCode": "3011", "city": "Bern", "country": "CH" }, "templateData": { "firstName": "Hans", "newPrice": "CHF 49" } }
    ]' \
    --print '{ "mode": "grayscale", "sides": "duplex" }' \
    --delivery economy \
    --no-notifications
  ```
</CodeGroup>

```json theme={null}
{
  "object": "letter_batch",
  "id": "lbatch_01HZX...",
  "total": 2,
  "status": "queued"
}
```

### Request fields

| Field           | Required | Description                                                                                                                                          |
| --------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `templateId`    | yes      | The stored letter template rendered once per recipient. An unknown id returns `404`.                                                                 |
| `recipients`    | yes      | 1–1,000 entries of `{ to, templateData? }`. `to` needs a `country` and a `street` or `poBox`; `templateData` fills that letter's `{{placeholders}}`. |
| `from`          | no       | Optional sender block applied to every letter.                                                                                                       |
| `print`         | no       | `mode`, `sides`, `addressPlacement` — shared by every letter. See [Print & delivery options](/letters/print-delivery).                               |
| `delivery`      | no       | Delivery class; defaults to `economy`. Shared by every letter.                                                                                       |
| `paymentSlip`   | no       | Declares a payment slip (`qr`, `sepaDe`, `sepaAt`) the template already draws, applied to every letter.                                              |
| `autoSend`      | no       | `true` (default) queues each letter for posting; `false` stores them as drafts.                                                                      |
| `notifications` | no       | Owner milestone emails per letter; defaults to `true`. See [Status & tracking](/letters/status-lifecycle).                                           |
| `metadata`      | no       | Opaque JSON object applied to every letter.                                                                                                          |

<Warning>
  The whole request is validated before anything is created — the template id,
  the shared options, and every recipient's address and country. One malformed
  recipient fails the call with `400` or `422` and no letter is created. Past
  create, each letter is its own irreversible physical send: the cancellation
  window applies per letter, not to the batch as a whole.
</Warning>

## Track a batch

`GET /v1/letters/batches/{id}` returns the batch with a live rollup: its
member letters counted by status in `statusCounts`, and `completed` — the
number of letters in a terminal state (`delivered`, `undeliverable`,
`canceled`, or `failed`).

<CodeGroup>
  ```bash title="cURL" theme={null}
  curl https://api.dairo.app/v1/letters/batches/lbatch_01HZX \
    -H "Authorization: Bearer $DAIRO_API_KEY"
  ```

  ```bash title="CLI" theme={null}
  dairo letter batch get lbatch_01HZX
  ```
</CodeGroup>

```json theme={null}
{
  "object": "letter_batch",
  "id": "lbatch_01HZX...",
  "templateId": "ltpl_price_change",
  "status": "queued",
  "total": 2,
  "completed": 1,
  "statusCounts": { "delivered": 1, "in_transit": 1 },
  "createdAt": "2026-06-22T10:01:02Z",
  "updatedAt": "2026-06-23T08:00:11Z"
}
```

To work with the letters individually, list them with the `batchId` filter —
each member is an ordinary letter you can fetch, track, and cancel one at a
time on [Send a letter](/letters/sending-a-letter). Each member's
`letter.status_changed` webhook events also carry the `batchId`, so a webhook
consumer can attribute every update to its batch.

```bash title="cURL" theme={null}
curl "https://api.dairo.app/v1/letters?batchId=lbatch_01HZX&limit=100" \
  -H "Authorization: Bearer $DAIRO_API_KEY"
```

## From an MCP agent

Connect an MCP-compatible agent to the [hosted server](/agent-first/mcp-server)
and batches are available as named tools, gated by the same scopes. Sending is
state-changing, so `send_letters` requires an explicit `confirm: true`.

| Tool (`action`)                                     | Operation                     | Scope          |
| --------------------------------------------------- | ----------------------------- | -------------- |
| `send_letters` `{ "action": "sendBatch" }`          | Create a batch                | `letters:send` |
| `list_letters` `{ "action": "getBatch" }`           | Get a batch's status rollup   | `letters:read` |
| `list_letters` `{ "action": "list", "batchId": … }` | List a batch's member letters | `letters:read` |

```text title="MCP" theme={null}
Tool: send_letters   (scope letters:send, confirm required)
Args: {
  "action": "sendBatch",
  "templateId": "ltpl_price_change",
  "recipients": [
    { "to": { "name": "Jane Doe", "street": "Hauptstrasse", "houseNumber": "12", "postalCode": "8001", "city": "Zürich", "country": "CH" }, "templateData": { "firstName": "Jane" } }
  ],
  "delivery": "economy",
  "confirm": true
}
```

<Tip>
  To project a batch's cost, [price one letter](/letters/pricing) with the
  template's page count and your delivery class. Each member is priced for its
  own page count, so when every recipient renders the same number of pages the
  batch total is the recipient count times the per-letter price.
</Tip>

## Next steps

* [API reference](/api-reference) — full request and response schemas for batches.
