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 on your workspace.
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 <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"}'{ "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 /broadcasts/{id}/recipients lists them, one per person, with what happened to each.
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, which GET /suppressions/unsubscribes lists, 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. All contacts, the built-in audience of everyone the workspace has written to, cannot be one of the audiences.
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 the marketing mail of the workspace. |
| {{attributes.<key>}} | A custom field of the contact, such as {{attributes.plan}}. Empty, or the fallback, when the contact has no such field. |
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.
A body or a template that does not place {{unsubscribeUrl}} itself gets a short footer, "You are receiving this because you are on this mailing list. Unsubscribe", in English. Once marketingPostalAddress is set with PATCH /settings, the footer also names the sender, marketingSenderName or else the workspace name, and shows that address, which many countries require of marketing mail. A template receives unsubscribeUrl whether it declares it or not, so one that places the link decides where it goes.
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 unsubscribed from all of the marketing mail of the workspace: every audience they are in shows unsubscribedAt on GET /audiences/{id}/contacts, GET /suppressions/unsubscribes lists them, 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.
A/B tests
abTest tries 2 to 4 versions on a share of the people first and sends the best one to everybody else. Each entry in variants replaces the subject, or the body as html and or text, and {} keeps the broadcast as written. A template broadcast can only vary the subject.
curl -X POST "$OE/broadcasts" -H "$AUTH" -H 'content-type: application/json' -d '{ "audienceIds": ["aud_4c1b8e2a7d9f05c36b4e8a71"], "from": "Acme <news@acme.com>", "subject": "The September release is out", "html": "<p>Here is what changed this month.</p>", "abTest": { "variants": [{}, { "subject": "Five things we shipped in September" }], "testPercent": 20, "metric": "opens", "waitMinutes": 240 }}'testPercent of the people, 20 unless set, get a version, split evenly between the versions. Once waitMinutes have passed, 240 unless set, the version with the highest share of its people who opened, or who clicked when metric is clicks, goes to everybody else. A tie goes to the earlier version.
GET /broadcasts/{id} shows where the test stands in content.abTest.phase, and GET /broadcasts/{id}/stats shows how each version did and which one won.
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 | audience_is_everyone | One of the audiences is All contacts, the built-in audience of everyone the workspace has written to. |
| 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. |
| 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.
Google Workspace