Skip to the documentation
TypeScript

Broadcasts

`broadcasts.preview`, `send`, `list`, `listAll`, `iterate`, `get`, `listRecipients`, `listAllRecipients`, `iterateRecipients`, `getRecipient`, `stats`, `analytics` and `cancel`.

Every method

broadcasts.ts
const draft = {  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' },} const reach = await nextmail.broadcasts.preview(draft)console.log(reach.recipients, reach.unsubscribed, reach.suppressed) const broadcast = await nextmail.broadcasts.send(draft) let latest = await nextmail.broadcasts.get(broadcast.id)while (['scheduled', 'queued', 'sending'].includes(latest.status)) {  await new Promise((resolve) => setTimeout(resolve, 5_000))  latest = await nextmail.broadcasts.get(broadcast.id)} for await (const copy of nextmail.broadcasts.iterateRecipients(broadcast.id)) {  console.log(copy.email, copy.status, copy.opens, copy.clicks)} const bounced = await nextmail.broadcasts.listRecipients(broadcast.id, { filter: 'bounced' })if (bounced.items[0]) {  const copy = await nextmail.broadcasts.getRecipient(broadcast.id, bounced.items[0].emailId)  console.log(copy.subject, copy.bouncedAt)} const stats = await nextmail.broadcasts.stats(broadcast.id, { grain: 'day' })console.log(stats.totals.opened, stats.totals.clicked, stats.totals.unsubscribed) const lately = await nextmail.broadcasts.stats(broadcast.id, { days: 1 })console.log(lately.window?.opened) const month = await nextmail.broadcasts.analytics({ days: 30 })for (const row of month.broadcasts) console.log(row.subject, row.sent, row.opened) const later = await nextmail.broadcasts.send({ ...draft, scheduledAt: 'P1D' })await nextmail.broadcasts.cancel(later.id) const history = await nextmail.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. listRecipients lists them with what happened to each. 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, get, listRecipients, listAllRecipients, iterateRecipients, getRecipient, stats and analytics 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, cancel and every read 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.

A custom field of the contact is {{attributes.<key>}}, such as {{attributes.plan}} for the field plan.

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.

A/B tests

Pass abTest to send, createDraft or update to try 2 to 4 versions on a share of the people first, each in variants with its own subject, or its own html and text. Once waitMinutes have passed, the version with the highest share of opens, or of clicks when metric is clicks, goes to everybody else. stats shows how each version did and which one won.

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. A body or a template that does not place {{unsubscribeUrl}} itself gets a short footer with the link, in English, which also names the sender and shows the postal address once marketingPostalAddress is set with settings.update. A template receives unsubscribeUrl whether it declares it or not.

Unsubscribing takes the person off all of the marketing mail of the workspace. AudienceContactResource.unsubscribedAt shows it in every audience they are in, suppressions.listUnsubscribes lists them, and taking them out of an audience and adding them again, or deleting the contact and making it again, leaves them unsubscribed. They stay in the address book, and mail sent to them one message at a time still goes. Only they can subscribe again, from the unsubscribe page or a sign-up form they confirm from their inbox.

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, 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. All contacts, the built-in audience of everyone the workspace has written to, is refused with 422 audience_is_everyone.

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.

Who it reached

listRecipients resolves one page of the people a broadcast went to, one row per copy, sorted by address, as { items, hasMore, nextCursor }. listAllRecipients walks every page into one array and iterateRecipients yields one copy at a time, fetching the next page only when the loop asks for it. limit goes from 1 to 200 and defaults to 50, and a cursor goes back with the same filter and q.

filterKeeps
pendingCopies still queued, scheduled or sending.
sentCopies that went out.
deliveredCopies the receiving server accepted.
openedCopies opened at least once.
not_openedCopies sent and never opened.
clickedCopies with at least one tracked click.
bouncedCopies that bounced.
complainedCopies the person reported as spam.
failedCopies that failed or were cancelled.
unsubscribedPeople who unsubscribed after the broadcast went out.

BROADCAST_RECIPIENT_FILTERS names each filter, and q searches the address and the name, ignoring case. Opens and clicks leave out image proxies and link scanners, and stay 0 when the broadcast went out with tracking off.

getRecipient(id, emailId) resolves one copy: the same row, plus 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. An emailId that is not a copy of this broadcast throws 404 recipient_not_found, and an unknown broadcast throws 404 broadcast_not_found.

stats resolves the totals and a series. totals counts copies sent, delivered, bounced, complained and failed, with pending for the ones still waiting, and people who opened, clicked and unsubscribed, with opens and clicks as event counts. series is sparse and oldest first, one bucket per grain (minute, hour or day, defaulting to hour) in which something happened, cut in offsetMinutes east of UTC. It counts each person once, at the first time it happened to them, so it adds up to the totals.

A key limited to particular addresses or domains reaches only the broadcasts sent from an address or domain it holds. list, listAll and iterate leave the others out, and get, the recipient methods, stats and cancel throw 404 broadcast_not_found for them.

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. listRecipients resolves to a page of BroadcastRecipientResource rows, getRecipient to a BroadcastRecipientContentResource and stats to a BroadcastStatsResource.

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.

Response: BroadcastRecipientResource

Each row of listRecipients, listAllRecipients and iterateRecipients. BroadcastRecipientContentResource, from getRecipient, adds subject, html and text.

emailIdstring
The `msg_` id of this person's copy. `getRecipient` reads it with its content and `emails.get` reads it as a sent email.
contactIdstring | null
The contact it went to, or null when the contact has been deleted since.
emailstring
The address the copy went to.
namestring | null
The name on the contact.
statusstring
The state of the copy: `queued`, `scheduled`, `sending`, `sent`, `failed` or `cancelled`.
sentAtstring | null
ISO-8601 UTC, when the copy went out.
deliveredAtstring | null
ISO-8601 UTC, when the receiving server accepted it, the first `email.delivered`.
bouncedAtstring | null
ISO-8601 UTC, when it bounced, the first `email.bounced`.
complainedAtstring | null
ISO-8601 UTC, when the person reported it as spam, the first `email.complained`.
failurestring | null
Why the copy failed, when it did.
opensnumber
Opens recorded, without the ones image proxies and scanners make. 0 when tracking was off.
firstOpenAtstring | null
ISO-8601 UTC, the first open.
clicksnumber
Clicks recorded on tracked links, without scanners.
firstClickAtstring | null
ISO-8601 UTC, the first click.
unsubscribedAtstring | null
ISO-8601 UTC, when this person unsubscribed from one of the broadcast's audiences after it went out, through its link or otherwise.