> ## 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 letter

> Create a physical letter from a PDF and a postal address, then list, fetch, cancel, and price letters over the API.

Create a letter from a PDF and a postal address, then follow it by id. This
page covers the core letter operations; letter templates and batches have
their own pages.

| Operation                                                                 | Method & path                  | Scope          |
| ------------------------------------------------------------------------- | ------------------------------ | -------------- |
| [Create a letter](/api-reference/dairo-api/letters/create-letter)         | `POST /v1/letters`             | `letters:send` |
| [List letters](/api-reference/dairo-api/letters/list-letters)             | `GET /v1/letters`              | `letters:read` |
| [Get a letter](/api-reference/dairo-api/letters/get-letter)               | `GET /v1/letters/{id}`         | `letters:read` |
| [Cancel a letter](/api-reference/dairo-api/letters/cancel-letter)         | `POST /v1/letters/{id}/cancel` | `letters:send` |
| [List letter events](/api-reference/dairo-api/letters/list-letter-events) | `GET /v1/letters/{id}/events`  | `letters:read` |
| [Price a letter](/api-reference/dairo-api/letters/price-letter)           | `POST /v1/letters/price`       | `letters:read` |

## Create a letter

Pass the PDF as `pdfBase64`, a `fileName`, and a recipient `to` address.
Everything else has defaults.

<CodeGroup>
  ```bash title="cURL" theme={null}
  curl -X POST https://api.dairo.app/v1/letters \
    -H "Authorization: Bearer $DAIRO_API_KEY" \
    -H "Content-Type: application/json" \
    -H "Idempotency-Key: invoice-inv_123" \
    -d '{
      "pdfBase64": "JVBERi0xLjQK...",
      "fileName": "invoice-2026-06.pdf",
      "to": {
        "name": "Jane Doe",
        "company": "Acme GmbH",
        "street": "Hauptstrasse",
        "houseNumber": "12",
        "postalCode": "8001",
        "city": "Zürich",
        "country": "CH"
      },
      "print": { "mode": "grayscale", "sides": "duplex", "addressPlacement": "left" },
      "delivery": "economy",
      "metadata": { "invoiceId": "inv_123" }
    }'
  ```

  ```ts title="TypeScript" theme={null}
  const letter = await dairo.letters.create(
    {
      pdfBase64: pdf.toString("base64"),
      fileName: "invoice-2026-06.pdf",
      to: {
        name: "Jane Doe",
        company: "Acme GmbH",
        street: "Hauptstrasse",
        houseNumber: "12",
        postalCode: "8001",
        city: "Zürich",
        country: "CH",
      },
      print: { mode: "grayscale", sides: "duplex", addressPlacement: "left" },
      delivery: "economy",
      metadata: { invoiceId: "inv_123" },
    },
    { idempotencyKey: "invoice-inv_123" },
  );
  console.log(letter.id, letter.status); // let_… queued
  ```

  ```python title="Python" theme={null}
  letter = dairo.letters.create(
      pdf_base64=base64.b64encode(pdf).decode(),
      file_name="invoice-2026-06.pdf",
      to={
          "name": "Jane Doe",
          "company": "Acme GmbH",
          "street": "Hauptstrasse",
          "house_number": "12",
          "postal_code": "8001",
          "city": "Zürich",
          "country": "CH",
      },
      print={"mode": "grayscale", "sides": "duplex", "address_placement": "left"},
      delivery="economy",
      metadata={"invoiceId": "inv_123"},
      idempotency_key="invoice-inv_123",
  )
  print(letter.id, letter.status)  # let_… queued
  ```

  ```bash title="CLI" theme={null}
  dairo letter send \
    --pdf ./invoice-2026-06.pdf \
    --to-name "Jane Doe" --to-company "Acme GmbH" \
    --to-street "Hauptstrasse" --to-house-number 12 \
    --to-postal-code 8001 --to-city "Zürich" --to-country CH \
    --grayscale --duplex --address-placement left \
    --delivery economy \
    --confirm
  ```
</CodeGroup>

The response is the `letter` object at `status: "queued"` — accepted and being
prepared, nothing printed yet. `pageCount` and `price` are `null` until the
letter is priced.

```json theme={null}
{
  "object": "letter",
  "id": "let_01HZX...",
  "status": "queued",
  "fileName": "invoice-2026-06.pdf",
  "pageCount": null,
  "to": {
    "name": "Jane Doe", "company": "Acme GmbH",
    "street": "Hauptstrasse", "houseNumber": "12",
    "postalCode": "8001", "city": "Zürich", "country": "CH"
  },
  "print": { "mode": "grayscale", "sides": "duplex", "addressPlacement": "left" },
  "delivery": "economy",
  "paymentSlip": null,
  "autoSend": true,
  "notifications": true,
  "price": null,
  "trackingNumber": null,
  "metadata": { "invoiceId": "inv_123" },
  "submittedAt": null,
  "createdAt": "2026-06-22T10:01:02Z",
  "updatedAt": "2026-06-22T10:01:02Z"
}
```

<Tip>
  Always pass an `Idempotency-Key` header (or `idempotencyKey` in the SDKs) when
  creating letters. A retried create with the same key returns the original
  letter instead of printing a second one — physical mail cannot be unsent. See
  [Retries & idempotency](/concepts/idempotency). In the CLI, `dairo letter send`
  creates a draft unless you pass `--confirm`, so an accidental rerun never
  posts anything.
</Tip>

### Document sources

A letter's document comes from exactly one of three sources:

| Source       | What it is                                                                                                                                                                                                                       |
| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `pdfBase64`  | The PDF inline, base64-encoded.                                                                                                                                                                                                  |
| `file`       | A PDF already in Dairo — either an email attachment (`attachmentId`, with an optional informational `messageId`) or a storage object (`objectId`). Exactly one of the two; the storage object must have passed its malware scan. |
| `templateId` | A stored [letter template](/letters/templates), rendered with `templateData` and the recipient composited into the address window.                                                                                               |

```json title="file: reuse an email attachment" theme={null}
{
  "file": { "attachmentId": "att_9f2c", "messageId": "msg_abc" },
  "fileName": "invoice-2026-06.pdf",
  "to": { "name": "Jane Doe", "street": "Hauptstrasse", "houseNumber": "12", "postalCode": "8001", "city": "Zürich", "country": "CH" }
}
```

```json title="file: reuse a storage object" theme={null}
{
  "file": { "objectId": "obj_7a1b" },
  "fileName": "invoice-2026-06.pdf",
  "to": { "name": "Jane Doe", "street": "Hauptstrasse", "houseNumber": "12", "postalCode": "8001", "city": "Zürich", "country": "CH" }
}
```

The document must be a real PDF no larger than 25 MB. A non-PDF is rejected
with `422`; an oversize file with `413`. Providing more than one source — or
none — is a `400`.

<Tip>
  Bringing your own PDF? It must follow the
  [layout contract](/letters/layout-and-verification) — A4 portrait, the
  recipient address inside the envelope window, franking zones clear, fonts
  embedded. `POST /v1/letters/verify` checks all of it and returns a structured
  pass/warn/fail report **before** anything is printed; `GET
    /v1/letters/requirements` returns the machine-readable spec plus compliant
  starter templates.
</Tip>

### Request fields

| Field                               | Required | Description                                                                                         |
| ----------------------------------- | -------- | --------------------------------------------------------------------------------------------------- |
| `pdfBase64` / `file` / `templateId` | one of   | The document source — exactly one of the three.                                                     |
| `fileName`                          | yes      | A name for the document, kept for your records.                                                     |
| `to`                                | yes      | Recipient postal address. `country` (ISO 3166-1 alpha-2) is required, plus a `street` or a `poBox`. |
| `from`                              | no       | Optional sender block, same shape as `to`.                                                          |
| `print`                             | no       | `mode`, `sides`, `addressPlacement`. Defaults: `grayscale`, `simplex`, `left`.                      |
| `delivery`                          | no       | Delivery class; defaults to `economy`.                                                              |
| `autoSend`                          | no       | `true` (default) queues the letter for posting; `false` stores it as a `draft`.                     |
| `templateData`                      | no       | Values that fill the template's `{{placeholders}}`; used only with `templateId`.                    |
| `payment` / `paymentSlip`           | no       | Add a payment slip — see [Print & delivery options](/letters/print-delivery).                       |
| `notifications`                     | no       | Owner milestone emails for this letter; defaults to `true`.                                         |
| `metadata`                          | no       | Opaque JSON object (up to 8 KB), stored and echoed back.                                            |

<Note>
  The recipient's `country` must be a destination Dairo can deliver to. An
  unsupported or malformed country is rejected with `422` naming `to.country`.
</Note>

## List letters

List your letters, most recent first. Page with `limit` (1–100, default 20)
and `cursor` using [keyset pagination](/concepts/pagination), and filter by
`status`, `country`, or `batchId`.

<CodeGroup>
  ```bash title="cURL" theme={null}
  curl "https://api.dairo.app/v1/letters?limit=20&status=in_transit" \
    -H "Authorization: Bearer $DAIRO_API_KEY"
  ```

  ```ts title="TypeScript" theme={null}
  const page = await dairo.letters.list({ limit: 20, status: "in_transit" });
  for (const letter of page.data) console.log(letter.id, letter.status);
  ```

  ```python title="Python" theme={null}
  page = dairo.letters.list(limit=20, status="in_transit")
  for letter in page.data:
      print(letter.id, letter.status)
  ```

  ```bash title="CLI" theme={null}
  dairo letter list --limit 20 --status in_transit
  ```
</CodeGroup>

List items are compact: the `to` block carries only the recipient's `city` and
`country`, never the full address.

```json theme={null}
{
  "object": "list",
  "data": [
    {
      "object": "letter",
      "id": "let_01HZX...",
      "status": "in_transit",
      "fileName": "invoice-2026-06.pdf",
      "to": { "city": "Zürich", "country": "CH" },
      "delivery": "economy",
      "trackingNumber": "RR123456785CH",
      "createdAt": "2026-06-22T10:01:02Z"
    }
  ],
  "pagination": { "nextCursor": "eyJjcmVhdGVkX2F0Ijoi...", "hasMore": true }
}
```

## Get one letter

Fetch a letter by `id` to read its current `status`, `price`, any
`trackingNumber`, and its full address, with the delivery `events` timeline
inlined newest first (up to 100 events). Fetching an in-flight letter also
refreshes its tracking state, so a poll returns the latest known status.

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

  ```ts title="TypeScript" theme={null}
  const letter = await dairo.letters.get("let_01HZX");
  console.log(letter.status, letter.trackingNumber);
  for (const e of letter.events) console.log(e.type, e.occurredAt);
  ```

  ```python title="Python" theme={null}
  letter = dairo.letters.get("let_01HZX")
  print(letter.status, letter.tracking_number)
  for e in letter.events:
      print(e.type, e.occurred_at)
  ```

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

```json theme={null}
{
  "object": "letter",
  "id": "let_01HZX...",
  "status": "in_transit",
  "trackingNumber": "RR123456785CH",
  "price": { "currency": "eur", "amount": 0.92 },
  "pageCount": 3,
  "to": {
    "name": "Jane Doe", "street": "Hauptstrasse", "houseNumber": "12",
    "postalCode": "8001", "city": "Zürich", "country": "CH"
  },
  "print": { "mode": "grayscale", "sides": "duplex", "addressPlacement": "left" },
  "delivery": "economy",
  "events": [
    {
      "object": "letter_event",
      "eventId": "lev_2",
      "type": "in_transit",
      "code": "handed_to_carrier",
      "description": "Handed to postal carrier",
      "occurredAt": "2026-06-23T08:00:00Z"
    }
  ],
  "createdAt": "2026-06-22T10:01:02Z",
  "updatedAt": "2026-06-23T08:00:11Z"
}
```

A letter id that is unknown or not yours returns `404` — the two cases are
indistinguishable by design.

## Cancel a letter

Cancel a letter that has not been dispatched. Cancellation is allowed while
the letter is `draft`, `queued`, `processing`, `printable`, or `submitted` —
once it is `in_transit` it is in the postal network and cannot be recalled.

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

  ```ts title="TypeScript" theme={null}
  const canceled = await dairo.letters.cancel("let_01HZX");
  console.log(canceled.status); // canceled
  ```

  ```python title="Python" theme={null}
  canceled = dairo.letters.cancel("let_01HZX")
  print(canceled.status)  # canceled
  ```

  ```bash title="CLI" theme={null}
  dairo letter cancel let_01HZX
  ```
</CodeGroup>

You get back the letter with `status: "canceled"` and `canceledAt` set.
Canceling a letter that is already past dispatch returns `409 Conflict` with
`code: "letter_not_cancelable"`.

## List a letter's events

Fetch the delivery timeline on its own. Events come back newest first, up to
the 100 most recent.

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

  ```ts title="TypeScript" theme={null}
  const events = await dairo.letters.listEvents("let_01HZX");
  for (const e of events.data) console.log(e.type, e.code, e.occurredAt);
  ```

  ```python title="Python" theme={null}
  events = dairo.letters.list_events("let_01HZX")
  for e in events.data:
      print(e.type, e.code, e.occurred_at)
  ```

  ```bash title="CLI" theme={null}
  dairo letter events let_01HZX
  ```
</CodeGroup>

```json theme={null}
{
  "object": "list",
  "data": [
    {
      "object": "letter_event",
      "eventId": "lev_1",
      "letterId": "let_01HZX...",
      "type": "submitted",
      "code": "submitted_for_print",
      "description": "Submitted to print center",
      "hasImage": false,
      "occurredAt": "2026-06-22T10:05:00Z",
      "recordedAt": "2026-06-22T10:05:03Z"
    }
  ],
  "pagination": { "nextCursor": null, "hasMore": false }
}
```

The event field reference is on [Status & tracking](/letters/status-lifecycle).

## Price a letter

A letter's cost depends on its page count, print options, delivery class, and
destination. `POST /v1/letters/price` returns the exact cost without creating
a letter — pass a `pageCount`, or the `pdfBase64` itself and Dairo counts the
pages.

```bash title="cURL" theme={null}
curl -X POST https://api.dairo.app/v1/letters/price \
  -H "Authorization: Bearer $DAIRO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "country": "CH",
    "pageCount": 3,
    "print": { "mode": "grayscale", "sides": "duplex" },
    "delivery": "economy"
  }'
```

The full request reference, the quote-then-send pattern, and how letters bill
are on [Pricing](/letters/pricing).

## Next steps

* [Send a batch](/letters/batches) — one template, up to 1,000 recipients, in one call.
* [API reference](/api-reference) — full request and response schemas for every letters endpoint.
