nextmail.broadcasts
Every method in this namespace: its signature, its parameters, what it returns and an example.
Methods
One message sent to everybody in one or more audiences, a personalised copy for each person, with unsubscribe handled for you.
nextmail.broadcasts.preview()nextmail.broadcasts.send()nextmail.broadcasts.list()nextmail.broadcasts.listAll()nextmail.broadcasts.iterate()nextmail.broadcasts.get()nextmail.broadcasts.stats()nextmail.broadcasts.analytics()nextmail.broadcasts.listRecipients()nextmail.broadcasts.listAllRecipients()nextmail.broadcasts.iterateRecipients()nextmail.broadcasts.getRecipient()nextmail.broadcasts.cancel()nextmail.broadcasts.createDraft()nextmail.broadcasts.update()nextmail.broadcasts.sendDraft()nextmail.broadcasts.duplicate()nextmail.broadcasts.resend()nextmail.broadcasts.sendTest()
nextmail.broadcasts.preview()
Count who a broadcast to some audiences would reach
preview(body: BroadcastPreviewInput, options?: RequestScope): Promise<BroadcastPreviewResource>Returns the numbers send would work from, without sending or writing anything: recipients, the people a broadcast to these audiences would reach now, unsubscribed, the contacts skipped because they have unsubscribed, and suppressed, the subscribed contacts skipped because their address is on the suppression list. A contact in several of the audiences counts once.
The count is taken at the moment of the call, so contacts who join or leave before a send change it. Only audienceIds, excludeAudienceIds and segmentId are sent, so you can pass the same object you are about to give send.
Parameters
body.audienceIdsArray<string>Required1 to 10 audience ids. One that names no audience in the workspace is a 404
audience_not_found, and All contacts, which no broadcast can go to, is a 422audience_is_everyone.body.excludeAudienceIdsArray<string>Up to 10 audiences whose members are left out of the count, as
sendleaves them out.body.segmentIdstringA saved segment that narrows the count to the contacts that match it, as
sendnarrows the broadcast.options.signalAbortSignalCancels the request.
options.apiKeystringOverrides the client's API key for this call only.
Returns
BroadcastPreviewResource with audienceIds, excludeAudienceIds and segmentId as sent, recipients, unsubscribed and suppressed.
Example
const draft = { audienceIds: ['aud_4c1b8e2a7d9f05c36b4e8a71', 'aud_7e3d9a1c5b2f84e06d9a3c51'], from: 'Acme <news@acme.com>', subject: 'The September release', text: 'Hi {{firstName|there}}, here is what changed this month.'} const reach = await nextmail.broadcasts.preview(draft) console.log(`${reach.recipients} will get it, ${reach.unsubscribed} unsubscribed, ${reach.suppressed} suppressed`) if (reach.recipients > 0) await nextmail.broadcasts.send(draft)Notes
Needs only
audiences:read, so a key that cannot send can still show somebody the count.The SDK retries it after a network failure or a retryable status, since it changes nothing.
Also available in
nextmail.broadcasts.send()
Send one message to everybody in one or more audiences
send(body: BroadcastCreate, options?: SendScope): Promise<SentBroadcastResource>Creates a broadcast: one message sent to every contact in the audiences you name, 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. Copies are not filed in the Sent folder, because the broadcast is the record.
The promise settles straight away with the broadcast queued, or scheduled when you pass scheduledAt, and the sending happens in the background, 50 people at a time. Poll get to follow status and counts as it goes.
Who gets it: every contact in at least one of audienceIds, counted once however many of them hold it, except a person who has unsubscribed and an address on the suppression list. audienceIds cannot name All contacts, the built-in audience of everyone the workspace has written to, because most of them never asked for its mail: that is a 422 audience_is_everyone. A contact added to one of the audiences after this call but before the sending reaches it is included. counts.recipients is the estimate taken at the call, and preview returns the same count without sending.
subject, html and text take merge fields, filled in from each contact: {{firstName}}, {{lastName}}, {{name}}, {{email}} and {{unsubscribeUrl}}. Each takes a fallback after a bar, so {{firstName|there}} reads there for a contact with no name. The first name is the first word of the contact's name and the last name is the rest. A custom field of the contact is {{attributes.<key>}}, such as {{attributes.plan|Free}}, and reads as its fallback, or as nothing, when the contact has no such field. Values are escaped in html, and any other {{…}} is left as written. With template, those five values are passed as props, but only the ones the template declares, except unsubscribeUrl, which every template receives.
Every copy carries the one-click unsubscribe headers mail clients and the large mailbox providers look for. 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 through settings.update. Following the link unsubscribes the person from all of the workspace's marketing mail: every audience they are in shows unsubscribedAt, suppressions.listUnsubscribes lists them, and later broadcasts and automation emails skip them, even after they are added to an audience again. Their contact and mail sent to them one message at a time are not affected.
The whole send is checked against the plan's monthly sends before anything is written. A broadcast the allowance cannot cover throws 429 send_quota_exceeded and leaves nothing behind, and each copy counts as one send.
Parameters
body.audienceIdsArray<string>Required1 to 10 audience ids such as
aud_9f2c4b7e1a0d63d84c5f2e7b. A contact in several of them gets one copy. An id that names no audience in the workspace is a 404audience_not_foundand All contacts is a 422audience_is_everyone, and either way nothing is sent.body.excludeAudienceIdsArray<string>Up to 10 audiences whose members are left out, even when they are in one of
audienceIds, such as the customers who already bought. All contacts may be named here. An id that names no audience is a 404audience_not_found.body.segmentIdstringA saved segment, from
segments.create, that narrows the audiences: only the contacts that match its conditions get a copy. Its conditions are read when the broadcast is sent or scheduled and kept with it. An id that names no segment is a 404segment_not_found.body.fromRecipientInputRequiredSender as
news@acme.com,Acme <news@acme.com>or{ email, name }. It must be an address the key may send as, otherwise 403from_address_forbidden.body.replyToRecipientInputWhere replies go, the same for every copy.
body.subjectstringRequired unless a template supplies it, at most 998 characters. Takes merge fields.
body.htmlstringHTML body, at most 1,000,000 characters, with merge fields. Without
{{unsubscribeUrl}}in it an unsubscribe footer is added.body.textstringPlain text body, at most 1,000,000 characters, with merge fields. Without
{{unsubscribeUrl}}in it an unsubscribe line is added.body.template{ id: string, version?: number, props?: Record<string, unknown>, slots?: Record<string, unknown> }A stored template instead of
htmlandtext, never beside them. The merge values reach it as props it declares, such asfirstName, andunsubscribeUrlreaches every template.body.tracking{ opens?: boolean, clicks?: boolean }Open and link tracking for every copy. A field left out follows the address it is sent from when that address set it, and is on otherwise.
body.tagsRecord<string, string>Up to 8 tags, keys of 1 to 64 letters, digits,
_or-, values up to 256 characters. Every copy carries them, plusbroadcast_id, which the server adds.body.utm{ source?: string, medium?: string, campaign?: string, term?: string, content?: string }Campaign tags added to every http and https link in the body as
utm_source,utm_medium,utm_campaign,utm_termandutm_content, so the visits show up in your web analytics. A link that already carries one keeps its own, and the unsubscribe link never gets them. Values are up to 100 characters,campaignup to 200.body.abTestBroadcastAbTestAn A/B test.
variantsholds 2 to 4 versions, each asubject, or a body ashtmland ortext, that replaces the broadcast's own, so{}keeps it as it is.testPercentof the people (5 to 50, 20 unless set) get a variant, split evenly between them, andmetric(opensunless set, orclicks) picks the winner oncewaitMinutes(30 to 4320, 240 unless set) have passed since the test went out. Everybody else then gets the winning variant. A tie goes to the earlier variant, and a template broadcast can only vary the subject.getshows where the test stands incontent.abTest, andstatshow each variant did.body.scheduledAtDate | stringA
Date, an ISO 8601 instant or a duration such asPT2HorP1D, in the future and at most 365 days out. Left out, the sending starts straight away.options.idempotencyKeystringYour own key, 1 to 255 characters of letters, digits,
_,.,:or-. Left out, the SDK makes one for the call, so its own retries never send twice. Sending the same key again answers with the broadcast it created instead of a new one; the same key with a different body is a 422idempotency_key_reuse.options.signalAbortSignalCancels the HTTP request. An aborted call may still have created the broadcast; call again with the same
idempotencyKeyto find out without sending twice.options.apiKeystringSends with this key instead of the client's.
Returns
SentBroadcastResource, a BroadcastResource plus replayed, with id (brd_ plus 24 hex), status queued or scheduled, audienceIds, from, subject, scheduledAt, createdAt and counts, whose recipients is the estimate and whose other counts start at 0.
Example
const broadcast = await nextmail.broadcasts.send({ 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><p><a href="{{unsubscribeUrl}}">Unsubscribe</a></p>', text: 'Hi {{firstName|there}},\n\nHere is what changed this month.\n\nUnsubscribe: {{unsubscribeUrl}}', tags: { campaign: 'release-2026-09' }, scheduledAt: 'PT2H'}) console.log(broadcast.id, broadcast.status, broadcast.replayed, broadcast.counts.recipients)Notes
Safe to retry. Every call carries an
Idempotency-Key, yours or one the SDK makes, so a retry after a network failure answers with the broadcast the first attempt created, withreplayed: true, instead of sending again. Keys are scoped to the API key or app that sent them. Reusing a key with a different body is a 422idempotency_key_reuse.Refusals: 422
no_recipientswhen the audiences are empty or everybody in them has unsubscribed or is suppressed, 422audience_is_everyonewhen one of them is All contacts, 409domain_not_sendablewhen thefromdomain cannot sign mail yet, 422 ontemplate.*when the template does not resolve.No attachments, cc, bcc, translation or encryption. A body from
htmlortextand atemplatetogether are refused.A test key (
oe_test_) creates a broadcast whose copies are marked sent and delivered to nobody, like any test send.
Also available in
nextmail.broadcasts.list()
List one page of broadcasts, newest first
list(options?: BroadcastListOptions): Promise<Page<BroadcastResource>>Returns one page of the broadcasts in the workspace, newest first, each with live counts. audienceId keeps the ones that were sent to that audience, alone or beside others.
Paging is keyset: options.limit takes 1 to 100 and defaults to 25, and nextCursor, the id of the last broadcast on the page, goes back as options.cursor while hasMore is true. Send the same audienceId with every page. listAll and iterate do that walk for you.
The individual messages are not here. List the copies of one broadcast with emails.list({ broadcastId }).
Parameters
options.audienceIdstringOnly the broadcasts that included this audience. An id that names no audience answers an empty page.
options.limitnumberRows per page, a whole number from 1 to 100. The server defaults to 25.
options.cursorstringThe
nextCursorfrom the previous page, a broadcast id. One that names no broadcast in the workspace is a 400invalid_cursor.options.signalAbortSignalCancels the request.
options.apiKeystringOverrides the client's API key for this call only.
Returns
Page<BroadcastResource> with items, hasMore and nextCursor. Each item has id, status, audienceIds, from, subject, counts and the timestamps.
Example
const page = await nextmail.broadcasts.list({ audienceId: 'aud_4c1b8e2a7d9f05c36b4e8a71', limit: 10 }) for (const broadcast of page.items) { console.log(broadcast.createdAt, broadcast.subject, broadcast.status, `${broadcast.counts.sent}/${broadcast.counts.recipients}`)}Notes
A key limited to particular addresses or domains lists only the broadcasts sent from an address or domain it holds.
Also available in
nextmail.broadcasts.listAll()
Collect every broadcast into one array
listAll(options?: BroadcastListOptions): Promise<Array<BroadcastResource>>Follows nextCursor from page to page and resolves with every broadcast in the workspace, newest first, or every one sent to audienceId when you pass it. limit sets the page size of each request, not the total.
Parameters
options.audienceIdstringOnly the broadcasts that included this audience.
options.limitnumberPage size per request, from 1 to 100, defaulting to 25 on the server.
options.cursorstringA broadcast id to start after.
options.signalAbortSignalCancels the request in flight and rejects the whole walk.
options.apiKeystringOverrides the client's API key for every page of this walk.
Returns
Array<BroadcastResource> holding every broadcast across all pages.
Example
const broadcasts = await nextmail.broadcasts.listAll({ limit: 100 }) const sent = broadcasts.reduce((total, broadcast) => total + broadcast.counts.sent, 0) console.log(`${broadcasts.length} broadcasts, ${sent} copies sent`)Notes
A failure on any page rejects the whole call.
Every row carries live counts, so collecting a long history reads the copies of every broadcast in it. Use
iterateto stop early.A key limited to particular addresses or domains lists only the broadcasts sent from an address or domain it holds.
Also available in
nextmail.broadcasts.iterate()
Stream broadcasts one at a time, newest first
iterate(options?: BroadcastListOptions): AsyncGenerator<BroadcastResource, void, undefined>Returns an async generator that yields broadcasts individually, newest first, and requests the next page only once the current one is drained. Breaking out of the loop stops the requests, so this is the cheap way to find the latest broadcast that matches something the filters cannot express.
Parameters
options.audienceIdstringOnly the broadcasts that included this audience.
options.limitnumberPage size per request, from 1 to 100, defaulting to 25 on the server.
options.cursorstringA broadcast id to start after.
options.signalAbortSignalCancels the request in flight and ends the iteration with an error.
options.apiKeystringOverrides the client's API key for every page of this walk.
Returns
AsyncGenerator<BroadcastResource, void, undefined> yielding one broadcast per step.
Example
for await (const broadcast of nextmail.broadcasts.iterate()) { if (broadcast.status === 'sending') { console.log('still going:', broadcast.id, broadcast.counts.queued, 'waiting') break }}Notes
The generator is lazy, so an abandoned loop costs only the pages you consumed.
A key limited to particular addresses or domains lists only the broadcasts sent from an address or domain it holds.
Also available in
nextmail.broadcasts.get()
Read one broadcast and how far it has got
get(id: string, options?: RequestScope): Promise<BroadcastResource>Returns one broadcast with counts read live from its copies, which makes this the call to poll while it sends.
status moves from scheduled or queued to sending and settles on sent once every copy handed over has gone out or failed. It reads sending for as long as copies are still waiting, even after the last person was reached and completedAt was set. cancelled and failed are the other two ends, and on failed 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.
counts.recipients is the estimate taken when it was created. created is the copies written, one per person reached, 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.
Parameters
idstringRequiredA
brd_id fromsendorlist.options.signalAbortSignalCancels the request.
options.apiKeystringOverrides the client's API key for this call only.
Returns
BroadcastResource with id, status, mode, source, audienceIds, from, subject, counts, lastError, scheduledAt, startedAt, completedAt, cancelledAt, createdAt and updatedAt, plus content: the subject, body or template, reply-to, tracking, tags, left-out audiences and campaign tags it holds, which is what update changes on a draft.
Example
const settled = new Set(['sent', 'cancelled', 'failed']) let broadcast = await nextmail.broadcasts.get('brd_5a8c1e3f7b2d94a06c8e1f3b') while (!settled.has(broadcast.status)) { await new Promise((resolve) => setTimeout(resolve, 5_000)) broadcast = await nextmail.broadcasts.get(broadcast.id) console.log(`${broadcast.counts.sent} of ${broadcast.counts.recipients} sent`)} console.log(broadcast.status, broadcast.lastError)Notes
A missing broadcast throws
NextmailApiErrorwithisNotFoundtrue and codebroadcast_not_found. An id from another workspace is a 404 too.For who each copy went to and what happened to it, use
listRecipients,getRecipientreads one copy with its content, andstatssums it up.A key limited to particular addresses or domains reads only broadcasts sent from an address or domain it holds. Any other id throws
broadcast_not_found, as if the broadcast did not exist.
Also available in
nextmail.broadcasts.stats()
Read how a broadcast performed
stats(id: string, options?: BroadcastStatsOptions): Promise<BroadcastStatsResource>Resolves with the totals and a series for one broadcast. totals counts copies sent, delivered, bounced, complained (reported as spam) and failed, 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 in which something happened, counting each person once at the first time it happened to them, so it adds up to the totals.
Pass days or minutes to also read what happened lately. window then counts the copies delivered, bounced, reported as spam, opened, clicked and unsubscribed inside it, and series keeps only its buckets, while totals still covers the whole broadcast. Without either, window is null.
Parameters
idstringRequiredA
brd_id fromsendorlist.options.grainTrackingGrainBucket width:
minute,hourorday, defaulting tohour.options.offsetMinutesnumberMinutes east of UTC to cut the buckets in, from -840 to 840. Pass
-new Date().getTimezoneOffset()for the local zone.options.daysnumberReads a window of this many days too, from 1 to 1095. It starts at the beginning of its first
grainbucket and ends now.options.minutesnumberThe window in minutes, from 1 to 1576800, which wins over
dayswhen both are sent.options.signalAbortSignalCancels the request.
options.apiKeystringOverrides the client API key for this call only.
Returns
BroadcastStatsResource with broadcastId, grain, totals, window and series, each bucket { bucket, delivered, opened, clicked, unsubscribed }. window is { since, delivered, bounced, complained, opened, clicked, unsubscribed }, or null when no window was asked for. links lists the 10 links clicked most as { url, label, clicks, people }, clients the 8 mail clients people opened it in as { client, people, opens }, and countries the 10 countries it was opened from as { country, people }. abTest is null unless the broadcast runs an A/B test, and otherwise { metric, winner, variants }, each variant { id, subject, sent, opened, clicked }.
Example
const stats = await nextmail.broadcasts.stats('brd_5a8c1e3f7b2d94a06c8e1f3b', { grain: 'day' }) const rate = stats.totals.sent ? stats.totals.opened / stats.totals.sent : 0 console.log(`${Math.round(rate * 100)}% opened`)Notes
Read only, so the SDK retries it after a network failure like any other read.
A key limited to particular addresses or domains reads only broadcasts sent from an address or domain it holds. Any other id throws
broadcast_not_found, as if the broadcast did not exist.
Also available in
nextmail.broadcasts.analytics()
Read how broadcasts did over a time window
analytics(options?: BroadcastAnalyticsOptions): Promise<BroadcastAnalyticsResource>Returns the numbers behind the Analytics tab of the Broadcasts page in one request: how the live broadcasts sent inside a window did, added up in totals and cut to grain in series, and one row per broadcast in broadcasts so they can be compared.
A copy counts when it was sent inside the window, and everything that happened to it afterwards counts with it, so an open today of a copy sent last week is in a 30 day window but not in a 1 day one. Test mode broadcasts are left out. totals and series add up the broadcasts named in broadcastIds, or every one when it is left out, while broadcasts always lists every broadcast in the window, newest first.
series is sparse and oldest first: a bucket in which nothing happened has no entry, so a chart must fill the gaps. It counts each person once, at the first time it happened to them. grain sets the bucket width and the key shape, YYYY-MM-DD, YYYY-MM-DDTHH or YYYY-MM-DDTHH:MM, and offsetMinutes shifts the boundaries so days break where the reader's day does. The window starts at the beginning of its oldest bucket, reported as since, and ends now, reported as until.
Parameters
options.broadcastIdsArray<string>Up to 50 broadcast ids for
totalsandseriesto add up, sent comma separated. Leave it out, or pass an empty array, for every broadcast in the window. An id with no copy in the window adds nothing, and more than 50 is a 422.options.daysnumberWindow length in days, from 1 to 1095, defaulting to 30.
options.minutesnumberWindow length in minutes, from 1 to 1576800, which wins over
dayswhen both are sent.options.grainTrackingGrainBucket width:
minute,hourorday, defaulting today.options.offsetMinutesnumberMinutes east of UTC to cut the buckets in, from -840 to 840, defaulting to 0. Pass
-new Date().getTimezoneOffset()for the local zone.options.signalAbortSignalCancels the request.
options.apiKeystringOverrides the client API key for this call only.
Returns
BroadcastAnalyticsResource, { object: 'broadcast_analytics', since, until, grain, offsetMinutes, broadcastIds, totals, series, broadcasts }. totals has broadcasts and the counts BroadcastStatsTotals has, each entry of series is { bucket, sent, delivered, opened, clicked, unsubscribed }, and each entry of broadcasts has id, subject, status, sentAt and the same counts.
Example
const analytics = await nextmail.broadcasts.analytics({ days: 90, offsetMinutes: -new Date().getTimezoneOffset()}) const rate = analytics.totals.delivered ? analytics.totals.opened / analytics.totals.delivered : 0 console.log(`${analytics.totals.broadcasts} broadcasts, ${Math.round(rate * 100)}% opened`) for (const row of analytics.broadcasts) { console.log(row.subject, row.sent, row.opened, row.clicked)}Notes
Read only, so the SDK retries it after a network failure like any other read.
opened,clickedandunsubscribedcount people, whileopensandclickscount events.A key limited to particular addresses or domains reads only broadcasts sent from an address or domain it holds, and the rest are left out as if they did not exist.
Also available in
nextmail.broadcasts.listRecipients()
List who a broadcast went to and what happened to each copy
listRecipients(id: string, options?: BroadcastRecipientListOptions): Promise<Page<BroadcastRecipientResource>>Resolves one page of the people a broadcast went to, one row per copy, sorted by address. Each row says the copy's status, when it was sent and delivered, whether it bounced or was reported as spam, how often it was opened and clicked, and whether the person unsubscribed from one of the broadcast's audiences after it went out.
Opens and clicks leave out image proxies and link scanners, and stay 0 when the broadcast went out with tracking off. emailId is the copy's msg_ id, which getRecipient reads with its content and emails.get reads as a sent email.
Parameters
idstringRequiredA
brd_id fromsendorlist.options.filterBroadcastRecipientFilterKeeps one group:
pending,sent,delivered,opened,not_opened(sent and never opened),clicked,bounced,complained,failed(failed or cancelled) orunsubscribed.BROADCAST_RECIPIENT_FILTERSnames them.options.qstringSearches the address and the name, ignoring case.
options.limitnumberPage size, from 1 to 200. The server defaults to 50.
options.cursorstringThe
nextCursorof the previous page. Send the samefilterandqwith it.options.signalAbortSignalCancels the request.
options.apiKeystringOverrides the client API key for this call only.
Returns
Page<BroadcastRecipientResource> with items, hasMore and nextCursor. Each item has emailId, contactId, email, name, status, sentAt, deliveredAt, bouncedAt, complainedAt, failure, opens, firstOpenAt, clicks, firstClickAt and unsubscribedAt.
Example
const page = await nextmail.broadcasts.listRecipients('brd_5a8c1e3f7b2d94a06c8e1f3b', { filter: 'bounced' }) for (const copy of page.items) console.log(copy.email, copy.bouncedAt)Notes
A missing broadcast throws
NextmailApiErrorwith codebroadcast_not_found.A key limited to particular addresses or domains reads only broadcasts sent from an address or domain it holds. Any other id throws
broadcast_not_found, as if the broadcast did not exist.
Also available in
nextmail.broadcasts.listAllRecipients()
Collect everybody a broadcast went to into one array
listAllRecipients(id: string, options?: BroadcastRecipientListOptions): Promise<Array<BroadcastRecipientResource>>Walks every page of listRecipients and resolves with every copy of the broadcast, sorted by address. One request per page, with the same filter and q on each.
Parameters
idstringRequiredA
brd_id fromsendorlist.options.filterBroadcastRecipientFilterKeeps one group:
pending,sent,delivered,opened,not_opened(sent and never opened),clicked,bounced,complained,failed(failed or cancelled) orunsubscribed.BROADCAST_RECIPIENT_FILTERSnames them.options.qstringSearches the address and the name, ignoring case.
options.limitnumberPage size for each request, from 1 to 200. The server defaults to 50.
options.cursorstringStarts the walk after this cursor instead of the first page.
options.signalAbortSignalCancels the request in flight and the walk with it.
options.apiKeystringOverrides the client API key for every page of this walk.
Returns
Array<BroadcastRecipientResource> holding every copy that matches.
Example
const unopened = await nextmail.broadcasts.listAllRecipients('brd_5a8c1e3f7b2d94a06c8e1f3b', { filter: 'not_opened' }) console.log(unopened.map((copy) => copy.email))Notes
If any page fails the promise rejects and the rows already fetched are discarded.
A key limited to particular addresses or domains reads only broadcasts sent from an address or domain it holds. Any other id throws
broadcast_not_found, as if the broadcast did not exist.
Also available in
nextmail.broadcasts.iterateRecipients()
Walk everybody a broadcast went to, one copy at a time
iterateRecipients(id: string, options?: BroadcastRecipientListOptions): AsyncGenerator<BroadcastRecipientResource, void, undefined>Yields every copy of the broadcast one at a time, fetching the next page only when the loop asks for it, sorted by address. Breaking out of the loop stops the requests.
Parameters
idstringRequiredA
brd_id fromsendorlist.options.filterBroadcastRecipientFilterKeeps one group:
pending,sent,delivered,opened,not_opened(sent and never opened),clicked,bounced,complained,failed(failed or cancelled) orunsubscribed.BROADCAST_RECIPIENT_FILTERSnames them.options.qstringSearches the address and the name, ignoring case.
options.limitnumberPage size for each request, from 1 to 200. The server defaults to 50.
options.cursorstringStarts after this cursor instead of the first page.
options.signalAbortSignalCancels the request in flight and the walk with it.
options.apiKeystringOverrides the client API key for every page of this walk.
Returns
AsyncGenerator<BroadcastRecipientResource, void, undefined> yielding one copy per step.
Example
for await (const copy of nextmail.broadcasts.iterateRecipients('brd_5a8c1e3f7b2d94a06c8e1f3b', { filter: 'clicked' })) { console.log(copy.email, copy.clicks)}Notes
Memory stays flat however large the broadcast, because only one page is held at a time.
A key limited to particular addresses or domains reads only broadcasts sent from an address or domain it holds. Any other id throws
broadcast_not_found, as if the broadcast did not exist.
Also available in
nextmail.broadcasts.getRecipient()
Read one person's copy of a broadcast
getRecipient(id: string, emailId: string, options?: RequestScope): Promise<BroadcastRecipientContentResource>Resolves with one copy: the same row listRecipients gives, plus the 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.
Parameters
idstringRequiredA
brd_id fromsendorlist.emailIdstringRequiredThe
emailIdof the copy, fromlistRecipients.options.signalAbortSignalCancels the request.
options.apiKeystringOverrides the client API key for this call only.
Returns
BroadcastRecipientContentResource: emailId, contactId, email, name, status, sentAt, deliveredAt, bouncedAt, complainedAt, failure, opens, firstOpenAt, clicks, firstClickAt and unsubscribedAt, plus subject, html and text.
Example
const copy = await nextmail.broadcasts.getRecipient('brd_5a8c1e3f7b2d94a06c8e1f3b', 'msg_01dad25067bc4dac966d515d') console.log(copy.subject, copy.deliveredAt)Notes
An
emailIdthat is not a copy of this broadcast throws with coderecipient_not_found.A key limited to particular addresses or domains reads only broadcasts sent from an address or domain it holds. Any other id throws
broadcast_not_found, as if the broadcast did not exist.
Also available in
nextmail.broadcasts.cancel()
Stop a broadcast that is scheduled, queued or still sending
cancel(id: string, options?: RequestScope): Promise<BroadcastResource>Stops a broadcast that is scheduled, queued or sending, including one that has reached everybody while some copies are still waiting to go. Nobody else is added, and every copy still waiting is cancelled. A copy already being handed over finishes, and copies that have gone cannot be recalled, so counts.sent keeps them and counts.cancelled shows what was stopped.
Once every copy has gone out there is nothing left to stop, and the call throws 409 broadcast_not_cancellable. A failed broadcast with copies still waiting can be cancelled to stop them, and one with nothing waiting is refused the same way. Cancelling a broadcast that is already cancelled resolves with it as it stands, so the call is safe to repeat, and the SDK retries it after a network failure.
Parameters
idstringRequiredThe
brd_id to cancel.options.signalAbortSignalCancels the HTTP request, not the broadcast. Read it with
getif you abort.options.apiKeystringCancels with this key instead of the client's.
Returns
BroadcastResource in its new state, status cancelled with cancelledAt set and the copies counted by where each one ended up.
Example
const scheduled = await nextmail.broadcasts.send({ audienceIds: ['aud_4c1b8e2a7d9f05c36b4e8a71'], from: 'news@acme.com', subject: 'Doors open on Friday', text: 'Hi {{firstName|there}}, doors open at nine.', scheduledAt: 'P1D'}) const cancelled = await nextmail.broadcasts.cancel(scheduled.id) console.log(cancelled.status, cancelled.counts.cancelled)Notes
Cancelling a scheduled broadcast before
scheduledAtsends nothing at all.A cancelled broadcast stays cancelled. There is no resume, so send again to the audiences that still need it.
A key limited to particular addresses or domains cancels only broadcasts sent from an address or domain it holds. Any other id throws
broadcast_not_found, as if the broadcast did not exist.
Also available in
nextmail.broadcasts.createDraft()
Save a broadcast as a draft without sending it
createDraft(body: BroadcastDraftCreate, options?: RequestScope): Promise<BroadcastResource>Saves a broadcast with status draft. Nothing is sent, nobody is counted against the plan and the audiences may still be empty, so a draft can be written over several sittings. update changes it, sendDraft sends it or schedules it, and get returns what it holds in content.
The body takes the fields send takes, except scheduledAt, which belongs to sendDraft. Only from is required. The sender, the audiences and a template are checked as they are saved, so a draft never names an address the key may not send as or an audience that does not exist.
Parameters
body.fromRecipientInputRequiredSender as
news@acme.com,Acme <news@acme.com>or{ email, name }. It must be an address the key may send as, otherwise 403from_address_forbidden.body.audienceIdsArray<string>Up to 10 audience ids. Left out or empty, the draft has no audience yet and
sendDraftrefuses it untilupdategives it one.body.excludeAudienceIdsArray<string>Up to 10 audiences whose members are left out, even when they are in one of
audienceIds.body.segmentIdstringA saved segment that narrows the audiences when the draft is sent. It has to exist when the draft is saved and again when it is sent.
body.replyToRecipientInputWhere replies go, the same for every copy.
body.subjectstringAt most 998 characters, with merge fields. Needed before sending unless a template supplies it.
body.htmlstringHTML body, at most 1,000,000 characters, with merge fields.
body.textstringPlain text body, at most 1,000,000 characters, with merge fields.
body.template{ id: string, version?: number, props?: Record<string, unknown>, slots?: Record<string, unknown> }A stored template instead of
htmlandtext, never beside them.body.tracking{ opens?: boolean, clicks?: boolean }Open and link tracking for every copy.
body.tagsRecord<string, string>Up to 8 tags every copy carries.
body.utm{ source?: string, medium?: string, campaign?: string, term?: string, content?: string }Campaign tags added to every http and https link in the body as
utm_source,utm_medium,utm_campaign,utm_termandutm_content. A link that already carries one of them keeps its own, and the unsubscribe link never gets them.body.abTestBroadcastAbTestAn A/B test, as
sendtakes it: 2 to 4variantsof the subject or the body, thetestPercentof the people that get them, themetricthat picks the winner and thewaitMinutesbefore it is picked.options.signalAbortSignalCancels the request.
options.apiKeystringOverrides the client's API key for this call only.
Returns
BroadcastResource with status draft, counts at 0 and the saved fields in content.
Example
const draft = await nextmail.broadcasts.createDraft({ from: 'Acme <news@acme.com>', audienceIds: ['aud_4c1b8e2a7d9f05c36b4e8a71'], subject: 'The October release', text: 'Hi {{firstName|there}}, here is what changed this month.', utm: { source: 'newsletter', medium: 'email', campaign: 'october-release' }}) console.log(draft.id, draft.status, draft.content?.utm)Notes
A draft is listed by
listbeside the broadcasts that went out, withstatusdraft.Refusals: 404
audience_not_foundonaudienceIdsorexcludeAudienceIds, 422audience_is_everyonewhenaudienceIdsnames All contacts, 403from_address_forbidden, and 422 ontemplate.*when the template does not resolve.
Also available in
nextmail.broadcasts.update()
Change a draft, or a scheduled broadcast before it starts
update(id: string, patch: BroadcastUpdate, options?: RequestScope): Promise<BroadcastResource>A partial update: a field you leave out keeps what it holds, and null clears replyTo, html, text, template or utm. Sending any of html, text or template replaces the body as a whole, so a field of the three you leave out is cleared.
A draft takes every change and stays a draft. A scheduled broadcast takes changes until its sending starts, and each change is checked the way send checks a new one: the audiences still reach somebody and the plan still covers them, and counts.recipients is counted again. scheduledAt moves a scheduled broadcast to another time. Once a broadcast is queued, sending or finished, the call throws 409 broadcast_not_editable.
Parameters
idstringRequiredThe
brd_id of a draft or of a scheduled broadcast.patch.audienceIdsArray<string>The new audiences, replacing the old list, up to 10.
patch.excludeAudienceIdsArray<string>The new list of audiences whose members are left out, replacing the old one.
patch.segmentIdstring | nullA saved segment that narrows the audiences. Null takes it away.
patch.fromRecipientInputA new sender the key may send as.
patch.replyToRecipientInput | nullWhere replies go. Null takes it away.
patch.subjectstringA new subject, with merge fields.
patch.htmlstring | nullA new HTML body.
patch.textstring | nullA new plain text body.
patch.template{ id: string, version?: number, props?: Record<string, unknown>, slots?: Record<string, unknown> } | nullA stored template instead of
htmlandtext.patch.tracking{ opens?: boolean, clicks?: boolean }Open and link tracking for every copy.
patch.tagsRecord<string, string>The new tags, replacing the old ones, up to 8.
patch.utm{ source?: string, medium?: string, campaign?: string, term?: string, content?: string } | nullNew campaign tags for the links, replacing the old ones. Null takes them away.
patch.abTestBroadcastAbTest | nullA new A/B test, as
sendtakes it, replacing the old one. Null takes it away.patch.scheduledAtDate | stringA new start for a scheduled broadcast, in the future and at most 365 days out. A draft is scheduled by
sendDraft, so a draft refuses it with 422invalid_parameter.options.signalAbortSignalCancels the request.
options.apiKeystringOverrides the client's API key for this call only.
Returns
BroadcastResource as it stands after the change, with content.
Example
const draft = await nextmail.broadcasts.createDraft({ from: 'news@acme.com', subject: 'Draft' }) const updated = await nextmail.broadcasts.update(draft.id, { audienceIds: ['aud_4c1b8e2a7d9f05c36b4e8a71'], subject: 'Doors open on Friday', text: 'Hi {{firstName|there}}, doors open at nine.'}) console.log(updated.content?.subject)Notes
A key limited to particular addresses or domains changes only broadcasts sent from an address or domain it holds. Any other id throws
broadcast_not_found, as if the broadcast did not exist.Changing a scheduled broadcast can throw what
sendthrows: 422no_recipients, 429send_quota_exceeded, 409stream_pausedwhile the broadcast lane of the workspace is paused.
Also available in
nextmail.broadcasts.sendDraft()
Send a draft now, or schedule it
sendDraft(id: string, body?: BroadcastSendDraftInput, options?: RequestScope): Promise<BroadcastResource>Turns a draft into a broadcast that goes out: queued straight away, or scheduled when you pass scheduledAt. The draft is checked the way send checks a new broadcast, so it needs an audience, a body and a subject (or a template that supplies one), somebody it can reach, and room in the plan's monthly sends for every copy. From here it behaves like any broadcast: get follows it and cancel stops it.
A broadcast that is not a draft throws 409 broadcast_not_draft, so calling it twice never sends twice.
Parameters
idstringRequiredThe
brd_id of the draft.body.scheduledAtDate | stringA
Date, an ISO 8601 instant or a duration such asPT2H, in the future and at most 365 days out. Left out, the sending starts straight away.options.signalAbortSignalCancels the HTTP request. An aborted call may still have sent the draft; read it with
getto find out.options.apiKeystringSends with this key instead of the client's.
Returns
BroadcastResource with status queued or scheduled and counts.recipients estimated at the call.
Example
const draft = await nextmail.broadcasts.createDraft({ from: 'Acme <news@acme.com>', audienceIds: ['aud_4c1b8e2a7d9f05c36b4e8a71'], subject: 'Doors open on Friday', text: 'Hi {{firstName|there}}, doors open at nine.'}) const sent = await nextmail.broadcasts.sendDraft(draft.id, { scheduledAt: 'PT2H' }) console.log(sent.status, sent.scheduledAt, sent.counts.recipients)Notes
Refusals: 409
broadcast_not_draft, 422no_recipientswhen the draft has no audience or nobody in it can be sent to, 422invalid_parameterwhen it has no body or no subject, 429send_quota_exceeded, and 409stream_pausedwhile the broadcast lane of the workspace is paused.The SDK does not retry it on its own after a network failure. Read the broadcast with
getbefore calling again.
Also available in
nextmail.broadcasts.duplicate()
Copy a broadcast into a new draft
duplicate(id: string, options?: RequestScope): Promise<BroadcastResource>Makes a new draft from any broadcast, a draft, a scheduled one or one that went out long ago: the same sender, audiences, left-out audiences, subject, body or template, tracking, tags and campaign tags. The time it was scheduled for is not copied. The original is not changed, and nothing is sent until sendDraft.
Parameters
idstringRequiredThe
brd_id to copy.options.signalAbortSignalCancels the request.
options.apiKeystringOverrides the client's API key for this call only.
Returns
BroadcastResource for the new draft, with its own brd_ id, status draft and content.
Example
const copy = await nextmail.broadcasts.duplicate('brd_5e8a2c1f9b3d74e06a5c2f81') const sent = await nextmail.broadcasts.sendDraft(copy.id) console.log(copy.id, sent.status)Notes
The sender, the audiences and the template are checked again as the copy is saved, so a copy of an old broadcast can throw 403
from_address_forbiddenor 404audience_not_foundwhen they are gone.
Also available in
nextmail.broadcasts.resend()
Send a broadcast again to the people who did not open it
resend(id: string, body?: BroadcastResendInput, options?: RequestScope): Promise<BroadcastResource>Creates a new broadcast with the same content that goes only to the people the first one reached and who have not opened it, optionally under a new subject. Somebody who has unsubscribed since, or whose address is now suppressed, is skipped as always. The new broadcast carries content.resendOf, the id it came from, and counts against the plan like any send.
The first broadcast has to have finished going out and to have tracked opens. Otherwise the call throws 409 resend_unavailable.
Parameters
idstringRequiredThe
brd_id of the broadcast that went out.body.subjectstringA new subject for the second try, 1 to 998 characters, with merge fields. Left out, the first subject is used again.
body.scheduledAtDate | stringWhen to start, in the future and at most 365 days out. Left out, the sending starts straight away.
options.signalAbortSignalCancels the HTTP request. An aborted call may still have created the broadcast; look for it with
list.options.apiKeystringSends with this key instead of the client's.
Returns
BroadcastResource for the new broadcast, queued or scheduled, with counts.recipients holding the people who had not opened.
Example
const second = await nextmail.broadcasts.resend('brd_5e8a2c1f9b3d74e06a5c2f81', { subject: 'In case you missed it: doors open on Friday', scheduledAt: 'P2D'}) console.log(second.id, second.counts.recipients, second.content?.resendOf)Notes
Opens are counted from the tracking pixel, so somebody whose mail app blocks images can look like they never opened. Give a second try some days, and do not send it more than once.
Refusals: 409
resend_unavailable, 422no_recipientswhen everybody opened it, 429send_quota_exceeded, and 409stream_pausedwhile the broadcast lane of the workspace is paused.
Also available in
nextmail.broadcasts.sendTest()
Send a test copy of a broadcast to yourself or a few people
sendTest(body: BroadcastTestInput, options?: RequestScope): Promise<BroadcastTestResource>Sends one copy of a broadcast to up to five addresses, to read it before it goes to an audience. It goes to the account address of the person the key or app acts for, the workspace owner for an API key, unless to names others. The merge fields are filled from the contact that has the address when there is one, and from a sample name otherwise, the subject starts with [Test], the unsubscribe footer is added as it would be, and the unsubscribe link in it does nothing. Opens and clicks are not tracked, and campaign tags in utm are added to the links.
Nobody is added to an audience and no broadcast is created. Each copy counts as one send.
Parameters
body.fromRecipientInputRequiredThe sender, an address the key may send as.
body.toArray<RecipientInput>1 to 5 addresses. Left out, the test goes to the account address of the person the key or app acts for.
body.replyToRecipientInputWhere replies go.
body.subjectstringRequired unless a template supplies it, with merge fields.
body.htmlstringHTML body with merge fields.
body.textstringPlain text body with merge fields.
body.template{ id: string, version?: number, props?: Record<string, unknown>, slots?: Record<string, unknown> }A stored template instead of
htmlandtext.body.utm{ source?: string, medium?: string, campaign?: string, term?: string, content?: string }Campaign tags added to the links, as the broadcast would add them.
options.signalAbortSignalCancels the request.
options.apiKeystringSends with this key instead of the client's.
Returns
BroadcastTestResource with the msg_ id of each copy in emailIds and the addresses in to.
Example
const test = await nextmail.broadcasts.sendTest({ from: 'Acme <news@acme.com>', to: ['me@acme.com'], subject: '{{firstName|Hello}}, doors open on Friday', html: '<p>Hi {{firstName|there}}, doors open at nine.</p>'}) console.log(test.emailIds, test.to)Notes
The SDK does not retry it on its own, since a retry would send the test again.
Google Workspace