Broadcasts
Compose, schedule, and send one email to many contacts.
A broadcast is one email sent to many contacts: all of them, a segment, or a topic. Compose in the dashboard's block editor or as HTML in code mode, with per-contact merge fields, or create broadcasts via the API.
Lifecycle
draft → scheduled → sending → sent
↘ canceled- Broadcasts are created as drafts. Only drafts can be edited or deleted.
POST /broadcasts/{id}/sendschedules the send — immediately, or at ascheduled_attimestamp.- A queued broadcast — scheduled, or already going out — can be canceled
with
POST /broadcasts/{id}/cancel. Emails already sent are not recalled; the response'scanceled_remainingsays how many were stopped, andsent_counton a read says how many had gone out. - On the wire,
scheduledandsendingboth read asqueued(matching the Resend SDK's status union);canceledis a MillionSend extension. A broadcast readsqueuedwithsent_atnull until its last email has gone out, however long that takes.
Block editor and HTML
The composer edits the body in one of two modes. The block editor builds the email from blocks. Code mode edits the HTML source beside a live preview. Both modes use the same per-contact merge fields.
A broadcast created with html (through the API, MCP or a migration) or
started from an HTML template is html-authored. The dashboard opens it on its
preview and edits it in code mode. The HTML stays byte for byte as written.
The block editor never touches it on its own, because parsing a
table-and-inline-CSS layout into blocks flattens it.
It becomes blocks only when you choose Convert to blocks, and the stored HTML changes only at the next save. Templates follow the same rule.
The other way works too. Edit as HTML moves a broadcast built with blocks to code mode. The switch always happens in the broadcast itself, since broadcasts can't be duplicated. The HTML that the blocks render replaces the blocks. That HTML leaves out blocks with a Show if condition.
Until the next save, Back to blocks puts the blocks back exactly as they were. After a save, converting back to blocks flattens the layout. On an empty body, the switch happens at once, so a new broadcast can start as HTML. A body that you save empty opens in the block editor again.
In code mode, a plain text part generated from the HTML keeps following the HTML as you edit. Plain text supplied through the API is sent as stored.
What the fan-out does
Every recipient email goes through the same pipeline as a transactional send, plus broadcast-specific handling:
- Audience resolution — globally unsubscribed contacts are always excluded; segment filters and topic subscriptions are evaluated at send time.
- Suppression checks — addresses on the suppression list are skipped.
- Merge fields — per-contact values (name, custom properties) are substituted into the template.
- Unsubscribe links — RFC 8058 one-click
List-Unsubscribeheaders and a hosted unsubscribe link are added to every message. To place the link in the body yourself, write{{{UNSUBSCRIBE_URL}}}— it is replaced per-recipient with their hosted unsubscribe URL.{{{RESEND_UNSUBSCRIBE_URL}}}is a supported alias, so templates written for Resend keep working unchanged (and roll back unchanged). - List headers: every message carries
List-Id,Precedence: bulkandAuto-Submitted: auto-generated, so mailbox providers file it as list mail.List-Idnames the list rather than the send:<{topic_id}.example.com>for a broadcast to a topic and<broadcasts.example.com>for the rest, whereexample.comis your sending domain. Every broadcast to the same list builds the same list's reputation.
Broadcast emails enter the send queue below transactional ones: a transactional email accepted while a broadcast is going out is sent ahead of the remaining recipients. Sends run on several lanes at once, paced by the instance's SES send rate.
On a self-hosted instance, unsubscribe links are built from APP_BASE_URL,
so broadcast sending is rejected until it is configured — see
Self-hosting. On Cloud this is
automatic.
Pacing
The platform sends a bounded number of emails a day. A broadcast that fits inside what is available goes out at once, at the sending rate. A larger one goes out in waves: the first wave now, the rest as capacity frees over the following days. Transactional email is never held behind a broadcast's waves.
The send tells you up front:
{
"id": "8c1f0b8e-…",
"finishes_at": "2026-09-18T13:26:05Z",
"estimated": true,
"warning": {
"code": "paced",
"days": 3,
"message": "170,000 recipients exceed the broadcast capacity available now; sending is paced and finishes about 2026-09-18T13:30:00Z. Transactional email is unaffected."
}
}finishes_atis the estimated instant the last email goes out;nullwhen no estimate is available. It is an estimate: other sends move it.warningis present only when the send takes more than one wave:pacedwhen the audience exceeds the capacity available now,queued_behindwhen other sends are ahead (the message says when this one starts). A domain warming up answerswarming_upinstead (see Warm-up).- From a domain under the
new domain limit, the estimate
follows the limit's schedule, and
warningisnew_domain_limitwhen the limit spreads the send over days. The limit alone never makes a send too large: when the estimate can't tell when it ends,finishes_atisnull. - On the Free plan, mail to providers that report complaints goes out to 10
recipients at once, then 10 an hour, across all the Free teams you
created, each email in turn: transactional email sent after a broadcast
waits behind it. Mail only to Gmail, Apple Mail, t-online or the SES
mailbox simulator never waits.
finishes_atdoesn't count this wait. - While a broadcast is going out,
GET /broadcasts/{id}and the list carry a livefinishes_atandsent_count. - An audience that would need more than 24 days of capacity — the
platform's, or your plan's — is refused with
422 broadcast_too_largeinstead of accepted and left waiting. Split it into smaller segments or contact support.
Warm-up
When the sending domain's warm-up is on, a broadcast goes out over days: each day sends up to that day's volume, most active contacts first, and contacts inactive for 90 days or more are skipped unless the warm-up sends them last. The Send broadcast dialog shows the plan day by day before you send. For a domain with no warm-up, it offers one when a broadcast is far bigger than anything the domain has sent in a day.
The send response carries warning.code warming_up:
{
"id": "8c1f0b8e-…",
"finishes_at": "2026-10-24T19:45:00Z",
"estimated": true,
"warning": {
"code": "warming_up",
"days": 6,
"message": "This domain is warming up, so this broadcast goes out over about 6 days, most active contacts first."
}
}finishes_atcomes from the warm-up's plan, which counts on every day being healthy. A day that holds or cuts the volume moves it later.- While the warm-up is paused,
daysis0andfinishes_atisnull. The broadcast waits until the warm-up resumes. - As with pacing, a broadcast the warm-up would need more than 24 days to
send is refused with
422 broadcast_too_large. Raise the daily target or send to a smaller segment.
The broadcast page shows the warm-up's pace day by day, how many inactive contacts were skipped and, when a day missed a signal, why the volume held or was cut. Mail waiting for the warm-up shows as Waiting.
By inbox provider
The broadcast page splits the broadcast's mail by inbox provider: Gmail, Microsoft, Yahoo, Apple and Other, each with how many emails were sent, delivered, opened, clicked, bounced, complained and delayed. It groups by the recipient's address, so a company domain whose mail Google or Microsoft hosts counts as Other. Opens count people only, never a machine's prefetch, and bounces count hard bounces only. Complaints are a share of the mail to providers that report them, the same base the guardrails use; Gmail and Apple never report one, so their cell shows a dash.
Guardrails
- Only a verified domain of your team may appear in
from. Your team's shared subdomain readsverifiedtoo, but sending a broadcast from it answers403 inbox_send_policy. - If your trailing bounce or complaint rate has crossed the SES pause
threshold, new broadcast sends are blocked with a
403 sending_pausederror, stopping the damage before SES pauses sending entirely. A complaint is dated by the UTC day its email was sent, as SES dates its own rate, so the pause weighs complaints about the last two days' mail and the warning about the last seven days'. The complaint rate counts only mail to providers that report complaints (Gmail, Apple Mail and t-online never do), and complaints count toward a warning or this pause only once there are 100 such sends. On the Free plan they count from the first email, judged together across all the Free teams you created, deleted ones included, and a pause holds every one of them. - An operator can lift a bounce pause early after a review (on MillionSend Cloud, ask support; on the Free plan the pause lifts on its own within two days). Bounces after the lift count as usual, and a complaint pause is never lifted early.
- If the platform's aggregate bounce rate in your sender's SES region nears
the line where SES acts on the account, broadcast sends in that region are
refused with
403 broadcasts_pauseduntil the rate recovers. When it is the complaint rate, only teams whose own complaint rate over the last 30 days reaches SES's 0.1% review line are refused; the pause refuses no other team's broadcasts. Transactional email keeps flowing, and the pause clears on its own. On MillionSend Cloud, each Free team whose mail drew a complaint in the region is also held for review.
See the API reference for all broadcast endpoints.
Recent changes
- An operator can now lift a bounce pause early after a review (on MillionSend Cloud, ask support; on the Free plan the pause lifts on its own within two days). Bounces after the lift count as usual, and a complaint pause is never lifted early.
- Complaints are dated by the day their email was sent, as SES dates its own rate, not the day they arrive: in the guardrail, the account score and the complaint rate on the Metrics page. A late complaint about older mail no longer pauses a team that is sending cleanly today.
- A pause for the platform's complaint rate in an SES region refuses
broadcasts with
403 broadcasts_pausedonly to the teams whose own complaint rate over the last 30 days is at 0.1% or more; before, it refused every team in the region. - On the Free plan, mail to providers that report complaints goes out to 10 recipients at once, then 10 an hour, across all the Free teams you created. See Pacing.
- On the Free plan, complaints count toward a warning or
403 sending_pausedfrom the first email, judged together across all the Free teams you created. Bounces and complaints from the SES mailbox simulator never count. - Edit as HTML in the composer moves a broadcast built with blocks to code mode. Back to blocks undoes the switch until the next save. See Block editor and HTML.
- A broadcast from a domain warming up goes out over days. The
send response carries
warning.codewarming_up, and one the warm-up would need more than 24 days to send is refused with422 broadcast_too_large. List-Idnames the topic, or the sending domain, instead of each broadcast, so every broadcast to the same list carries the same one.- The broadcast page splits delivery by inbox provider.
- The complaint rate behind
403 sending_pausedcounts only mail to providers that report complaints, and only once there are 100 such sends. - A broadcast with emails still waiting — for capacity, or for the plan's
cap to reset — reads
queuedwithsent_atnull until its last email has gone out. It used to readsentas soon as every email was written. - An audience whose plan cap would need more than 24 days is refused with
422 broadcast_too_largeinstead of accepted and left waiting. POST /broadcasts/{id}/cancelworks on a broadcast that is already going out; the response carriescanceled_remaining.