Broadcasts

Draft an email to your audience, send a test to your team, then prepare and send it, all at once or in batches, and read back who got it.

Updated ·8 min read
POST/v1/broadcasts

A broadcast is one email to an audience in a project: the same thing the dashboard's Emails → Broadcasts tab composes. The API drafts it, sends a test to your team, and then sends it in two steps so that what goes out is exactly what you looked at.

Scopes#

Operation Scope
List, retrieve, recipients broadcasts:read
Create, update, delete a draft, send a test broadcasts:write
Prepare, send, schedule, pause, resume, cancel broadcasts:send

Create a draft#

POST /v1/broadcasts
Parameter Type Required Description
project string Yes A prj_ handle.
name string No What the dashboard lists it as. Defaults to the subject.
subject string No Up to 200 characters. Required before it can be sent.
preheader string No The preview line most inboxes show after the subject.
from_name string No Defaults to the project's saved sender.
reply_to string No Defaults to the project's saved reply-to address, then to the workspace owner's email.
content object No { "format", "body" }, below.
audience object No Who gets it, below. Defaults to everyone subscribed, narrowed by the project's default recipients.
delivery object No All at once (default) or in batches, below.

Returns 201 with the broadcast object. A draft never sends anything.

Content#

format is one of:

  • text: a plain-text email, no HTML and no tracking. It reads like a note from a person, which is what you want for founder updates and questions.
  • markdown: formatted, and rendered with the project's email design.
  • html: sent as you wrote it.

Merge tags work in the subject and body, for example {{first_name|there}}, where the part after | is the fallback when a name is missing or is clearly not a name. Every email gets an unsubscribe link and your postal address automatically.

Audience#

{
  "type": "segment",
  "recipients": "verified",
  "segment": "seg_7qk2mn4pr8vx",
  "exclude": {
    "tags": ["tag_3hx8kq2mvp4r"],
    "segments": ["seg_2pv9kd4mxq8r"],
    "broadcasts": ["bcd_9rk2mx4pq7vn"]
  }
}

type is all, segment (with segment) or tag (with tag). recipients chooses, among the people the audience could reach, verified (they clicked the verification link), unverified (waiting for verification: sent a link to verify their email, an expired one included, and not clicked it yet) or all. Someone who was never asked to verify (a form that does not ask, POST /v1/contacts, an import) is neither verified nor waiting, so only all reaches them. Everyone who joins is subscribed, so this only narrows who is emailed, never widens it. Leave it out on a new broadcast and it takes the project's default: verified when every active form asks people to verify their email, otherwise all. On an update, an audience sent without recipients keeps the broadcast's own choice, so replacing the audience never widens who it goes to. exclude leaves out anyone in the listed segments or tags, or anyone who received one of the listed broadcasts. Every value is a handle from this project: one from another project is a 422 naming it, never ignored, because ignoring an exclusion would widen the send.

Update or delete a draft#

PATCH /v1/broadcasts/{broadcast}
DELETE /v1/broadcasts/{broadcast}

PATCH changes only the fields you send. A broadcast can be edited while it is draft, scheduled or failed; after that it is a 409. Only a draft or failed broadcast can be deleted: one that went out stays as the record of who received it.

Send a test#

POST /v1/broadcasts/{broadcast}/test

Sends one [TEST] copy to email, which must belong to a member of your workspace. Merge tags are filled with sample values.

Prepare, then send#

POST /v1/broadcasts/{broadcast}/prepare
POST /v1/broadcasts/{broadcast}/send

prepare runs the checks the dashboard runs before a send and writes nothing. It returns:

{
  "object": "broadcast_preparation",
  "broadcast": "bcd_4kq8mx2rvn7p",
  "ready": true,
  "problems": [],
  "recipients": 214,
  "first_batch": 50,
  "delivery": { "mode": "batches", "batch_size": 50, "interval": "day" },
  "gradual": null,
  "recipient_choice": "verified",
  "unverified_recipients": 0,
  "skipped": { "audience": 240, "unsubscribed": 9, "cannot_receive": 3, "not_subscribed": 4, "not_chosen": 8, "excluded": 2, "shared_address": 0 },
  "send_at": null,
  "name_fallback": { "first_name": { "count": 31, "capped": false } },
  "quota": { "limit": 10000, "used": 1200, "remaining": 8800 },
  "extra_emails": null,
  "sample": [{ "id": "con_7kq3mxrz9dnp", "identifier": "ad***@example.com", "name": "Ada" }],
  "confirmation_token": "eyJiIjoi...",
  "tolerance": 2,
  "expires_at": "2026-09-24T10:17:11+00:00"
}

problems lists anything that would stop the send, in plain words. skipped explains who in the audience will not get it (audience is recipients plus the other six): unsubscribed, cannot_receive (a bounce, a spam report, a block or an address that cannot receive email), not_subscribed (an imported contact added as not subscribed), not_chosen (left out by audience.recipients), excluded and shared_address (two contacts with the same address get one email). recipient_choice is the choice the send uses and unverified_recipients is how many of recipients have not verified their email: show the founder that number before asking for a yes.

gradual is null, or says the send goes out gradually because the sender is new: first_batch, hourly_limit, batches, hours and a message you can show as it is. When it is set, first_batch is the first step of that ramp, whatever delivery says. name_fallback counts recipients greeted by the fallback name; on a list over 10,000 people it counts the first 10,000 by signup and says capped: true. skipped and name_fallback are reused for up to two minutes while the broadcast is unchanged, so on a list that is still growing they can trail recipients slightly. extra_emails is null while the plan covers the send. When it does not, it says how many emails the send is short, the extra emails that would cover it and their price, and links to the dashboard, where the founder approves them: the API never buys them.

To schedule rather than send now, pass send_at to prepare too: the token is bound to the time it was prepared for, so a token prepared for now cannot schedule, and one prepared for a time cannot send now.

send sends now and answers 202 with the broadcast (status is queuing or sending: on its way, not yet delivered). With send_at (ISO 8601; without an offset it is read as UTC) it is scheduled instead and answers 200.

Pass the confirmation_token from prepare and the send only happens if nothing changed since: same content, same audience (including the rules of any segment it names), the same status, the same send_at, prepared by the same key, within 15 minutes, and the same number of recipients within tolerance: exact for a list under 100 people, within 1% above that, so a list that keeps growing while you read the numbers does not refuse forever. For a batched broadcast that is the whole audience, not just the first batch. Otherwise it is a 422 on confirmation_token that says what changed, and nothing is sent. The token is optional over REST, for an integration with its own review step; over MCP it is required.

Send a broadcast in batches#

Set delivery to send to a slice of the audience at a time:

{ "mode": "batches", "batch_size": 50, "interval": "day" }

The first batch goes out when you send; after that, one batch of batch_size people every hour or day, oldest signups first, until everyone has it. The audience is read live at each batch, so someone who signs up mid-send joins a later batch, and nobody ever gets it twice. stats.batches shows how many batches went out, how many people are left and when the next one is due.

If a batch bounces or draws complaints past a safe rate, the broadcast pauses itself and failure_reason says why. Fix the list, then resume it.

Pause, resume, cancel#

POST /v1/broadcasts/{broadcast}/pause
POST /v1/broadcasts/{broadcast}/resume
POST /v1/broadcasts/{broadcast}/cancel

Each answers with the broadcast in its new state, or a 409 naming its current state when there was nothing to do. Pausing keeps a batched broadcast's schedule; resuming sends to everyone who has not had it yet; canceling is final. Emails already sent stand.

List broadcasts and recipients#

GET /v1/broadcasts?project=prj_…&status=sent
GET /v1/broadcasts/{broadcast}/recipients?status=bounced

Both are paginated. A recipient carries the contact's con_… handle, its address and a status of queued, sent, delivered, bounced, complained, suppressed, failed or canceled, the same values a contact's email history uses. canceled means the broadcast was canceled before it reached them.

In a list, stats.batches.remaining is null: it is a live count of the audience, so it is only computed when you retrieve one broadcast.

A follow-up that skips people who replied#

  1. Save a segment of people who got the first broadcast at least five days ago: { "field": "received_broadcast", "operator": "in", "value": ["bcd_4kq8mx2rvn7p"], "days": 5 } (segments).
  2. Create the follow-up with that segment as its audience, excluding a replied tag and a segment of people who already joined.
  3. Prepare it, check the count, and send.

The broadcast object#

{
  "object": "broadcast",
  "id": "bcd_4kq8mx2rvn7p",
  "project": "prj_7Qk2mN4pR8",
  "name": "Launch week",
  "status": "sent",
  "subject": "We launch Tuesday",
  "preheader": null,
  "from_name": "Ada from Acme",
  "reply_to": "ada@acme.com",
  "content": { "format": "markdown", "body": "Hi {{first_name|there}} ..." },
  "audience": { "type": "all", "recipients": "all", "segment": null, "tag": null, "exclude": { "segments": [], "tags": [], "broadcasts": [] } },
  "delivery": { "mode": "all", "batch_size": null, "interval": null },
  "scheduled_at": null,
  "sent_at": "2026-09-24T10:20:00+00:00",
  "failure_reason": null,
  "stats": {
    "recipients": 214, "sent": 214, "delivered": 209, "bounced": 2, "complained": 0,
    "unsubscribed": 3, "pending": 0, "failed": 0, "suppressed": 0, "canceled": 0, "batches": null
  },
  "created_at": "2026-09-24T10:02:11+00:00",
  "updated_at": "2026-09-24T10:20:00+00:00",
  "url": "/v1/broadcasts/bcd_4kq8mx2rvn7p"
}

Next#

  • Segments, to build the audience a broadcast targets
  • Tags, to label the people a broadcast should reach or skip

Common questions

Which scopes do broadcasts need?

broadcasts:read to list them, read results and recipients. broadcasts:write to create and edit drafts and send a test to your team. broadcasts:send to prepare, send, schedule, pause, resume and cancel. Only a teammate who may send campaigns can create a key with broadcasts:send.

Can a broadcast reach someone who unsubscribed?

No. Only subscribed contacts are ever emailed. Unsubscribed, bounced, complained and not-subscribed contacts are skipped whatever the audience says, and prepare tells you how many.

Can I email people who have not verified their email?

Yes. Everyone who joins is subscribed, so they can be emailed whether or not they verified. Set audience.recipients to verified, unverified or all. A new broadcast takes the project default: verified when every active form asks people to verify, otherwise all.

Why does prepare say my first broadcast goes out gradually?

Your first emails go out gradually. Until your sending record is established, a broadcast to more than 50 people starts with 50 and grows each hour, up to 1,000 an hour. prepare returns gradual with the first batch, the hourly limit and a plain sentence you can show the founder.

Can one person get the same broadcast twice?

No. Each broadcast writes one ledger row per person and the database refuses a second. Batches, retries, a resume after a pause and a send that is called twice all land on that same row.

Why is my confirmation token refused?

A token lasts 15 minutes and only while nothing changed: the subject, sender, content, audience, delivery and status must match, send_at must be the one it was prepared with (or absent for sending now), the recipient count must not have moved past its tolerance (exact under 100 people, within 1% above that), and it must be spent by the key that prepared it. Call prepare again and look at the new numbers.

Are opens and clicks reported?

Not yet. Tracking is off, so the API reports sent, delivered, bounced, complained and unsubscribed, and never a zero that looks like nobody opened.

Can I write a designed block email over the API?

No. Block emails are designed in the dashboard composer. The API reads them as format: blocks with a null body and can send them, but it writes markdown, text or html.

Was this page helpful?
curl https://api.getlaunchlist.com/v1/broadcasts \\
  -H "Authorization: Bearer llk_live_8xKq3Rp9Wm2Nz7Vt6yL1cG5dH0jA4sE8uNqB2fXk" \\
  -H "Content-Type: application/json" \\
  -d '{
    "project": "prj_7Qk2mN4pR8",
    "subject": "Quick question, {{first_name|there}}",
    "content": {
      "format": "text",
      "body": "Hi {{first_name|there}},\n\nWhat made you sign up? Just hit reply.\n\nAda"
    },
    "audience": { "type": "tag", "tag": "tag_9mvd4rq2ph7k" }
  }'
{
  "object": "broadcast",
  "id": "bcd_4kq8mx2rvn7p",
  "project": "prj_7Qk2mN4pR8",
  "name": "Quick question, {{first_name|there}}",
  "status": "draft",
  "subject": "Quick question, {{first_name|there}}",
  "content": { "format": "text", "body": "Hi {{first_name|there}}, ..." },
  "audience": { "type": "tag", "recipients": "all", "segment": null, "tag": "tag_9mvd4rq2ph7k", "exclude": { "segments": [], "tags": [], "broadcasts": [] } },
  "delivery": { "mode": "all", "batch_size": null, "interval": null },
  "stats": { "recipients": 0, "sent": 0, "delivered": 0, "bounced": 0, "batches": null },
  "url": "/v1/broadcasts/bcd_4kq8mx2rvn7p"
}