---
title: "Broadcasts"
description: "Every operation in this group: what it accepts, what it returns and the errors it can answer with."
url: "https://openemail.uk/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`, `500`, described in the [error catalog](https://openemail.uk/docs/api/errors).

Also available in: SDK [`broadcasts.list()`](https://openemail.uk/docs/sdk/reference/broadcasts#list), [`broadcasts.listAll()`](https://openemail.uk/docs/sdk/reference/broadcasts#listAll), [`broadcasts.iterate()`](https://openemail.uk/docs/sdk/reference/broadcasts#iterate); CLI [`openemail broadcasts list`](https://openemail.uk/docs/cli/reference/broadcasts#broadcasts-list); MCP [`listBroadcasts`](https://openemail.uk/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 contact that has unsubscribed from every one of the chosen audiences it is in, and except an address on the suppression list. 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. They are escaped in `html`, and any other `{{…}}` is left as written. With `template`, the same values are passed as props, but only the props the template declares, so a template that declares none of them is sent unchanged.

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. An `html` or `text` body that does not place `{{unsubscribeUrl}}` itself gets a one-line footer with the link. A template is sent as it is, so put `{{unsubscribeUrl}}` in the template. The link opens a page with an Unsubscribe button, and following it marks the person unsubscribed in every audience this broadcast was sent to (`unsubscribedAt` on `GET /audiences/{id}/contacts`). Their other audiences, 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.
- `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}}`, 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, and the template is not given a footer, so declare and place `unsubscribeUrl` yourself.
  - `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.
- `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 `"scheduled"`, `"queued"`, `"sending"`, `"sent"`, `"cancelled"`, `"failed"`): `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"`): 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`)
  - `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 `"scheduled"`, `"queued"`, `"sending"`, `"sent"`, `"cancelled"`, `"failed"`): `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"`): 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`)
  - `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. `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.
- The errors every operation can return: `400`, `401`, `500`, described in the [error catalog](https://openemail.uk/docs/api/errors).

Also available in: SDK [`broadcasts.send()`](https://openemail.uk/docs/sdk/reference/broadcasts#send); CLI [`openemail broadcasts send`](https://openemail.uk/docs/cli/reference/broadcasts#broadcasts-send); MCP [`sendToAudience`](https://openemail.uk/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.

**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`: `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`, `500`, described in the [error catalog](https://openemail.uk/docs/api/errors).

Also available in: SDK [`broadcasts.preview()`](https://openemail.uk/docs/sdk/reference/broadcasts#preview); CLI [`openemail broadcasts preview`](https://openemail.uk/docs/cli/reference/broadcasts#broadcasts-preview); MCP [`previewAudienceSend`](https://openemail.uk/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`, `500`, described in the [error catalog](https://openemail.uk/docs/api/errors).

Also available in: SDK [`broadcasts.analytics()`](https://openemail.uk/docs/sdk/reference/broadcasts#analytics); CLI [`openemail broadcasts analytics`](https://openemail.uk/docs/cli/reference/broadcasts#broadcasts-analytics); MCP [`getBroadcastAnalytics`](https://openemail.uk/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`, `500`, described in the [error catalog](https://openemail.uk/docs/api/errors).

Also available in: SDK [`broadcasts.get()`](https://openemail.uk/docs/sdk/reference/broadcasts#get); CLI [`openemail broadcasts get`](https://openemail.uk/docs/cli/reference/broadcasts#broadcasts-get); MCP [`getBroadcast`](https://openemail.uk/docs/mcp/tools/broadcasts#getBroadcast).

### `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`, `500`, described in the [error catalog](https://openemail.uk/docs/api/errors).

Also available in: SDK [`broadcasts.listRecipients()`](https://openemail.uk/docs/sdk/reference/broadcasts#listRecipients), [`broadcasts.listAllRecipients()`](https://openemail.uk/docs/sdk/reference/broadcasts#listAllRecipients), [`broadcasts.iterateRecipients()`](https://openemail.uk/docs/sdk/reference/broadcasts#iterateRecipients); CLI [`openemail broadcasts list-recipients`](https://openemail.uk/docs/cli/reference/broadcasts#broadcasts-list-recipients); MCP [`listBroadcastRecipients`](https://openemail.uk/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`, `500`, described in the [error catalog](https://openemail.uk/docs/api/errors).

Also available in: SDK [`broadcasts.getRecipient()`](https://openemail.uk/docs/sdk/reference/broadcasts#getRecipient); CLI [`openemail broadcasts get-recipient`](https://openemail.uk/docs/cli/reference/broadcasts#broadcasts-get-recipient); MCP [`getBroadcastRecipient`](https://openemail.uk/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`, `500`, described in the [error catalog](https://openemail.uk/docs/api/errors).

Also available in: SDK [`broadcasts.stats()`](https://openemail.uk/docs/sdk/reference/broadcasts#stats); CLI [`openemail broadcasts stats`](https://openemail.uk/docs/cli/reference/broadcasts#broadcasts-stats); MCP [`getBroadcastStats`](https://openemail.uk/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`, `500`, described in the [error catalog](https://openemail.uk/docs/api/errors).

Also available in: SDK [`broadcasts.cancel()`](https://openemail.uk/docs/sdk/reference/broadcasts#cancel); CLI [`openemail broadcasts cancel`](https://openemail.uk/docs/cli/reference/broadcasts#broadcasts-cancel); MCP [`cancelBroadcast`](https://openemail.uk/docs/mcp/tools/broadcasts#cancelBroadcast).

### Objects

#### `Broadcast`

`object`

- `object` (`string`, one of `"broadcast"`)
- `id` (`string`): The durable handle, `brd_` + 24 hex.
- `status` (`string`, one of `"scheduled"`, `"queued"`, `"sending"`, `"sent"`, `"cancelled"`, `"failed"`): `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"`): 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`)

#### `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 `"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`)

#### `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.
- `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`, `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`, `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`)
