---
title: "Broadcasts"
description: "Every operation in this group: what it accepts, what it returns and the errors it can answer with."
url: "https://nextmail.sh/docs/api/reference/broadcasts"
area: "API"
category: "Reference"
---

# Broadcasts

Every operation in this group: what it accepts, what it returns and the errors it can answer with.

## Operations

One message sent to everybody in one or more audiences, as a separate copy for each person, personalised from the contact with merge fields and carrying a one-click unsubscribe. The sending happens in the background: create the broadcast, then read it for its progress, and list its copies with `GET /emails?broadcastId=`.

Every copy carries the unsubscribe headers the large mailbox providers require of bulk mail, and a person who unsubscribes is skipped by every later broadcast to those audiences. The suppression list applies as it does to every send.

### `GET /broadcasts`

List broadcasts

Every broadcast in this workspace, newest first, one page at a time, each with live `counts`. `audienceId` keeps the ones that were sent to that audience, among others or alone.

The individual messages are not here. List them with `GET /emails?broadcastId=`.

A key limited to particular addresses or domains lists only the broadcasts sent from an address or domain it holds.

Requires the `emails:read` scope.

- Scopes: `emails:read`.

**Query parameters**

- `limit` (`integer`, at least 1, at most 100, default `25`): Rows per page, 1 to 100.
- `cursor` (`string`): A broadcast id. Keyset, not offset: pass the previous page's `nextCursor`. One that names nothing in this list is a 400 `invalid_cursor`.
- `audienceId` (`string`, 1 to 64 characters): Only the broadcasts that included this audience. An id that names no audience answers an empty list rather than a 404.

**Returns**

- `200` `BroadcastList`: A page of broadcasts, newest first.

**Errors**

- `400`: `invalid_cursor` when `cursor` names no broadcast in this workspace.
- `422`: `invalid_parameter` on a `limit` out of range.
- The errors every operation can return: `401`, `403`, `404`, `429`, `500`, described in the [error catalog](https://nextmail.sh/docs/api/errors).

Also available in: TypeScript [`nextmail.broadcasts.list()`](https://nextmail.sh/docs/sdk/reference/broadcasts#list), [`nextmail.broadcasts.listAll()`](https://nextmail.sh/docs/sdk/reference/broadcasts#listAll), [`nextmail.broadcasts.iterate()`](https://nextmail.sh/docs/sdk/reference/broadcasts#iterate); Python [`broadcasts.list()`](https://nextmail.sh/docs/python/reference/broadcasts#list), [`broadcasts.list_all()`](https://nextmail.sh/docs/python/reference/broadcasts#listAll), [`broadcasts.iterate()`](https://nextmail.sh/docs/python/reference/broadcasts#iterate); Ruby [`broadcasts.list`](https://nextmail.sh/docs/ruby/reference/broadcasts#list), [`broadcasts.list_all`](https://nextmail.sh/docs/ruby/reference/broadcasts#listAll), [`broadcasts.iterate`](https://nextmail.sh/docs/ruby/reference/broadcasts#iterate); PHP [`broadcasts->list`](https://nextmail.sh/docs/php/reference/broadcasts#list), [`broadcasts->listAll`](https://nextmail.sh/docs/php/reference/broadcasts#listAll), [`broadcasts->iterate`](https://nextmail.sh/docs/php/reference/broadcasts#iterate); Go [`Broadcasts.List`](https://nextmail.sh/docs/go/reference/broadcasts#list), [`Broadcasts.ListAll`](https://nextmail.sh/docs/go/reference/broadcasts#listAll), [`Broadcasts.Iterate`](https://nextmail.sh/docs/go/reference/broadcasts#iterate); Java [`broadcasts().list`](https://nextmail.sh/docs/java/reference/broadcasts#list), [`broadcasts().listAll`](https://nextmail.sh/docs/java/reference/broadcasts#listAll), [`broadcasts().iterate`](https://nextmail.sh/docs/java/reference/broadcasts#iterate); C# [`Broadcasts.ListAsync`](https://nextmail.sh/docs/csharp/reference/broadcasts#list), [`Broadcasts.ListAllAsync`](https://nextmail.sh/docs/csharp/reference/broadcasts#listAll), [`Broadcasts.IterateAsync`](https://nextmail.sh/docs/csharp/reference/broadcasts#iterate); CLI [`nextmail broadcasts list`](https://nextmail.sh/docs/cli/reference/broadcasts#broadcasts-list); MCP [`listBroadcasts`](https://nextmail.sh/docs/mcp/tools/broadcasts#listBroadcasts).

### `POST /broadcasts`

Send to audiences

Sends one message to everybody in one or more audiences, as a separate copy for each person. Every copy has exactly one recipient and no cc or bcc, so nobody sees who else it went to, and every copy is an ordinary email with its own `msg_` id, events, tracking and webhooks. `GET /emails?broadcastId=` lists them. Copies are not filed in the Sent folder: the broadcast is the record.

The call answers 202 straight away with the broadcast `queued`, or `scheduled` when `scheduledAt` is set, and the sending happens in the background, 50 people at a time. Read `GET /broadcasts/{id}` for `status` and `counts` as it goes.

WHO GETS IT. Every contact in at least one of `audienceIds`, counted once however many of them hold it, except a person who has unsubscribed (`GET /suppressions/unsubscribes` lists them), and except an address on the suppression list. A contact in one of `excludeAudienceIds` is left out too, even when they are in one of `audienceIds`. `audienceIds` cannot name All contacts, the built-in audience that holds everyone this workspace has ever written to, because most of those people never asked for its mail: that is a 422 `audience_is_everyone`, and a send to it that was scheduled before this rule fails when its time comes. A contact added to one of the audiences after this call but before the sending reaches it is included. `counts.recipients` is the estimate taken now, and `POST /broadcasts/preview` returns the same count without sending anything.

QUOTA. The whole send is checked against the plan's monthly sends before anything is written. A broadcast the allowance cannot cover is refused with 429 `send_quota_exceeded` and leaves nothing behind. Each copy counts as one send.

PERSONALISATION. `subject`, `html` and `text` take merge fields, filled in for each person: `{{firstName}}`, `{{lastName}}`, `{{name}}`, `{{email}}`, `{{unsubscribeUrl}}`. Each takes a fallback after a bar, such as `{{firstName|there}}`, used when the contact has no value for it. The values come from the contact: the first name is the first word of its name and the last name is the rest. A custom field of the contact is `{{attributes.<key>}}`, such as `{{attributes.plan|Free}}`, and reads as its fallback, or as nothing, when the contact has no such field. Every value is escaped in `html`, and any other `{{…}}` is left as written. With `template`, the five values above are passed as props, but only the props the template declares, except `unsubscribeUrl`, which every template receives.

UNSUBSCRIBE. Every copy carries `List-Unsubscribe` and `List-Unsubscribe-Post: List-Unsubscribe=One-Click`, which is what lets a mail client offer its own unsubscribe button and what the large mailbox providers require of bulk mail. A body or a template that does not place `{{unsubscribeUrl}}` itself gets a short footer with the link, in English, which also names the sender and shows the postal address once `marketingPostalAddress` is set on `PATCH /settings`. A template receives `unsubscribeUrl` whether it declares it or not, so one that places it decides where the link goes. The link opens a page with an Unsubscribe button, and following it unsubscribes the person from all of this workspace's marketing mail: every audience they are in shows `unsubscribedAt` on `GET /audiences/{id}/contacts`, they are listed on `GET /suppressions/unsubscribes`, and later broadcasts and automation emails skip them, even after they are added to an audience again or their contact is deleted and made again. Their contact and mail sent to them one message at a time are not affected.

LIMITS. 1 to 10 audiences and up to 8 tags, and every copy also carries the tag `broadcast_id`. No attachments, cc, bcc, translation or encryption. The body comes from `html` and or `text`, or from `template`, never both.

IDEMPOTENCY. Send an `Idempotency-Key` and a retry with the same key answers 200 with the broadcast the first call created, with `replayed: true` and `Idempotency-Replayed: true`, instead of sending again. The same key with a different body is a 422 `idempotency_key_reuse`. Without a key, sending the same body twice sends the broadcast twice.

Requires the `emails:send` and `audiences:read` scopes.

- Scopes: `emails:send`, `audiences:read`.
- Honours `Idempotency-Key`.

**Headers**

- `Idempotency-Key` (`string`, up to 255 characters, pattern `^[A-Za-z0-9_.:-]+$`): Makes a retry safe. Reusing one with a different body is a 422.

**Request body**

- `audienceIds` (`string[]`, required, 1 to 10 items): 1 to 10 audience ids. A contact in several of them gets one copy. Each id has to name an audience in this workspace, or nothing is sent.
- `excludeAudienceIds` (`string[]`, up to 10 items): Up to 10 audiences whose members are left out, even when they are also in one of `audienceIds`, such as the customers who already bought. All contacts may be named here.
- `segmentId` (`string`, 1 to 64 characters): A saved segment that narrows the audiences: only the contacts that match its conditions get a copy, such as the people who opened in the last 90 days. The conditions are read when the broadcast is sent or scheduled and kept with it, so editing the segment later changes nothing that is already on its way. Resending to the people who did not open leaves the segment out, because they already passed it.
- `from` (`string | object`, required): The sender, as on `POST /emails`: an address the key may send as, bare or with a name.
  - `email` (`string`, required, up to 320 characters)
  - `name` (`string`, up to 128 characters)
- `replyTo` (`string | object`): Where replies go, the same for every copy.
  - `email` (`string`, required, up to 320 characters)
  - `name` (`string`, up to 128 characters)
- `subject` (`string`, up to 998 characters, default `""`): Required unless a template supplies it. Takes merge fields: `{{firstName}}`, `{{lastName}}`, `{{name}}`, `{{email}}`, `{{unsubscribeUrl}}`, and `{{attributes.<key>}}` for a custom field of the contact, each with an optional fallback after a bar, such as `{{firstName|there}}`.
- `html` (`string`, up to 1000000 characters): The HTML body, with the same merge fields, their values escaped. Without `{{unsubscribeUrl}}` in it, a one-line unsubscribe footer is added.
- `text` (`string`, up to 1000000 characters): The plain text body, with the same merge fields. Without `{{unsubscribeUrl}}` in it, an unsubscribe line is added.
- `template` (`object`): A stored template instead of `html` and `text`. The merge values are passed as props, but only the ones the template declares, except `unsubscribeUrl`, which every template receives. A template that does not place it gets the same footer as a body.
  - `id` (`string`, required, 1 to 128 characters)
  - `version` (`integer`, more than 0, at most 100000)
  - `props` (`Record<string, any>`)
  - `slots` (`Record<string, any>`)
- `tracking` (`object`): Open and link tracking for every copy. A field left out takes the `from` address setting, as on `POST /emails`.
  - `opens` (`boolean`)
  - `clicks` (`boolean`)
- `tags` (`Record<string, string>`, default `{}`): Up to 8 tags, copied onto every copy beside `broadcast_id`, which the server adds.
- `utm` (`object`): Campaign tags added to every http and https link in the body: `source`, `medium`, `campaign`, `term` and `content` become `utm_source` and the rest, so the visits show up in your web analytics. A link that already carries one keeps its own value, and the unsubscribe link never gets them. Values are 1 to 100 characters, `campaign` up to 200.
  - `source` (`string`, 1 to 100 characters)
  - `medium` (`string`, 1 to 100 characters)
  - `campaign` (`string`, 1 to 200 characters)
  - `term` (`string`, 1 to 100 characters)
  - `content` (`string`, 1 to 100 characters)
- `abTest` (`object`): An A/B test. `variants` holds 2 to 4 versions, each a `subject`, or a body as `html` and or `text`, that replaces the broadcast's own, so `{}` keeps the broadcast as it is. `testPercent` (5 to 50, 20 unless set) of the people get a variant, split evenly between them, and `metric` (`opens` unless set, or `clicks`) picks the winner once `waitMinutes` (30 to 4320, 240 unless set) have passed since the test went out. Everybody else then gets the winning variant. The rate counts the people who opened or clicked out of the copies each variant sent, a tie goes to the earlier variant, and with nothing to compare the first variant wins. A template broadcast can only vary the subject.
  - `variants` (`object[]`, required, 2 to 4 items)
    - `subject` (`string`, 1 to 998 characters)
    - `html` (`string`, 1 to 1000000 characters)
    - `text` (`string`, 1 to 1000000 characters)
  - `testPercent` (`integer`, at least 5, at most 50, default `20`)
  - `metric` (`string`, one of `"opens"`, `"clicks"`, default `"opens"`)
  - `waitMinutes` (`integer`, at least 30, at most 4320, default `240`)
- `scheduledAt` (`string`, 3 to 64 characters): When to start: an ISO 8601 instant or a duration such as `PT2H`, in the future and at most 365 days out. Left out, the sending starts straight away.

**Returns**

- `200` `object`: An `Idempotency-Key` replayed an earlier call. This is that broadcast, not a new one.
  - `object` (`string`, one of `"broadcast"`)
  - `id` (`string`): The durable handle, `brd_` + 24 hex.
  - `status` (`string`, one of `"draft"`, `"scheduled"`, `"queued"`, `"sending"`, `"sent"`, `"cancelled"`, `"failed"`): `draft` is saved and not sent: `POST /broadcasts/{id}/send` sends it. `scheduled` waits for `scheduledAt`. `queued` waits for the sending to start. `sending` is walking the audiences, or has finished walking them while copies are still waiting to go. `sent` means every copy handed over has been sent or settled. `cancelled` was stopped by `POST /broadcasts/{id}/cancel`. `failed` means the whole broadcast stopped: the `from` address can no longer be sent from, the template stopped resolving, the plan ran out part way, the sending itself kept failing, or not one copy could be written, and `lastError` says which.
  - `mode` (`string`, one of `"live"`, `"test"`): The mode of the key that created it. A broadcast made with a test key sends its copies the way any test send goes: accepted and marked sent, and delivered to nobody.
  - `source` (`string`, one of `"api"`, `"oauth"`, `"composer"`, `"mcp"`, `"ai"`, `"form"`, `"mail-app"`, `"booking"`): Where it was started: `api` for a key, `oauth` for a connected app, `composer` for the app, `mcp` for an assistant.
  - `audienceIds` (`string[]`): The audiences it was sent to, each once.
  - `from` (`string`): The address every copy is sent from.
  - `subject` (`string`): The subject as written, merge fields and all, so it reads the same for every copy. Empty when a template supplies the subject.
  - `counts` (`BroadcastCounts`)
  - `lastError` (`string`, nullable): Why the broadcast failed, or the most recent copy that could not be written and why. Null while nothing has gone wrong.
  - `scheduledAt` (`string`, nullable, format `date-time`): When the sending is due to start. Null for a broadcast sent straight away.
  - `startedAt` (`string`, nullable, format `date-time`): When the sending reached the first people. Null until then.
  - `completedAt` (`string`, nullable, format `date-time`): When the last person was reached and every copy had been written. Copies can still be waiting to go after it, which is why `status` can read `sending` with this set.
  - `cancelledAt` (`string`, nullable, format `date-time`)
  - `createdAt` (`string`, format `date-time`)
  - `updatedAt` (`string`, format `date-time`)
  - `content` (`BroadcastContent`)
  - `replayed` (`boolean`): True when an `Idempotency-Key` replayed an earlier call, false for a new broadcast.
- `202` `object`: Accepted. The `Location` header names the broadcast; nothing has been sent yet.
  - `object` (`string`, one of `"broadcast"`)
  - `id` (`string`): The durable handle, `brd_` + 24 hex.
  - `status` (`string`, one of `"draft"`, `"scheduled"`, `"queued"`, `"sending"`, `"sent"`, `"cancelled"`, `"failed"`): `draft` is saved and not sent: `POST /broadcasts/{id}/send` sends it. `scheduled` waits for `scheduledAt`. `queued` waits for the sending to start. `sending` is walking the audiences, or has finished walking them while copies are still waiting to go. `sent` means every copy handed over has been sent or settled. `cancelled` was stopped by `POST /broadcasts/{id}/cancel`. `failed` means the whole broadcast stopped: the `from` address can no longer be sent from, the template stopped resolving, the plan ran out part way, the sending itself kept failing, or not one copy could be written, and `lastError` says which.
  - `mode` (`string`, one of `"live"`, `"test"`): The mode of the key that created it. A broadcast made with a test key sends its copies the way any test send goes: accepted and marked sent, and delivered to nobody.
  - `source` (`string`, one of `"api"`, `"oauth"`, `"composer"`, `"mcp"`, `"ai"`, `"form"`, `"mail-app"`, `"booking"`): Where it was started: `api` for a key, `oauth` for a connected app, `composer` for the app, `mcp` for an assistant.
  - `audienceIds` (`string[]`): The audiences it was sent to, each once.
  - `from` (`string`): The address every copy is sent from.
  - `subject` (`string`): The subject as written, merge fields and all, so it reads the same for every copy. Empty when a template supplies the subject.
  - `counts` (`BroadcastCounts`)
  - `lastError` (`string`, nullable): Why the broadcast failed, or the most recent copy that could not be written and why. Null while nothing has gone wrong.
  - `scheduledAt` (`string`, nullable, format `date-time`): When the sending is due to start. Null for a broadcast sent straight away.
  - `startedAt` (`string`, nullable, format `date-time`): When the sending reached the first people. Null until then.
  - `completedAt` (`string`, nullable, format `date-time`): When the last person was reached and every copy had been written. Copies can still be waiting to go after it, which is why `status` can read `sending` with this set.
  - `cancelledAt` (`string`, nullable, format `date-time`)
  - `createdAt` (`string`, format `date-time`)
  - `updatedAt` (`string`, format `date-time`)
  - `content` (`BroadcastContent`)
  - `replayed` (`boolean`): True when an `Idempotency-Key` replayed an earlier call, false for a new broadcast.

**Errors**

- `403`: `insufficient_scope` unless the key holds both `emails:send` and `audiences:read`, or `from_address_forbidden` when the key may not send as `from`.
- `404`: `audience_not_found` on `audienceIds` when an id names no audience in this workspace. Nothing is written.
- `409`: `domain_not_sendable`: the `from` domain is known to this workspace but cannot sign mail yet, the same refusal `POST /emails` gives.
- `422`: `no_recipients` on `audienceIds` when the audiences are empty or everybody in them has unsubscribed or is suppressed. `audience_is_everyone` on `audienceIds` when one of them is All contacts, the built-in audience. `invalid_parameter` or `unknown_parameter` on a field that does not validate: no body, `html` or `text` alongside `template`, no `subject` without a template, more than 10 audiences or 8 tags, or a `scheduledAt` that is not in the future or more than 365 days out. `template_not_found`, `template_not_published` and the other template refusals on `template.*`, or a prop that does not fit the template on `template.props.<name>`.
- `429`: `send_quota_exceeded`: the plan cannot cover a copy for every recipient this month. Nothing was written, and no broadcast is left behind. `rate_limited`: the key or app made more than 600 requests in 60 seconds. Wait for the seconds in `Retry-After`.
- The errors every operation can return: `400`, `401`, `500`, described in the [error catalog](https://nextmail.sh/docs/api/errors).

Also available in: TypeScript [`nextmail.broadcasts.send()`](https://nextmail.sh/docs/sdk/reference/broadcasts#send); Python [`broadcasts.send()`](https://nextmail.sh/docs/python/reference/broadcasts#send); Ruby [`broadcasts.send`](https://nextmail.sh/docs/ruby/reference/broadcasts#send); PHP [`broadcasts->send`](https://nextmail.sh/docs/php/reference/broadcasts#send); Go [`Broadcasts.Send`](https://nextmail.sh/docs/go/reference/broadcasts#send); Java [`broadcasts().send`](https://nextmail.sh/docs/java/reference/broadcasts#send); C# [`Broadcasts.SendAsync`](https://nextmail.sh/docs/csharp/reference/broadcasts#send); CLI [`nextmail broadcasts send`](https://nextmail.sh/docs/cli/reference/broadcasts#broadcasts-send); MCP [`sendToAudience`](https://nextmail.sh/docs/mcp/tools/broadcasts#sendToAudience).

### `POST /broadcasts/preview`

Count who a broadcast would reach

The numbers `POST /broadcasts` would work from, without sending anything or writing anything: how many people it would reach, how many are skipped because they have unsubscribed, and how many because their address is suppressed. A contact in several of the audiences counts once.

The count is taken at the moment of the call. Contacts who join or leave before the send starts change it.

Requires the `audiences:read` scope.

- Scopes: `audiences:read`.

**Request body**

- `audienceIds` (`string[]`, required, 1 to 10 items): 1 to 10 audience ids, the same list you would send.
- `excludeAudienceIds` (`string[]`, up to 10 items): Up to 10 audiences whose members are left out, even when they are also in one of `audienceIds`, such as the customers who already bought. All contacts may be named here.
- `segmentId` (`string`, 1 to 64 characters): A saved segment that narrows the audiences: only the contacts that match its conditions get a copy, such as the people who opened in the last 90 days. The conditions are read when the broadcast is sent or scheduled and kept with it, so editing the segment later changes nothing that is already on its way. Resending to the people who did not open leaves the segment out, because they already passed it.

**Returns**

- `200` `BroadcastPreview`: The counts. Nothing was sent.

**Errors**

- `404`: `audience_not_found` on `audienceIds` when an id names no audience in this workspace.
- `422`: `audience_is_everyone` on `audienceIds` when one of them is All contacts, the built-in audience, which no broadcast can go to. `invalid_parameter` on `audienceIds` when it is empty or holds more than 10 ids, `unknown_parameter` for any other key in the body.
- The errors every operation can return: `400`, `401`, `403`, `429`, `500`, described in the [error catalog](https://nextmail.sh/docs/api/errors).

Also available in: TypeScript [`nextmail.broadcasts.preview()`](https://nextmail.sh/docs/sdk/reference/broadcasts#preview); Python [`broadcasts.preview()`](https://nextmail.sh/docs/python/reference/broadcasts#preview); Ruby [`broadcasts.preview`](https://nextmail.sh/docs/ruby/reference/broadcasts#preview); PHP [`broadcasts->preview`](https://nextmail.sh/docs/php/reference/broadcasts#preview); Go [`Broadcasts.Preview`](https://nextmail.sh/docs/go/reference/broadcasts#preview); Java [`broadcasts().preview`](https://nextmail.sh/docs/java/reference/broadcasts#preview); C# [`Broadcasts.PreviewAsync`](https://nextmail.sh/docs/csharp/reference/broadcasts#preview); CLI [`nextmail broadcasts preview`](https://nextmail.sh/docs/cli/reference/broadcasts#broadcasts-preview); MCP [`previewAudienceSend`](https://nextmail.sh/docs/mcp/tools/broadcasts#previewAudienceSend).

### `GET /broadcasts/analytics`

Broadcast analytics across broadcasts

The numbers behind the Analytics tab of the Broadcasts page, in one request: how the live broadcasts sent inside a window did, added up and as a series cut to `grain`, plus one row per broadcast so they can be compared.

A copy counts when it was sent, or written if it has not gone yet, inside the window, and everything that happened to it afterwards counts with it, so an open today of a copy sent last week is in a 30 day window but not in a 1 day one. Test mode broadcasts are left out. `totals` and `series` cover the broadcasts in `broadcastIds`, or every one when it is left out, and `broadcasts` always lists every broadcast in the window. The series counts each person once, at the first time it happened to them.

A key limited to particular addresses or domains reads only broadcasts sent from an address or domain it holds, and the rest are left out as if they did not exist.

Requires the `emails:read` scope.

- Scopes: `emails:read`.

**Query parameters**

- `broadcastIds` (`string`, up to 4000 characters): Comma separated broadcast ids, at most 50, that `totals` and `series` add up. Leave it out for every broadcast in the window. A repeated id counts once, and one with no copy in the window adds nothing.
- `days` (`integer`, at least 1, at most 1095, default `30`): How far back to look. The window starts at the beginning of its first bucket in the offset you asked for, so the oldest bucket is a whole one, and ends now.
- `minutes` (`integer`, at least 1, at most 1576800): The window in minutes, which wins over `days` when both are sent.
- `grain` (`string`, one of `"minute"`, `"hour"`, `"day"`, default `"day"`): How wide one bucket is, and the shape of its key: `YYYY-MM-DD` for a day, `YYYY-MM-DDTHH` for an hour, `YYYY-MM-DDTHH:MM` for a minute.
- `offsetMinutes` (`integer`, at least -840, at most 840, default `0`): Minutes east of UTC to cut the buckets in. Pass `-new Date().getTimezoneOffset()` for the local zone.

**Returns**

- `200` `BroadcastAnalytics`: The window, the totals, the series and every broadcast in the window.

**Errors**

- `422`: `invalid_parameter` on `broadcastIds` for more than 50 ids, on `days`, `minutes` or `offsetMinutes` out of range, or on an unknown `grain`.
- The errors every operation can return: `400`, `401`, `403`, `404`, `429`, `500`, described in the [error catalog](https://nextmail.sh/docs/api/errors).

Also available in: TypeScript [`nextmail.broadcasts.analytics()`](https://nextmail.sh/docs/sdk/reference/broadcasts#analytics); Python [`broadcasts.analytics()`](https://nextmail.sh/docs/python/reference/broadcasts#analytics); Ruby [`broadcasts.analytics`](https://nextmail.sh/docs/ruby/reference/broadcasts#analytics); PHP [`broadcasts->analytics`](https://nextmail.sh/docs/php/reference/broadcasts#analytics); Go [`Broadcasts.Analytics`](https://nextmail.sh/docs/go/reference/broadcasts#analytics); Java [`broadcasts().analytics`](https://nextmail.sh/docs/java/reference/broadcasts#analytics); C# [`Broadcasts.AnalyticsAsync`](https://nextmail.sh/docs/csharp/reference/broadcasts#analytics); CLI [`nextmail broadcasts analytics`](https://nextmail.sh/docs/cli/reference/broadcasts#broadcasts-analytics); MCP [`getBroadcastAnalytics`](https://nextmail.sh/docs/mcp/tools/broadcasts#getBroadcastAnalytics).

### `GET /broadcasts/{id}`

Retrieve a broadcast

One broadcast with `counts` read live from its copies, which makes this the call to poll while it sends. `status` settles on `sent` once every copy handed over has gone out or failed, and on `failed` or `cancelled` when it stopped early.

A key limited to particular addresses or domains reads only broadcasts sent from an address or domain it holds. Any other broadcast answers 404 `broadcast_not_found`, as if it did not exist.

Requires the `emails:read` scope.

- Scopes: `emails:read`.

**Path parameters**

- `id` (`string`, required): A `brd_` id from `POST /broadcasts` or `GET /broadcasts`.

**Returns**

- `200` `Broadcast`: The broadcast.

**Errors**

- `404`: `broadcast_not_found` when the id names no broadcast in this workspace.
- The errors every operation can return: `400`, `401`, `403`, `422`, `429`, `500`, described in the [error catalog](https://nextmail.sh/docs/api/errors).

Also available in: TypeScript [`nextmail.broadcasts.get()`](https://nextmail.sh/docs/sdk/reference/broadcasts#get); Python [`broadcasts.get()`](https://nextmail.sh/docs/python/reference/broadcasts#get); Ruby [`broadcasts.get`](https://nextmail.sh/docs/ruby/reference/broadcasts#get); PHP [`broadcasts->get`](https://nextmail.sh/docs/php/reference/broadcasts#get); Go [`Broadcasts.Get`](https://nextmail.sh/docs/go/reference/broadcasts#get); Java [`broadcasts().get`](https://nextmail.sh/docs/java/reference/broadcasts#get); C# [`Broadcasts.GetAsync`](https://nextmail.sh/docs/csharp/reference/broadcasts#get); CLI [`nextmail broadcasts get`](https://nextmail.sh/docs/cli/reference/broadcasts#broadcasts-get); MCP [`getBroadcast`](https://nextmail.sh/docs/mcp/tools/broadcasts#getBroadcast).

### `PATCH /broadcasts/{id}`

Update a draft or a scheduled broadcast

A partial update: a field left out keeps what it holds, and `null` clears `replyTo`, `html`, `text`, `template` or `utm`. Sending any of `html`, `text` or `template` replaces the body as a whole.

A `draft` takes every change and stays a draft. A `scheduled` broadcast takes changes until its sending starts, each checked the way `POST /broadcasts` checks a new one, and `counts.recipients` is counted again. `scheduledAt` moves a scheduled broadcast to another time. Any other broadcast answers 409 `broadcast_not_editable`.

A key limited to particular addresses or domains changes only broadcasts sent from an address or domain it holds.

Requires the `emails:send` and `audiences:read` scopes.

- Scopes: `emails:send`, `audiences:read`.

**Path parameters**

- `id` (`string`, required): A `brd_` id from `POST /broadcasts` or `GET /broadcasts`.

**Request body**

- `audienceIds` (`string[]`, up to 10 items): The new audiences, replacing the old list.
- `excludeAudienceIds` (`string[]`, up to 10 items): Up to 10 audiences whose members are left out, even when they are also in one of `audienceIds`, such as the customers who already bought. All contacts may be named here.
- `segmentId` (`string`, nullable, 1 to 64 characters): A saved segment that narrows the audiences: only the contacts that match its conditions get a copy, such as the people who opened in the last 90 days. The conditions are read when the broadcast is sent or scheduled and kept with it, so editing the segment later changes nothing that is already on its way. Resending to the people who did not open leaves the segment out, because they already passed it. Null takes it away.
- `from` (`string | object`): A new sender the key may send as.
  - `email` (`string`, required, up to 320 characters)
  - `name` (`string`, up to 128 characters)
- `replyTo` (`string | object`, nullable): Where replies go. Null takes it away.
  - `email` (`string`, required, up to 320 characters)
  - `name` (`string`, up to 128 characters)
- `subject` (`string`, up to 998 characters): A new subject, with merge fields.
- `html` (`string`, nullable, up to 1000000 characters): A new HTML body. Sending `html`, `text` or `template` replaces the body as a whole.
- `text` (`string`, nullable, up to 1000000 characters): A new plain text body.
- `template` (`object`, nullable): A stored template instead of `html` and `text`. Null takes it away.
  - `id` (`string`, required, 1 to 128 characters)
  - `version` (`integer`, more than 0, at most 100000)
  - `props` (`Record<string, any>`)
  - `slots` (`Record<string, any>`)
- `tracking` (`object`): Open and link tracking for every copy.
  - `opens` (`boolean`)
  - `clicks` (`boolean`)
- `tags` (`Record<string, string>`): The new tags, replacing the old ones, up to 8.
- `utm` (`object`, nullable): Campaign tags added to every http and https link in the body: `source`, `medium`, `campaign`, `term` and `content` become `utm_source` and the rest, so the visits show up in your web analytics. A link that already carries one keeps its own value, and the unsubscribe link never gets them. Values are 1 to 100 characters, `campaign` up to 200. Null takes them away.
  - `source` (`string`, 1 to 100 characters)
  - `medium` (`string`, 1 to 100 characters)
  - `campaign` (`string`, 1 to 200 characters)
  - `term` (`string`, 1 to 100 characters)
  - `content` (`string`, 1 to 100 characters)
- `abTest` (`object`, nullable): An A/B test. `variants` holds 2 to 4 versions, each a `subject`, or a body as `html` and or `text`, that replaces the broadcast's own, so `{}` keeps the broadcast as it is. `testPercent` (5 to 50, 20 unless set) of the people get a variant, split evenly between them, and `metric` (`opens` unless set, or `clicks`) picks the winner once `waitMinutes` (30 to 4320, 240 unless set) have passed since the test went out. Everybody else then gets the winning variant. The rate counts the people who opened or clicked out of the copies each variant sent, a tie goes to the earlier variant, and with nothing to compare the first variant wins. A template broadcast can only vary the subject. Null takes it away.
  - `variants` (`object[]`, required, 2 to 4 items)
    - `subject` (`string`, 1 to 998 characters)
    - `html` (`string`, 1 to 1000000 characters)
    - `text` (`string`, 1 to 1000000 characters)
  - `testPercent` (`integer`, at least 5, at most 50, default `20`)
  - `metric` (`string`, one of `"opens"`, `"clicks"`, default `"opens"`)
  - `waitMinutes` (`integer`, at least 30, at most 4320, default `240`)
- `scheduledAt` (`string`, 3 to 64 characters): A new start for a scheduled broadcast, in the future and at most 365 days out. A draft refuses it: give the time to `POST /broadcasts/{id}/send`.

**Returns**

- `200` `Broadcast`: The broadcast as it stands after the change, with its `content`.

**Errors**

- `403`: `from_address_forbidden` when the key may not send as `from`.
- `404`: `broadcast_not_found` when the id names no broadcast in this workspace, `audience_not_found` on an audience id.
- `409`: `broadcast_not_editable`: the broadcast is queued, sending or finished. `stream_paused` while the broadcast lane of the workspace is paused.
- `422`: `no_recipients` when a scheduled broadcast would reach nobody, `invalid_parameter` on `scheduledAt` for a draft, which is scheduled when it is sent, or on a field that does not validate.
- `429`: `send_quota_exceeded`: the plan cannot cover the scheduled broadcast after the change. `rate_limited`: the key or app made more than 600 requests in 60 seconds. Wait for the seconds in `Retry-After`.
- The errors every operation can return: `400`, `401`, `500`, described in the [error catalog](https://nextmail.sh/docs/api/errors).

Also available in: TypeScript [`nextmail.broadcasts.update()`](https://nextmail.sh/docs/sdk/reference/broadcasts#update); Python [`broadcasts.update()`](https://nextmail.sh/docs/python/reference/broadcasts#update); Ruby [`broadcasts.update`](https://nextmail.sh/docs/ruby/reference/broadcasts#update); PHP [`broadcasts->update`](https://nextmail.sh/docs/php/reference/broadcasts#update); Go [`Broadcasts.Update`](https://nextmail.sh/docs/go/reference/broadcasts#update); Java [`broadcasts().update`](https://nextmail.sh/docs/java/reference/broadcasts#update); C# [`Broadcasts.UpdateAsync`](https://nextmail.sh/docs/csharp/reference/broadcasts#update); CLI [`nextmail broadcasts update`](https://nextmail.sh/docs/cli/reference/broadcasts#broadcasts-update); MCP [`updateBroadcast`](https://nextmail.sh/docs/mcp/tools/broadcasts#updateBroadcast).

### `GET /broadcasts/{id}/recipients`

List the recipients of a broadcast

Everybody the broadcast went to, one row per copy, sorted by address: the copy id, its status, when it was sent and delivered, whether it bounced or was reported as spam, how often it was opened and clicked, and whether the person unsubscribed after it went out.

Opens and clicks leave out image proxies and link scanners, and stay 0 when the broadcast was sent with tracking off. Keyset paging: pass `nextCursor` back as `cursor`, with the same `filter` and `q`.

A key limited to particular addresses or domains reads only broadcasts sent from an address or domain it holds. Any other broadcast answers 404 `broadcast_not_found`, as if it did not exist.

Requires the `emails:read` scope.

- Scopes: `emails:read`.

**Path parameters**

- `id` (`string`, required): A `brd_` id from `POST /broadcasts` or `GET /broadcasts`.

**Query parameters**

- `filter` (`string`, one of `"pending"`, `"sent"`, `"delivered"`, `"opened"`, `"not_opened"`, `"clicked"`, `"bounced"`, `"complained"`, `"failed"`, `"unsubscribed"`): Keeps one group: `pending` (still queued or sending), `sent`, `delivered`, `opened`, `not_opened` (sent and never opened), `clicked`, `bounced`, `complained`, `failed` (failed or cancelled) or `unsubscribed`.
- `q` (`string`, up to 200 characters): Searches the address and the name, ignoring case.
- `limit` (`integer`, at least 1, at most 200, default `50`): Rows per page, 1 to 200.
- `cursor` (`string`): The previous page's `nextCursor`, passed back as it came. It is opaque: it holds where the last row sat in this list's order, so a row deleted or edited between pages never breaks the walk, and the next page starts at the first row that sorts after it. A value this list did not hand out is a 400 `invalid_cursor`.

**Returns**

- `200` `BroadcastRecipientList`: A page of recipients, by address.

**Errors**

- `404`: `broadcast_not_found` when the id names no broadcast in this workspace.
- The errors every operation can return: `400`, `401`, `403`, `422`, `429`, `500`, described in the [error catalog](https://nextmail.sh/docs/api/errors).

Also available in: TypeScript [`nextmail.broadcasts.listRecipients()`](https://nextmail.sh/docs/sdk/reference/broadcasts#listRecipients), [`nextmail.broadcasts.listAllRecipients()`](https://nextmail.sh/docs/sdk/reference/broadcasts#listAllRecipients), [`nextmail.broadcasts.iterateRecipients()`](https://nextmail.sh/docs/sdk/reference/broadcasts#iterateRecipients); Python [`broadcasts.list_recipients()`](https://nextmail.sh/docs/python/reference/broadcasts#listRecipients), [`broadcasts.list_all_recipients()`](https://nextmail.sh/docs/python/reference/broadcasts#listAllRecipients), [`broadcasts.iterate_recipients()`](https://nextmail.sh/docs/python/reference/broadcasts#iterateRecipients); Ruby [`broadcasts.list_recipients`](https://nextmail.sh/docs/ruby/reference/broadcasts#listRecipients), [`broadcasts.list_all_recipients`](https://nextmail.sh/docs/ruby/reference/broadcasts#listAllRecipients), [`broadcasts.iterate_recipients`](https://nextmail.sh/docs/ruby/reference/broadcasts#iterateRecipients); PHP [`broadcasts->listRecipients`](https://nextmail.sh/docs/php/reference/broadcasts#listRecipients), [`broadcasts->listAllRecipients`](https://nextmail.sh/docs/php/reference/broadcasts#listAllRecipients), [`broadcasts->iterateRecipients`](https://nextmail.sh/docs/php/reference/broadcasts#iterateRecipients); Go [`Broadcasts.ListRecipients`](https://nextmail.sh/docs/go/reference/broadcasts#listRecipients), [`Broadcasts.ListAllRecipients`](https://nextmail.sh/docs/go/reference/broadcasts#listAllRecipients), [`Broadcasts.IterateRecipients`](https://nextmail.sh/docs/go/reference/broadcasts#iterateRecipients); Java [`broadcasts().listRecipients`](https://nextmail.sh/docs/java/reference/broadcasts#listRecipients), [`broadcasts().listAllRecipients`](https://nextmail.sh/docs/java/reference/broadcasts#listAllRecipients), [`broadcasts().iterateRecipients`](https://nextmail.sh/docs/java/reference/broadcasts#iterateRecipients); C# [`Broadcasts.ListRecipientsAsync`](https://nextmail.sh/docs/csharp/reference/broadcasts#listRecipients), [`Broadcasts.ListAllRecipientsAsync`](https://nextmail.sh/docs/csharp/reference/broadcasts#listAllRecipients), [`Broadcasts.IterateRecipientsAsync`](https://nextmail.sh/docs/csharp/reference/broadcasts#iterateRecipients); CLI [`nextmail broadcasts list-recipients`](https://nextmail.sh/docs/cli/reference/broadcasts#broadcasts-list-recipients); MCP [`listBroadcastRecipients`](https://nextmail.sh/docs/mcp/tools/broadcasts#listBroadcastRecipients).

### `GET /broadcasts/{id}/recipients/{emailId}`

Retrieve one copy of a broadcast

One person's copy: the same row `GET /broadcasts/{id}/recipients` lists, plus the subject, HTML and text exactly as that person received them, with the merge fields filled in and their own unsubscribe link. The HTML is from before open and click tracking was added.

A key limited to particular addresses or domains reads only broadcasts sent from an address or domain it holds. Any other broadcast answers 404 `broadcast_not_found`, as if it did not exist.

Requires the `emails:read` scope.

- Scopes: `emails:read`.

**Path parameters**

- `id` (`string`, required): A `brd_` id from `POST /broadcasts` or `GET /broadcasts`.
- `emailId` (`string`, required): The `emailId` of the copy, from the recipients list.

**Returns**

- `200` `BroadcastRecipientContent`: The copy and its content.

**Errors**

- `404`: `broadcast_not_found` for an unknown broadcast, `recipient_not_found` for a copy that is not part of it.
- The errors every operation can return: `400`, `401`, `403`, `422`, `429`, `500`, described in the [error catalog](https://nextmail.sh/docs/api/errors).

Also available in: TypeScript [`nextmail.broadcasts.getRecipient()`](https://nextmail.sh/docs/sdk/reference/broadcasts#getRecipient); Python [`broadcasts.get_recipient()`](https://nextmail.sh/docs/python/reference/broadcasts#getRecipient); Ruby [`broadcasts.get_recipient`](https://nextmail.sh/docs/ruby/reference/broadcasts#getRecipient); PHP [`broadcasts->getRecipient`](https://nextmail.sh/docs/php/reference/broadcasts#getRecipient); Go [`Broadcasts.GetRecipient`](https://nextmail.sh/docs/go/reference/broadcasts#getRecipient); Java [`broadcasts().getRecipient`](https://nextmail.sh/docs/java/reference/broadcasts#getRecipient); C# [`Broadcasts.GetRecipientAsync`](https://nextmail.sh/docs/csharp/reference/broadcasts#getRecipient); CLI [`nextmail broadcasts get-recipient`](https://nextmail.sh/docs/cli/reference/broadcasts#broadcasts-get-recipient); MCP [`getBroadcastRecipient`](https://nextmail.sh/docs/mcp/tools/broadcasts#getBroadcastRecipient).

### `GET /broadcasts/{id}/stats`

Broadcast statistics

How the broadcast performed: copies sent, delivered, bounced, reported as spam and failed, and how many people opened, clicked and unsubscribed, as totals and as a series cut to `grain`. The series counts each person once, at the first time it happened to them, so it adds up to the totals.

Send `days` or `minutes` to also read what happened lately: `window` then counts the copies delivered, bounced, reported, opened, clicked and unsubscribed inside it, and `series` keeps only its buckets. `totals` always covers the whole broadcast.

A key limited to particular addresses or domains reads only broadcasts sent from an address or domain it holds. Any other broadcast answers 404 `broadcast_not_found`, as if it did not exist.

Requires the `emails:read` scope.

- Scopes: `emails:read`.

**Path parameters**

- `id` (`string`, required): A `brd_` id from `POST /broadcasts` or `GET /broadcasts`.

**Query parameters**

- `grain` (`string`, one of `"minute"`, `"hour"`, `"day"`, default `"hour"`): Bucket width of the series.
- `offsetMinutes` (`integer`, at least -840, at most 840, default `0`): Minutes east of UTC to cut the buckets in. Pass `-new Date().getTimezoneOffset()` for the local zone.
- `days` (`integer`, at least 1, at most 1095): Reads a window too: how far back it reaches. It starts at the beginning of its first `grain` bucket and ends now. Leave it and `minutes` out for no window.
- `minutes` (`integer`, at least 1, at most 1576800): The window in minutes, which wins over `days` when both are sent.

**Returns**

- `200` `BroadcastStats`: Totals, the window and the series.

**Errors**

- `404`: `broadcast_not_found` when the id names no broadcast in this workspace.
- `422`: `invalid_parameter` on `days`, `minutes` or `offsetMinutes` out of range, or on an unknown `grain`.
- The errors every operation can return: `400`, `401`, `403`, `429`, `500`, described in the [error catalog](https://nextmail.sh/docs/api/errors).

Also available in: TypeScript [`nextmail.broadcasts.stats()`](https://nextmail.sh/docs/sdk/reference/broadcasts#stats); Python [`broadcasts.stats()`](https://nextmail.sh/docs/python/reference/broadcasts#stats); Ruby [`broadcasts.stats`](https://nextmail.sh/docs/ruby/reference/broadcasts#stats); PHP [`broadcasts->stats`](https://nextmail.sh/docs/php/reference/broadcasts#stats); Go [`Broadcasts.Stats`](https://nextmail.sh/docs/go/reference/broadcasts#stats); Java [`broadcasts().stats`](https://nextmail.sh/docs/java/reference/broadcasts#stats); C# [`Broadcasts.StatsAsync`](https://nextmail.sh/docs/csharp/reference/broadcasts#stats); CLI [`nextmail broadcasts stats`](https://nextmail.sh/docs/cli/reference/broadcasts#broadcasts-stats); MCP [`getBroadcastStats`](https://nextmail.sh/docs/mcp/tools/broadcasts#getBroadcastStats).

### `POST /broadcasts/{id}/cancel`

Cancel a broadcast

Stops a broadcast that is `scheduled`, `queued` or `sending`, including one that has reached everybody while some copies are still waiting to go. Nobody else is added, and every copy still waiting to go is cancelled. A copy already being handed over finishes, and copies that have gone cannot be recalled, so `counts.sent` keeps them. No body.

Cancelling a cancelled broadcast answers it as it stands, so a retry is safe.

A key limited to particular addresses or domains cancels only broadcasts sent from an address or domain it holds. Any other broadcast answers 404 `broadcast_not_found`, as if it did not exist.

Requires the `emails:send` scope.

- Scopes: `emails:send`.

**Path parameters**

- `id` (`string`, required): A `brd_` id from `POST /broadcasts` or `GET /broadcasts`.

**Returns**

- `200` `Broadcast`: The broadcast, now `cancelled`.

**Errors**

- `404`: `broadcast_not_found` when the id names no broadcast in this workspace.
- `409`: `broadcast_not_cancellable`: every copy has already gone out, or the broadcast `failed`.
- The errors every operation can return: `400`, `401`, `403`, `422`, `429`, `500`, described in the [error catalog](https://nextmail.sh/docs/api/errors).

Also available in: TypeScript [`nextmail.broadcasts.cancel()`](https://nextmail.sh/docs/sdk/reference/broadcasts#cancel); Python [`broadcasts.cancel()`](https://nextmail.sh/docs/python/reference/broadcasts#cancel); Ruby [`broadcasts.cancel`](https://nextmail.sh/docs/ruby/reference/broadcasts#cancel); PHP [`broadcasts->cancel`](https://nextmail.sh/docs/php/reference/broadcasts#cancel); Go [`Broadcasts.Cancel`](https://nextmail.sh/docs/go/reference/broadcasts#cancel); Java [`broadcasts().cancel`](https://nextmail.sh/docs/java/reference/broadcasts#cancel); C# [`Broadcasts.CancelAsync`](https://nextmail.sh/docs/csharp/reference/broadcasts#cancel); CLI [`nextmail broadcasts cancel`](https://nextmail.sh/docs/cli/reference/broadcasts#broadcasts-cancel); MCP [`cancelBroadcast`](https://nextmail.sh/docs/mcp/tools/broadcasts#cancelBroadcast).

### `POST /broadcasts/drafts`

Save a broadcast draft

Saves a broadcast with `status` `draft`. Nothing is sent, nothing counts against the plan and the audiences may still be empty, so a draft can be written over several sittings. `PATCH /broadcasts/{id}` changes it, `POST /broadcasts/{id}/send` sends or schedules it, and `GET /broadcasts/{id}` returns what it holds in `content`.

The body takes the fields `POST /broadcasts` takes, except `scheduledAt`, and only `from` is required. The sender, the audiences and a template are checked as they are saved.

Requires the `emails:send` and `audiences:read` scopes.

- Scopes: `emails:send`, `audiences:read`.

**Request body**

- `audienceIds` (`string[]`, up to 10 items, default `[]`): Up to 10 audience ids. May be empty in a draft, but not when it is sent.
- `excludeAudienceIds` (`string[]`, up to 10 items): Up to 10 audiences whose members are left out, even when they are also in one of `audienceIds`, such as the customers who already bought. All contacts may be named here.
- `segmentId` (`string`, 1 to 64 characters): A saved segment that narrows the audiences: only the contacts that match its conditions get a copy, such as the people who opened in the last 90 days. The conditions are read when the broadcast is sent or scheduled and kept with it, so editing the segment later changes nothing that is already on its way. Resending to the people who did not open leaves the segment out, because they already passed it.
- `from` (`string | object`, required): The sender, an address the key may send as, bare or with a name.
  - `email` (`string`, required, up to 320 characters)
  - `name` (`string`, up to 128 characters)
- `replyTo` (`string | object`): Where replies go, the same for every copy.
  - `email` (`string`, required, up to 320 characters)
  - `name` (`string`, up to 128 characters)
- `subject` (`string`, up to 998 characters, default `""`): At most 998 characters, with merge fields. Needed before sending unless a template supplies it.
- `html` (`string`, up to 1000000 characters): HTML body, at most 1,000,000 characters, with merge fields.
- `text` (`string`, up to 1000000 characters): Plain text body, at most 1,000,000 characters, with merge fields.
- `template` (`object`): A stored template instead of `html` and `text`, never beside them.
  - `id` (`string`, required, 1 to 128 characters)
  - `version` (`integer`, more than 0, at most 100000)
  - `props` (`Record<string, any>`)
  - `slots` (`Record<string, any>`)
- `tracking` (`object`): Open and link tracking for every copy.
  - `opens` (`boolean`)
  - `clicks` (`boolean`)
- `tags` (`Record<string, string>`, default `{}`): Up to 8 tags every copy carries.
- `utm` (`object`): Campaign tags added to every http and https link in the body: `source`, `medium`, `campaign`, `term` and `content` become `utm_source` and the rest, so the visits show up in your web analytics. A link that already carries one keeps its own value, and the unsubscribe link never gets them. Values are 1 to 100 characters, `campaign` up to 200.
  - `source` (`string`, 1 to 100 characters)
  - `medium` (`string`, 1 to 100 characters)
  - `campaign` (`string`, 1 to 200 characters)
  - `term` (`string`, 1 to 100 characters)
  - `content` (`string`, 1 to 100 characters)
- `abTest` (`object`): An A/B test. `variants` holds 2 to 4 versions, each a `subject`, or a body as `html` and or `text`, that replaces the broadcast's own, so `{}` keeps the broadcast as it is. `testPercent` (5 to 50, 20 unless set) of the people get a variant, split evenly between them, and `metric` (`opens` unless set, or `clicks`) picks the winner once `waitMinutes` (30 to 4320, 240 unless set) have passed since the test went out. Everybody else then gets the winning variant. The rate counts the people who opened or clicked out of the copies each variant sent, a tie goes to the earlier variant, and with nothing to compare the first variant wins. A template broadcast can only vary the subject.
  - `variants` (`object[]`, required, 2 to 4 items)
    - `subject` (`string`, 1 to 998 characters)
    - `html` (`string`, 1 to 1000000 characters)
    - `text` (`string`, 1 to 1000000 characters)
  - `testPercent` (`integer`, at least 5, at most 50, default `20`)
  - `metric` (`string`, one of `"opens"`, `"clicks"`, default `"opens"`)
  - `waitMinutes` (`integer`, at least 30, at most 4320, default `240`)

**Returns**

- `201` `Broadcast`: The draft, with its `content`. The `Location` header names it. Nothing was sent.

**Errors**

- `403`: `insufficient_scope` unless the key holds both `emails:send` and `audiences:read`, or `from_address_forbidden` when the key may not send as `from`.
- `404`: `audience_not_found` on `audienceIds` or `excludeAudienceIds`.
- `422`: `audience_is_everyone` when `audienceIds` names All contacts, `invalid_parameter` or `unknown_parameter` on a field that does not validate, and the template refusals on `template.*`.
- The errors every operation can return: `400`, `401`, `429`, `500`, described in the [error catalog](https://nextmail.sh/docs/api/errors).

Also available in: TypeScript [`nextmail.broadcasts.createDraft()`](https://nextmail.sh/docs/sdk/reference/broadcasts#createDraft); Python [`broadcasts.create_draft()`](https://nextmail.sh/docs/python/reference/broadcasts#createDraft); Ruby [`broadcasts.create_draft`](https://nextmail.sh/docs/ruby/reference/broadcasts#createDraft); PHP [`broadcasts->createDraft`](https://nextmail.sh/docs/php/reference/broadcasts#createDraft); Go [`Broadcasts.CreateDraft`](https://nextmail.sh/docs/go/reference/broadcasts#createDraft); Java [`broadcasts().createDraft`](https://nextmail.sh/docs/java/reference/broadcasts#createDraft); C# [`Broadcasts.CreateDraftAsync`](https://nextmail.sh/docs/csharp/reference/broadcasts#createDraft); CLI [`nextmail broadcasts create-draft`](https://nextmail.sh/docs/cli/reference/broadcasts#broadcasts-create-draft); MCP [`createBroadcastDraft`](https://nextmail.sh/docs/mcp/tools/broadcasts#createBroadcastDraft).

### `POST /broadcasts/test`

Send a test of a broadcast

Sends one copy of a broadcast to up to 5 addresses, to read it before it goes out. Without `to` it goes to the account address of the person the key or app acts for, the workspace owner for an API key. The merge fields are filled from the contact that has the address when there is one and from a sample name otherwise, the subject starts with `[Test]`, the unsubscribe footer is added as it would be and its link does nothing. Opens and clicks are not tracked, and `utm` is applied to the links.

No broadcast is created and nobody joins an audience. Each copy counts as one send.

Requires the `emails:send` scope.

- Scopes: `emails:send`.

**Request body**

- `from` (`string | object`, required): The sender, an address the key may send as.
  - `email` (`string`, required, up to 320 characters)
  - `name` (`string`, up to 128 characters)
- `replyTo` (`string | object`): Where replies go.
  - `email` (`string`, required, up to 320 characters)
  - `name` (`string`, up to 128 characters)
- `subject` (`string`, up to 998 characters, default `""`): Required unless a template supplies it, with merge fields.
- `html` (`string`, up to 1000000 characters): HTML body with merge fields.
- `text` (`string`, up to 1000000 characters): Plain text body with merge fields.
- `template` (`object`): A stored template instead of `html` and `text`.
  - `id` (`string`, required, 1 to 128 characters)
  - `version` (`integer`, more than 0, at most 100000)
  - `props` (`Record<string, any>`)
  - `slots` (`Record<string, any>`)
- `utm` (`object`): Campaign tags added to every http and https link in the body: `source`, `medium`, `campaign`, `term` and `content` become `utm_source` and the rest, so the visits show up in your web analytics. A link that already carries one keeps its own value, and the unsubscribe link never gets them. Values are 1 to 100 characters, `campaign` up to 200.
  - `source` (`string`, 1 to 100 characters)
  - `medium` (`string`, 1 to 100 characters)
  - `campaign` (`string`, 1 to 200 characters)
  - `term` (`string`, 1 to 100 characters)
  - `content` (`string`, 1 to 100 characters)
- `to` (`(string | object)[]`, 1 to 5 items): 1 to 5 addresses. Left out, the test goes to the account address of the person the key or app acts for.
  - `email` (`string`, required, up to 320 characters)
  - `name` (`string`, up to 128 characters)

**Returns**

- `200` `BroadcastTest`: The copies that were sent.

**Errors**

- `403`: `from_address_forbidden` when the key may not send as `from`.
- `409`: `domain_not_sendable`: the `from` domain cannot sign mail yet.
- `422`: `invalid_parameter` on a field that does not validate: no body, `html` or `text` beside `template`, no `subject` without a template, or more than 5 addresses in `to`. The template refusals on `template.*`.
- `429`: `send_quota_exceeded`: the plan has no sends left this month. `rate_limited`: the key or app made more than 600 requests in 60 seconds. Wait for the seconds in `Retry-After`.
- The errors every operation can return: `400`, `401`, `404`, `500`, described in the [error catalog](https://nextmail.sh/docs/api/errors).

Also available in: TypeScript [`nextmail.broadcasts.sendTest()`](https://nextmail.sh/docs/sdk/reference/broadcasts#sendTest); Python [`broadcasts.send_test()`](https://nextmail.sh/docs/python/reference/broadcasts#sendTest); Ruby [`broadcasts.send_test`](https://nextmail.sh/docs/ruby/reference/broadcasts#sendTest); PHP [`broadcasts->sendTest`](https://nextmail.sh/docs/php/reference/broadcasts#sendTest); Go [`Broadcasts.SendTest`](https://nextmail.sh/docs/go/reference/broadcasts#sendTest); Java [`broadcasts().sendTest`](https://nextmail.sh/docs/java/reference/broadcasts#sendTest); C# [`Broadcasts.SendTestAsync`](https://nextmail.sh/docs/csharp/reference/broadcasts#sendTest); CLI [`nextmail broadcasts send-test`](https://nextmail.sh/docs/cli/reference/broadcasts#broadcasts-send-test); MCP [`sendBroadcastTest`](https://nextmail.sh/docs/mcp/tools/broadcasts#sendBroadcastTest).

### `POST /broadcasts/{id}/send`

Send a draft

Turns a draft into a broadcast that goes out: `queued` straight away, or `scheduled` with `scheduledAt`. The draft is checked the way `POST /broadcasts` checks a new broadcast: it needs an audience, a body, a subject or a template that supplies one, somebody to reach and room in the plan for every copy. A broadcast that is not a draft answers 409 `broadcast_not_draft`, so a repeated call never sends twice. The body is optional.

Requires the `emails:send` and `audiences:read` scopes.

- Scopes: `emails:send`, `audiences:read`.

**Path parameters**

- `id` (`string`, required): A `brd_` id from `POST /broadcasts` or `GET /broadcasts`.

**Request body**

- `scheduledAt` (`string`, 3 to 64 characters): When to start: an ISO 8601 instant or a duration such as `PT2H`, in the future and at most 365 days out. Left out, the sending starts straight away.

**Returns**

- `202` `Broadcast`: Accepted. The broadcast is `queued` or `scheduled`, and nothing has been sent yet.

**Errors**

- `403`: `from_address_forbidden` when the key may no longer send as the draft's `from`.
- `404`: `broadcast_not_found`, or `audience_not_found` when an audience of the draft is gone.
- `409`: `broadcast_not_draft`, `stream_paused` while the broadcast lane is paused, or `domain_not_sendable`.
- `422`: `no_recipients` when the draft has no audience or nobody in it can be sent to, `invalid_parameter` when it has no body or no subject, or on a `scheduledAt` that is not in the future or more than 365 days out.
- `429`: `send_quota_exceeded`: the plan cannot cover a copy for every recipient this month. `rate_limited`: the key or app made more than 600 requests in 60 seconds. Wait for the seconds in `Retry-After`.
- The errors every operation can return: `400`, `401`, `500`, described in the [error catalog](https://nextmail.sh/docs/api/errors).

Also available in: TypeScript [`nextmail.broadcasts.sendDraft()`](https://nextmail.sh/docs/sdk/reference/broadcasts#sendDraft); Python [`broadcasts.send_draft()`](https://nextmail.sh/docs/python/reference/broadcasts#sendDraft); Ruby [`broadcasts.send_draft`](https://nextmail.sh/docs/ruby/reference/broadcasts#sendDraft); PHP [`broadcasts->sendDraft`](https://nextmail.sh/docs/php/reference/broadcasts#sendDraft); Go [`Broadcasts.SendDraft`](https://nextmail.sh/docs/go/reference/broadcasts#sendDraft); Java [`broadcasts().sendDraft`](https://nextmail.sh/docs/java/reference/broadcasts#sendDraft); C# [`Broadcasts.SendDraftAsync`](https://nextmail.sh/docs/csharp/reference/broadcasts#sendDraft); CLI [`nextmail broadcasts send-draft`](https://nextmail.sh/docs/cli/reference/broadcasts#broadcasts-send-draft); MCP [`sendBroadcastDraft`](https://nextmail.sh/docs/mcp/tools/broadcasts#sendBroadcastDraft).

### `POST /broadcasts/{id}/duplicate`

Duplicate a broadcast

Copies any broadcast into a new draft: the same sender, audiences, left-out audiences, subject, body or template, tracking, tags and campaign tags, without the time it was scheduled for. The original is not changed and nothing is sent. No body.

Requires the `emails:send` and `audiences:read` scopes.

- Scopes: `emails:send`, `audiences:read`.

**Path parameters**

- `id` (`string`, required): A `brd_` id from `POST /broadcasts` or `GET /broadcasts`.

**Returns**

- `201` `Broadcast`: The new draft. The `Location` header names it.

**Errors**

- `403`: `from_address_forbidden` when the key may not send as the original `from`.
- `404`: `broadcast_not_found`, or `audience_not_found` when an audience of the original is gone.
- `422`: The template refusals on `template.*` when the original template no longer resolves.
- The errors every operation can return: `400`, `401`, `429`, `500`, described in the [error catalog](https://nextmail.sh/docs/api/errors).

Also available in: TypeScript [`nextmail.broadcasts.duplicate()`](https://nextmail.sh/docs/sdk/reference/broadcasts#duplicate); Python [`broadcasts.duplicate()`](https://nextmail.sh/docs/python/reference/broadcasts#duplicate); Ruby [`broadcasts.duplicate`](https://nextmail.sh/docs/ruby/reference/broadcasts#duplicate); PHP [`broadcasts->duplicate`](https://nextmail.sh/docs/php/reference/broadcasts#duplicate); Go [`Broadcasts.Duplicate`](https://nextmail.sh/docs/go/reference/broadcasts#duplicate); Java [`broadcasts().duplicate`](https://nextmail.sh/docs/java/reference/broadcasts#duplicate); C# [`Broadcasts.DuplicateAsync`](https://nextmail.sh/docs/csharp/reference/broadcasts#duplicate); CLI [`nextmail broadcasts duplicate`](https://nextmail.sh/docs/cli/reference/broadcasts#broadcasts-duplicate); MCP [`duplicateBroadcast`](https://nextmail.sh/docs/mcp/tools/broadcasts#duplicateBroadcast).

### `POST /broadcasts/{id}/resend`

Resend to the people who did not open

Creates a new broadcast with the same content for the people the first one reached and who have not opened it, optionally under a new `subject`. Somebody who has unsubscribed since, or whose address is now suppressed, is skipped. The new broadcast names the first in `content.resendOf` and counts against the plan like any send. The first broadcast has to be `sent` and to have tracked opens, or the call answers 409 `resend_unavailable`. The body is optional.

Opens are counted from the tracking pixel, so a person whose mail app blocks images can look like they never opened.

Requires the `emails:send` and `audiences:read` scopes.

- Scopes: `emails:send`, `audiences:read`.

**Path parameters**

- `id` (`string`, required): A `brd_` id from `POST /broadcasts` or `GET /broadcasts`.

**Request body**

- `subject` (`string`, 1 to 998 characters): A new subject for the second try, with merge fields. Left out, the first subject is used.
- `scheduledAt` (`string`, 3 to 64 characters): When to start, in the future and at most 365 days out. Left out, the sending starts straight away.

**Returns**

- `202` `Broadcast`: Accepted. The new broadcast, `queued` or `scheduled`. The `Location` header names it.

**Errors**

- `404`: `broadcast_not_found` when the id names no broadcast in this workspace.
- `409`: `resend_unavailable`: the broadcast has not finished going out or did not track opens. `stream_paused` while the broadcast lane is paused.
- `422`: `no_recipients` when everybody it reached has opened it.
- `429`: `send_quota_exceeded`: the plan cannot cover a copy for every recipient this month. `rate_limited`: the key or app made more than 600 requests in 60 seconds. Wait for the seconds in `Retry-After`.
- The errors every operation can return: `400`, `401`, `403`, `500`, described in the [error catalog](https://nextmail.sh/docs/api/errors).

Also available in: TypeScript [`nextmail.broadcasts.resend()`](https://nextmail.sh/docs/sdk/reference/broadcasts#resend); Python [`broadcasts.resend()`](https://nextmail.sh/docs/python/reference/broadcasts#resend); Ruby [`broadcasts.resend`](https://nextmail.sh/docs/ruby/reference/broadcasts#resend); PHP [`broadcasts->resend`](https://nextmail.sh/docs/php/reference/broadcasts#resend); Go [`Broadcasts.Resend`](https://nextmail.sh/docs/go/reference/broadcasts#resend); Java [`broadcasts().resend`](https://nextmail.sh/docs/java/reference/broadcasts#resend); C# [`Broadcasts.ResendAsync`](https://nextmail.sh/docs/csharp/reference/broadcasts#resend); CLI [`nextmail broadcasts resend`](https://nextmail.sh/docs/cli/reference/broadcasts#broadcasts-resend); MCP [`resendBroadcast`](https://nextmail.sh/docs/mcp/tools/broadcasts#resendBroadcast).

### Objects

#### `Broadcast`

`object`

- `object` (`string`, one of `"broadcast"`)
- `id` (`string`): The durable handle, `brd_` + 24 hex.
- `status` (`string`, one of `"draft"`, `"scheduled"`, `"queued"`, `"sending"`, `"sent"`, `"cancelled"`, `"failed"`): `draft` is saved and not sent: `POST /broadcasts/{id}/send` sends it. `scheduled` waits for `scheduledAt`. `queued` waits for the sending to start. `sending` is walking the audiences, or has finished walking them while copies are still waiting to go. `sent` means every copy handed over has been sent or settled. `cancelled` was stopped by `POST /broadcasts/{id}/cancel`. `failed` means the whole broadcast stopped: the `from` address can no longer be sent from, the template stopped resolving, the plan ran out part way, the sending itself kept failing, or not one copy could be written, and `lastError` says which.
- `mode` (`string`, one of `"live"`, `"test"`): The mode of the key that created it. A broadcast made with a test key sends its copies the way any test send goes: accepted and marked sent, and delivered to nobody.
- `source` (`string`, one of `"api"`, `"oauth"`, `"composer"`, `"mcp"`, `"ai"`, `"form"`, `"mail-app"`, `"booking"`): Where it was started: `api` for a key, `oauth` for a connected app, `composer` for the app, `mcp` for an assistant.
- `audienceIds` (`string[]`): The audiences it was sent to, each once.
- `from` (`string`): The address every copy is sent from.
- `subject` (`string`): The subject as written, merge fields and all, so it reads the same for every copy. Empty when a template supplies the subject.
- `counts` (`BroadcastCounts`)
- `lastError` (`string`, nullable): Why the broadcast failed, or the most recent copy that could not be written and why. Null while nothing has gone wrong.
- `scheduledAt` (`string`, nullable, format `date-time`): When the sending is due to start. Null for a broadcast sent straight away.
- `startedAt` (`string`, nullable, format `date-time`): When the sending reached the first people. Null until then.
- `completedAt` (`string`, nullable, format `date-time`): When the last person was reached and every copy had been written. Copies can still be waiting to go after it, which is why `status` can read `sending` with this set.
- `cancelledAt` (`string`, nullable, format `date-time`)
- `createdAt` (`string`, format `date-time`)
- `updatedAt` (`string`, format `date-time`)
- `content` (`BroadcastContent`)

#### `BroadcastAnalytics`

`object`

- `object` (`string`, one of `"broadcast_analytics"`)
- `since` (`string`, format `date-time`): The start of the window, floored to the start of its first bucket in the local time of `offsetMinutes`.
- `until` (`string`, format `date-time`): The end of the window, the moment of the read.
- `grain` (`string`, one of `"minute"`, `"hour"`, `"day"`)
- `offsetMinutes` (`integer`): The offset the buckets were cut in, as sent or 0.
- `broadcastIds` (`string[]`): The ids `totals` and `series` cover, each once. Empty means every broadcast in the window.
- `totals` (`object`): Added up over the copies of the broadcasts in `broadcastIds` that were sent inside the window. `opened`, `clicked` and `unsubscribed` count people; `opens` and `clicks` count events.
  - `broadcasts` (`integer`): How many broadcasts those copies belong to.
  - `recipients` (`integer`)
  - `pending` (`integer`): Copies still queued, scheduled or sending.
  - `sent` (`integer`)
  - `delivered` (`integer`)
  - `bounced` (`integer`)
  - `complained` (`integer`)
  - `failed` (`integer`): Copies that failed or were cancelled.
  - `opened` (`integer`)
  - `clicked` (`integer`)
  - `unsubscribed` (`integer`)
  - `opens` (`integer`)
  - `clicks` (`integer`)
- `series` (`object[]`): SPARSE, oldest first: one bucket per `grain` in which something happened to those copies, each counted once, at the first time it happened.
  - `bucket` (`string`): `YYYY-MM-DD`, `YYYY-MM-DDTHH` or `YYYY-MM-DDTHH:MM`, in the offset asked for.
  - `sent` (`integer`)
  - `delivered` (`integer`)
  - `opened` (`integer`)
  - `clicked` (`integer`)
  - `unsubscribed` (`integer`)
- `broadcasts` (`object[]`): Every broadcast with a copy sent inside the window, newest first, whatever `broadcastIds` picks, each with the same counts over its copies in the window. Use it to compare broadcasts or to pick ids.
  - `id` (`string`)
  - `subject` (`string`)
  - `status` (`string`, one of `"draft"`, `"scheduled"`, `"queued"`, `"sending"`, `"sent"`, `"cancelled"`, `"failed"`)
  - `sentAt` (`string`, format `date-time`): When it started sending, or when it was created if it has not.
  - `recipients` (`integer`)
  - `pending` (`integer`): Copies still queued, scheduled or sending.
  - `sent` (`integer`)
  - `delivered` (`integer`)
  - `bounced` (`integer`)
  - `complained` (`integer`)
  - `failed` (`integer`): Copies that failed or were cancelled.
  - `opened` (`integer`)
  - `clicked` (`integer`)
  - `unsubscribed` (`integer`)
  - `opens` (`integer`)
  - `clicks` (`integer`)

#### `BroadcastContent`

`object`

- `subject` (`string`, required): The subject as written, merge fields and all.
- `html` (`string`, required, nullable): The HTML body as written, before the unsubscribe footer is added.
- `text` (`string`, required, nullable): The plain text body as written.
- `template` (`object`, required, nullable): The stored template the body comes from, with the props and slots it was given.
  - `id` (`string`)
  - `version` (`integer`)
  - `props` (`object`)
  - `slots` (`object`)
- `replyTo` (`string`, required, nullable): Where replies go.
- `tracking` (`object`, required, nullable): Open and link tracking for every copy.
  - `opens` (`boolean`)
  - `clicks` (`boolean`)
- `tags` (`Record<string, string>`, required)
- `excludeAudienceIds` (`string[]`, required): The audiences whose members are left out.
- `segmentId` (`string`, required, nullable): The saved segment that narrows the audiences, or null.
- `utm` (`object`, required, nullable): The campaign tags added to the links.
  - `source` (`string`, 1 to 100 characters)
  - `medium` (`string`, 1 to 100 characters)
  - `campaign` (`string`, 1 to 200 characters)
  - `term` (`string`, 1 to 100 characters)
  - `content` (`string`, 1 to 100 characters)
- `abTest` (`object`, required, nullable): The A/B test, or null. Beside the test as it was asked for, `phase` says where it stands: `testing` while the variants go out, `waiting` until `decidesAt`, and `winner` once `winner` names the variant everybody else gets. `GET /broadcasts/{id}/stats` returns how each variant did.
  - `variants` (`object[]`, required, 2 to 4 items)
    - `subject` (`string`, 1 to 998 characters)
    - `html` (`string`, 1 to 1000000 characters)
    - `text` (`string`, 1 to 1000000 characters)
  - `testPercent` (`integer`, at least 5, at most 50, default `20`)
  - `metric` (`string`, one of `"opens"`, `"clicks"`, default `"opens"`)
  - `waitMinutes` (`integer`, at least 30, at most 4320, default `240`)
  - `phase` (`string`, nullable, one of `"testing"`, `"waiting"`, `"winner"`): Null until the sending starts.
  - `winner` (`string`, nullable): The id of the winning variant, `a` to `d`, once it is picked.
  - `decidesAt` (`string`, nullable, format `date-time`): While `waiting`, when the winner is picked.
  - `decidedAt` (`string`, nullable, format `date-time`): When the winner was picked.
- `resendOf` (`string`, required, nullable): For a broadcast made by `POST /broadcasts/{id}/resend`, the broadcast it sends again to the people who did not open it.

#### `BroadcastCounts`

`object`

Where the broadcast stands. The first four are kept by the sending itself; the last five are counted live from the copies, by the send status each one has now.

- `recipients` (`integer`): The estimate taken when the broadcast was created: the people it was expected to reach. It does not move afterwards, so contacts who join or leave while it sends make `created` differ from it.
- `created` (`integer`): Copies written so far, one per person reached. Each is an ordinary email with its own `msg_` id.
- `skipped` (`integer`): People passed over while sending because their address was on the suppression list by the time the sending reached them.
- `failedToQueue` (`integer`): People whose copy could not be written at all. `lastError` on the broadcast names the most recent one and why.
- `queued` (`integer`): Copies waiting to be sent.
- `sending` (`integer`): Copies being handed over right now.
- `sent` (`integer`): Copies that went out.
- `failed` (`integer`): Copies whose send failed. `GET /emails/{id}` on one says why.
- `cancelled` (`integer`): Copies a cancel stopped before they went.

#### `BroadcastList`

`object`

- `object` (`string`, one of `"list"`)
- `data` (`Broadcast[]`)
- `hasMore` (`boolean`): True when another page follows. Pass `nextCursor` back as `cursor` to read it.
- `nextCursor` (`string`, nullable): The id of the last row on this page, or null on the last page.

#### `BroadcastPreview`

`object`

- `object` (`string`, one of `"broadcast_preview"`)
- `audienceIds` (`string[]`): The audiences as sent.
- `excludeAudienceIds` (`string[]`): The audiences whose members were left out, as sent, or empty.
- `segmentId` (`string`, nullable): The segment the counts were narrowed to, as sent, or null.
- `recipients` (`integer`): The people a broadcast to these audiences would reach now. A contact in several of them counts once.
- `unsubscribed` (`integer`): Contacts in these audiences who would be skipped because they have unsubscribed from every one of them they are in.
- `suppressed` (`integer`): Subscribed contacts who would be skipped because their address is on the suppression list after a bounce or a complaint, or because somebody added it there.

#### `BroadcastRecipient`

`object`

- `object` (`string`, one of `"broadcast_recipient"`)
- `emailId` (`string`): The `msg_` id of this person's copy. `GET /emails/{id}` reads it as a sent email.
- `contactId` (`string`, nullable): The contact it went to, null if deleted since.
- `email` (`string`): The address the copy went to.
- `name` (`string`, nullable)
- `status` (`string`): The status of the copy on the send log: `queued`, `scheduled`, `sending`, `sent`, `bounced`, `failed` or `cancelled`.
- `sentAt` (`string`, nullable, format `date-time`)
- `deliveredAt` (`string`, nullable, format `date-time`): When the receiving server accepted it, the first `email.delivered`.
- `bouncedAt` (`string`, nullable, format `date-time`): When it bounced, the first `email.bounced`.
- `complainedAt` (`string`, nullable, format `date-time`): When the person reported it as spam, the first `email.complained`.
- `failure` (`string`, nullable): Why the copy failed, when it did.
- `opens` (`integer`): Opens recorded, without the ones image proxies and scanners make. 0 when tracking was off.
- `firstOpenAt` (`string`, nullable, format `date-time`)
- `clicks` (`integer`): Clicks recorded on tracked links, without scanners.
- `firstClickAt` (`string`, nullable, format `date-time`)
- `unsubscribedAt` (`string`, nullable, format `date-time`): When this person unsubscribed from one of the broadcast's audiences after it was sent, through its link or otherwise.

#### `BroadcastRecipientContent`

`object`

- `object` (`string`, one of `"broadcast_recipient"`)
- `emailId` (`string`): The `msg_` id of this person's copy. `GET /emails/{id}` reads it as a sent email.
- `contactId` (`string`, nullable): The contact it went to, null if deleted since.
- `email` (`string`): The address the copy went to.
- `name` (`string`, nullable)
- `status` (`string`): The status of the copy on the send log: `queued`, `scheduled`, `sending`, `sent`, `bounced`, `failed` or `cancelled`.
- `sentAt` (`string`, nullable, format `date-time`)
- `deliveredAt` (`string`, nullable, format `date-time`): When the receiving server accepted it, the first `email.delivered`.
- `bouncedAt` (`string`, nullable, format `date-time`): When it bounced, the first `email.bounced`.
- `complainedAt` (`string`, nullable, format `date-time`): When the person reported it as spam, the first `email.complained`.
- `failure` (`string`, nullable): Why the copy failed, when it did.
- `opens` (`integer`): Opens recorded, without the ones image proxies and scanners make. 0 when tracking was off.
- `firstOpenAt` (`string`, nullable, format `date-time`)
- `clicks` (`integer`): Clicks recorded on tracked links, without scanners.
- `firstClickAt` (`string`, nullable, format `date-time`)
- `unsubscribedAt` (`string`, nullable, format `date-time`): When this person unsubscribed from one of the broadcast's audiences after it was sent, through its link or otherwise.
- `subject` (`string`): The subject as this person got it, merge fields filled in.
- `html` (`string`, nullable): The HTML as this person got it, before open and click tracking was added.
- `text` (`string`, nullable)

#### `BroadcastRecipientList`

`object`

- `object` (`string`, one of `"list"`)
- `data` (`BroadcastRecipient[]`)
- `hasMore` (`boolean`): True when another page follows. Pass `nextCursor` back as `cursor` to read it.
- `nextCursor` (`string`, nullable): An opaque cursor for the next page, or null on the last page. Pass it back unchanged.

#### `BroadcastStats`

`object`

- `object` (`string`, one of `"broadcast_stats"`)
- `broadcastId` (`string`)
- `grain` (`string`, one of `"minute"`, `"hour"`, `"day"`)
- `totals` (`object`): Counts over every copy. `opened`, `clicked` and `unsubscribed` count people; `opens` and `clicks` count events.
  - `recipients` (`integer`)
  - `pending` (`integer`): Copies still queued, scheduled or sending.
  - `sent` (`integer`)
  - `delivered` (`integer`)
  - `bounced` (`integer`)
  - `complained` (`integer`)
  - `failed` (`integer`): Copies that failed or were cancelled.
  - `opened` (`integer`)
  - `clicked` (`integer`)
  - `unsubscribed` (`integer`)
  - `opens` (`integer`)
  - `clicks` (`integer`)
- `window` (`object`, nullable): Null unless `days` or `minutes` was sent. Each count is the copies whose first such event fell inside the window, so it tells you what changed lately, while `totals` keeps the whole send.
  - `since` (`string`, format `date-time`): The start of the window, floored to the start of its first `grain` bucket in the offset asked for. It ends now.
  - `delivered` (`integer`)
  - `bounced` (`integer`)
  - `complained` (`integer`)
  - `opened` (`integer`)
  - `clicked` (`integer`)
  - `unsubscribed` (`integer`)
- `series` (`object[]`): SPARSE, oldest first: one bucket per `grain` in which something happened. Each counts people by the first time it happened to them. With a window, only the buckets inside it.
  - `bucket` (`string`): `YYYY-MM-DD`, `YYYY-MM-DDTHH` or `YYYY-MM-DDTHH:MM`, in the offset asked for.
  - `delivered` (`integer`)
  - `opened` (`integer`)
  - `clicked` (`integer`)
  - `unsubscribed` (`integer`)
- `links` (`object[]`): The 10 links clicked most, the most clicked first. Copies sent without link tracking have none.
  - `url` (`string`): The address the link points to.
  - `label` (`string`, nullable): The text of the link, when it had any.
  - `clicks` (`integer`): Clicks on it, every time it was clicked.
  - `people` (`integer`): The people who clicked it at least once.
- `clients` (`object[]`): The 8 mail clients people opened the copies in, the most used first, from the opens a person made, not the ones a privacy proxy made.
  - `client` (`string`, nullable): Such as Gmail or Apple Mail, or null when it could not be told.
  - `people` (`integer`)
  - `opens` (`integer`)
- `countries` (`object[]`): The 10 countries the copies were opened from, the most first, as ISO 3166 codes.
  - `country` (`string`)
  - `people` (`integer`)
- `abTest` (`object`, nullable): Null unless the broadcast runs an A/B test. Each variant counts the copies it sent in the test and the people who opened or clicked one of them, so the rates can be compared before and after the winner is picked.
  - `metric` (`string`, one of `"opens"`, `"clicks"`)
  - `winner` (`string`, nullable): The winning variant, or null until it is picked.
  - `variants` (`object[]`)
    - `id` (`string`): `a` to `d`, in the order of `abTest.variants`.
    - `subject` (`string`, nullable): The subject of the variant, or null when it keeps the broadcast's own.
    - `sent` (`integer`)
    - `opened` (`integer`)
    - `clicked` (`integer`)

#### `BroadcastTest`

`object`

- `object` (`string`, required, one of `"broadcast_test"`)
- `emailIds` (`string[]`, required): The `msg_` id of each copy, in the order of `to`.
- `to` (`string[]`, required): The addresses, lower-cased.
