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}`.
Runs the real call against your workspace, with your own key.
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 -X POST "$OE/broadcasts" -H "$AUTH" -H 'content-type: application/json' -d '{ "audienceIds": ["aud_4c1b8e2a7d9f05c36b4e8a71"], "from": "Acme <[email protected]>", "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"}'{ "object": "broadcast", "id": "brd_5a8c1e3f7b2d94a06c8e1f3b", "status": "scheduled", "mode": "live", "source": "api", "audienceIds": ["aud_4c1b8e2a7d9f05c36b4e8a71"], "from": "Acme <[email protected]>", "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.