---
title: "Send to audiences"
description: "Sends one message to everybody in one or more audiences, as a separate copy for each person and personalised from each contact. 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. The call answers `202` straight away and the sending runs in the background, so follow it with `GET /broadcasts/{id}`."
url: "https://openemail.uk/docs/api/broadcasts/send"
area: "API"
category: "Mailbox"
---

# Send to audiences

Sends one message to everybody in one or more audiences, as a separate copy for each person and personalised from each contact. 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. The call answers `202` straight away and the sending runs in the background, so follow it with `GET /broadcasts/{id}`.

`POST /broadcasts`

## POST /broadcasts

Sends one message to everybody in one or more audiences, as a separate copy for each person and personalised from each contact. 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. The call answers `202` straight away and the sending runs in the background, so follow it with `GET /broadcasts/{id}`.

## Example

Needs `emails:send` and `audiences:read`. `audienceIds` holds 1 to 10 ids. The body comes from `html` and or `text`, or from a stored `template`, never both, and `subject` is required unless the template supplies it.

**curl**

```
curl -X POST "$OE/broadcasts" -H "$AUTH" -H 'content-type: application/json' -d '{
  "audienceIds": ["aud_4c1b8e2a7d9f05c36b4e8a71"],
  "from": "Acme <news@acme.com>",
  "subject": "{{firstName|Hello}}, the September release is out",
  "html": "<p>Hi {{firstName|there}},</p><p>Here is what changed this month.</p>",
  "text": "Hi {{firstName|there}}, here is what changed this month. Unsubscribe: {{unsubscribeUrl}}",
  "tags": { "campaign": "release-2026-09" },
  "scheduledAt": "PT2H"
}'
```

**Response**

```
{
  "object": "broadcast",
  "id": "brd_5a8c1e3f7b2d94a06c8e1f3b",
  "status": "scheduled",
  "mode": "live",
  "source": "api",
  "audienceIds": ["aud_4c1b8e2a7d9f05c36b4e8a71"],
  "from": "Acme <news@acme.com>",
  "subject": "{{firstName|Hello}}, the September release is out",
  "counts": {
    "recipients": 412,
    "created": 0,
    "skipped": 0,
    "failedToQueue": 0,
    "queued": 0,
    "sending": 0,
    "sent": 0,
    "failed": 0,
    "cancelled": 0
  },
  "lastError": null,
  "scheduledAt": "2026-09-23T14:00:00.000Z",
  "startedAt": null,
  "completedAt": null,
  "cancelledAt": null,
  "createdAt": "2026-09-23T12:00:00.000Z",
  "updatedAt": "2026-09-23T12:00:00.000Z",
  "replayed": false
}
```

> The answer is `queued`, or `scheduled` with `scheduledAt`, which takes an ISO 8601 instant or a duration such as `PT2H`, at most 365 days out. `counts.recipients` is the estimate taken now, and the other counts start at 0. The `Location` header names the broadcast.

> Safe to retry with an `Idempotency-Key` header: the same key answers `200` with the broadcast the first call created and `Idempotency-Replayed: true`, and 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.

> The copies are not filed in the Sent folder, because the broadcast is the record. `GET /emails?broadcastId=brd_5a8c1e3f7b2d94a06c8e1f3b` lists them, one per person.

## Who gets it

Every contact in at least one of the audiences, counted once however many of them hold it. Two kinds of contact are left out: one that has unsubscribed from every one of the chosen audiences it is in, and one whose address is on the suppression list after a bounce or a complaint, or because somebody added it there. A contact added to one of the audiences after the call but before the sending reaches it is included.

The sending walks the audiences 50 people at a time and hands each copy to the same pipeline `POST /emails` uses, so each copy is retried, tracked and reported like any other message. `POST /broadcasts/preview` returns the count this call would start from, without sending anything.

> The whole send is checked against the monthly sends of the plan 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.

## Merge fields

`subject`, `html` and `text` are filled in for each person. Every field takes a fallback after a bar, used when the contact has no value for it, so `{{firstName|there}}` becomes "there" for a contact saved without a name. Values are escaped in `html`, spaces inside the braces are allowed, and any other `{{…}}` is left exactly as written.

| Field | Filled with |
| --- | --- |
| `{{firstName}}` | The first word of the contact name. |
| `{{lastName}}` | The rest of the contact name after the first word. |
| `{{name}}` | The whole contact name. |
| `{{email}}` | The address the copy is going to. |
| `{{unsubscribeUrl}}` | The link that unsubscribes this person from these audiences. |

With `template` instead of a body, the same five values are passed as props, but only the props the template declares. A template that declares `firstName` gets it, and a prop it does not declare is never sent, so the copies never fail on an unknown prop. Anything you put in `template.props` goes to every copy alike.

## Unsubscribe

Every copy carries `List-Unsubscribe` and `List-Unsubscribe-Post: List-Unsubscribe=One-Click`. That is what lets a mail client show 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: "You are receiving this because you are on this mailing list. Unsubscribe". A template is sent exactly as it is, so put `{{unsubscribeUrl}}` in the template.

The link opens a page with an Unsubscribe button, so a link scanner that fetches it unsubscribes nobody, while the one-click request a mail client makes unsubscribes straight away. Either way the person is marked unsubscribed in every audience this broadcast went to, which shows as `unsubscribedAt` on `GET /audiences/{id}/contacts`. Their other audiences, their contact and mail sent to them one message at a time are not affected.

## Refusals

| Status | Code | When |
| --- | --- | --- |
| 403 | `from_address_forbidden` | The key may not send as `from`. |
| 404 | `audience_not_found` | An id in `audienceIds` names no audience in this workspace. |
| 409 | `domain_not_sendable` | The `from` domain cannot sign mail yet, as on `POST /emails`. |
| 422 | `no_recipients` | The audiences are empty, or everybody in them has unsubscribed or is suppressed. |
| 422 | `invalid_parameter` | No body, `html` or `text` beside `template`, no `subject` without a template, more than 10 audiences or 8 tags, or a `scheduledAt` that is not in the future or is more than 365 days out. |
| 422 | `template_not_found` | The template does not resolve. Other template refusals name `template.*` too. |
| 422 | `capability_unsupported` | The key is limited to particular addresses. Audiences belong to the whole workspace. |
| 429 | `send_quota_exceeded` | The plan cannot cover a copy for everybody this month. |

> No attachments, cc, bcc, translation or encryption. `tags` takes up to 8, and every copy also carries `broadcast_id`, which the server adds.
