Broadcasts
`broadcasts.preview`, `send`, `list`, `listAll`, `iterate`, `get` and `cancel`.
Every method
const draft = { 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' },} const reach = await openemail.broadcasts.preview(draft)console.log(reach.recipients, reach.unsubscribed, reach.suppressed) const broadcast = await openemail.broadcasts.send(draft) let latest = await openemail.broadcasts.get(broadcast.id)while (['scheduled', 'queued', 'sending'].includes(latest.status)) { await new Promise((resolve) => setTimeout(resolve, 5_000)) latest = await openemail.broadcasts.get(broadcast.id)} for await (const copy of openemail.emails.iterate({ broadcastId: broadcast.id })) { console.log(copy.id, copy.status)} const later = await openemail.broadcasts.send({ ...draft, scheduledAt: 'P1D' })await openemail.broadcasts.cancel(later.id) const history = await openemail.broadcasts.list({ audienceId: draft.audienceIds[0] })console.log(latest.status, latest.counts.sent, history.items.length)A broadcast 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. emails.list({ broadcastId }) lists them. The copies are not filed in the Sent folder, because the broadcast is the record.
send resolves straight away with the broadcast queued, or scheduled when you pass scheduledAt, and the sending runs in the background. send needs emails:send and audiences:read, preview needs audiences:read, list, listAll, iterate and get need emails:read, and cancel needs emails:send.
Every send carries an Idempotency-Key, yours through options.idempotencyKey or one the SDK makes, so a retry after a network failure answers with the broadcast the first attempt created instead of sending twice. preview, get and cancel are safe to repeat and are retried.
Merge fields
subject, html and text are filled in for each person from their contact. {{firstName}} is the first word of the contact name, {{lastName}} the rest of it, {{name}} the whole name, {{email}} the address the copy goes to and {{unsubscribeUrl}} the link that unsubscribes them.
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, and any other {{…}} is left exactly as written.
Pass template instead of html and text to send a stored template. The same five values reach it as props, but only the props the template declares, so a template that declares firstName gets it and one that does not is never refused for it. Anything in template.props goes to every copy alike.
Unsubscribe
Every copy carries the one-click unsubscribe headers that let a mail client show its own unsubscribe button, which 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 exactly as it is, so put {{unsubscribeUrl}} in the template.
Unsubscribing marks the person unsubscribed in every audience that broadcast went to, and AudienceContactResource.unsubscribedAt shows it on audiences.listContacts. They stay in the audience and in the address book, their other audiences are untouched, and mail sent to them one message at a time still goes. Taking them out of the audience and adding them again makes them subscribed afresh.
Who is skipped
A broadcast reaches every contact in at least one of audienceIds, once however many of them hold it. It skips a contact that has unsubscribed from every one of those audiences it is in, and an address 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 send but before the sending reaches it is included.
preview returns the same numbers without sending: recipients, unsubscribed and suppressed. A send that would reach nobody throws 422 no_recipients.
The whole send is checked against the monthly sends of the plan before anything is written, so a broadcast the allowance cannot cover throws 429 send_quota_exceeded and leaves nothing behind. Each copy counts as one send.
Status and progress
get reads counts live from the copies, so poll it while a broadcast sends. status moves from scheduled or queued to sending and settles on sent once every copy handed over has gone out or failed. It stays sending while copies are still waiting, even after completedAt says the last person was reached. failed means the whole broadcast stopped, and lastError says why: 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.
cancel stops a broadcast that is scheduled, queued or sending. Nobody else is added and every copy still waiting is cancelled, while copies that have gone cannot be recalled. Once every copy has gone, cancel throws 409 broadcast_not_cancellable, and cancelling a cancelled broadcast resolves with it as it stands.
Response: BroadcastResource
send, get and cancel each resolve to one of these. list resolves to a page of them, { items, hasMore, nextCursor }, newest first, and listAll and iterate walk every page. preview resolves to a BroadcastPreviewResource with audienceIds, recipients, unsubscribed and suppressed.
idstring- The durable handle, `brd_` followed by 24 hex characters.
statusBroadcastStatus- `scheduled`, `queued`, `sending`, `sent`, `cancelled` or `failed`. `BROADCAST_STATUSES` names each one.
modeApiKeyMode- `live` or `test`, from the key that created it. The copies of a test broadcast are marked sent and delivered to nobody.
sourceEmailSource- Where it was started: `api` for a key, `oauth` for a connected app, `composer` for the app, `mcp` for an assistant.
audienceIdsstring[]- The audiences it was sent to, each once.
fromstring- The address every copy is sent from.
subjectstring- The subject as written, merge fields and all. Empty when a template supplies the subject.
countsBroadcastCounts- `recipients` is the estimate taken at `send`. `created` counts the copies written, `skipped` the people passed over because their address was suppressed by then, and `failedToQueue` the people whose copy could not be written. `queued`, `sending`, `sent`, `failed` and `cancelled` count the copies by the state each one is in now.
lastErrorstring | null- Why the broadcast failed, or the most recent copy that could not be written and why. Null while nothing has gone wrong.
scheduledAtstring | null- ISO-8601 UTC, when the sending is due to start. Null for a broadcast sent straight away.
startedAtstring | null- ISO-8601 UTC, when the sending reached the first people.
completedAtstring | null- ISO-8601 UTC, when the last person was reached. Copies can still be waiting to go after it.
cancelledAtstring | null- ISO-8601 UTC, when `cancel` stopped it.
createdAtstring- ISO-8601 UTC, when `send` was called. Fixes the list order.
updatedAtstring- ISO-8601 UTC, bumped as the sending moves on.