A broadcast sends a single email to many recipients — a newsletter, a product announcement, a campaign. Point it at a contact list or an inline recipient set, write your content once with {{ variables }}, and Sendara fans it out in the background, personalizing each copy and tracking delivery as it goes.
Every recipient becomes a normal message that flows through the same pipeline as POST /v1/send — so suppression, unsubscribe handling, spend caps, idempotency, and provider events all apply automatically. The endpoints live under /v1/broadcasts.
POST /v1/send instead — broadcasts are for one message to many people.The broadcast lifecycle
A broadcast moves through a small set of states. You create it as a draft, then send it now or schedule it; the worker flips it to sending while it fans out, then to a terminal sent (or failed) when every recipient has been processed.
| Status | Meaning |
|---|---|
| draft | Created but not yet sending. Can be sent, scheduled, edited via re-create, or deleted. |
| scheduled | A future scheduled_at is set. Sends automatically at that time. Can still be cancelled. |
| sending | Fan-out is in progress. Recipients are being resolved and sent concurrently. |
| sent | Every recipient has been handed off. Terminal — check stats for delivery outcomes. |
| cancelled | A draft or scheduled broadcast was cancelled before it started sending. |
| failed | The fan-out could not start (e.g. the audience failed to resolve) or every recipient failed. |
Create a broadcast
POST /v1/broadcasts creates a broadcast. Give it a from_email on a verified domain, some content, and an audience. Set send_now: true to start fan-out immediately, or a future scheduled_at to send later — omit both to keep it as a draft you send explicitly.
Request body
template_id, or a subject together with a body_html or body_text — and an audience — either an audience_list_id or a non-empty recipients array. Missing either returns invalid_request (400).The response is the created broadcast. When you pass send_now: true it is returned with status: "sending" and fan-out has already begun; poll get for live counts.
{
"id": "bc_a1b2c3",
"name": "October newsletter",
"status": "sending",
"from_email": "news@mail.acme.com",
"subject": "What shipped in October, {{ first_name }}",
"message_type": "marketing",
"audience_list_id": "list_9f21",
"total_recipients": 0,
"sent_count": 0,
"failed_count": 0,
"test_mode": false,
"created_at": "2026-06-14T10:00:00Z",
"updated_at": "2026-06-14T10:00:00Z"
}total_recipients is 0 until the worker has resolved the audience. Once fan-out starts it is set to the deduped, consent-filtered recipient count — then sent_count and failed_count climb as each message is handed off.Audiences vs. inline recipients
There are two ways to target a broadcast. Use whichever fits — you can mix neither, but not both is required.
A contact list
Pass an audience_list_id to send to a saved contact list. Both static and dynamic (rule-based) lists are supported — dynamic lists are resolved at send time. Each contact's attributes become template variables automatically, so first_name, last_name, email, and any custom attributes are available in your {{ … }} placeholders.
Inline recipients
Pass a recipients array for an ad-hoc send — no list needed. Each entry is { email, data }, where datasupplies that recipient's variables.
curl https://api.sendara.dev/v1/broadcasts \
-H "Authorization: Bearer sk_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"name": "Beta invites",
"from_email": "team@mail.acme.com",
"subject": "You are in, {{ first_name }}",
"body_html": "<p>Hi {{ first_name }}, your access code is {{ code }}.</p>",
"recipients": [
{ "email": "ada@acme.com", "data": { "first_name": "Ada", "code": "A1B2" } },
{ "email": "grace@acme.com", "data": { "first_name": "Grace", "code": "C3D4" } }
],
"send_now": true
}'Personalization
Inline subject, body_html, and body_text support lightweight {{ variable }}substitution. For a contact list, variables come from each contact's fields and attributes; for inline recipients, they come from the per-recipient data object.
template_id, which renders each recipient through the full template engine.Scheduling
Set a future scheduled_at (RFC 3339) and the broadcast is created with status: "scheduled". A background sweeper claims due broadcasts and fans them out at the scheduled time — no cron on your side. Until then you can cancel it.
curl https://api.sendara.dev/v1/broadcasts \
-H "Authorization: Bearer sk_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"name": "Black Friday teaser",
"from_email": "news@mail.acme.com",
"subject": "Something is coming",
"body_html": "<p>Mark your calendar.</p>",
"audience_list_id": "list_9f21",
"scheduled_at": "2026-11-27T14:00:00Z"
}'scheduled_at in the past (or omitted) does not schedule anything — the broadcast stays a draft until you send it. Pair a future scheduled_at with send_now and the schedule wins.Send a draft
If you created a broadcast without send_now — for example to review it first — kick off fan-out with POST /v1/broadcasts/{id}/send. It accepts a broadcast in draft or scheduled state, returns 202 Accepted with status: "sending", and begins sending immediately (overriding any future schedule).
curl -X POST https://api.sendara.dev/v1/broadcasts/bc_a1b2c3/send \
-H "Authorization: Bearer sk_live_xxx"draft or scheduled. Calling /send on a broadcast that is already sending, sent, cancelled, or failed returns invalid_state (409).Cancel a broadcast
POST /v1/broadcasts/{id}/cancel stops a draft or scheduled broadcast before it starts, moving it to cancelled. Once a broadcast is sending it has already begun handing recipients to the send pipeline and can no longer be cancelled.
curl -X POST https://api.sendara.dev/v1/broadcasts/bc_a1b2c3/cancel \
-H "Authorization: Bearer sk_live_xxx"scheduled returns invalid_state (409); an unknown id returns not_found (404).List broadcasts
GET /v1/broadcasts returns your broadcasts, newest first, under a broadcasts array. Page with limit (default 50, max 200) and offset.
curl "https://api.sendara.dev/v1/broadcasts?limit=20" \
-H "Authorization: Bearer sk_live_xxx"{
"broadcasts": [
{
"id": "bc_a1b2c3",
"name": "October newsletter",
"status": "sent",
"from_email": "news@mail.acme.com",
"message_type": "marketing",
"total_recipients": 1840,
"sent_count": 1840,
"failed_count": 0,
"test_mode": false,
"created_at": "2026-06-14T10:00:00Z",
"completed_at": "2026-06-14T10:03:11Z"
}
]
}Get a broadcast with stats
GET /v1/broadcasts/{id} returns the broadcast plus a live stats object aggregated from the per-recipient messages and their provider events. This is what you poll while a broadcast is sending and how you measure results after.
curl https://api.sendara.dev/v1/broadcasts/bc_a1b2c3 \
-H "Authorization: Bearer sk_live_xxx"{
"broadcast": {
"id": "bc_a1b2c3",
"name": "October newsletter",
"status": "sent",
"from_email": "news@mail.acme.com",
"message_type": "marketing",
"audience_list_id": "list_9f21",
"total_recipients": 1840,
"sent_count": 1840,
"failed_count": 0,
"test_mode": false,
"started_at": "2026-06-14T10:00:02Z",
"completed_at": "2026-06-14T10:03:11Z",
"created_at": "2026-06-14T10:00:00Z",
"updated_at": "2026-06-14T10:03:11Z"
},
"stats": {
"total": 1840,
"queued": 0,
"sent": 1840,
"delivered": 1798,
"opened": 642,
"bounced": 31,
"complained": 2,
"failed": 0
}
}sent_count / failed_count on the broadcast track how many recipients were handed to the send pipeline. The stats object reflects what happened after — delivered, opened, bounced, and complained are derived from provider events and keep updating after status reaches sent.Delete a broadcast
DELETE /v1/broadcasts/{id} removes a broadcast and returns 204 No Content. You can delete a broadcast in a settled state — draft, cancelled, sent, or failed. A broadcast that is scheduled or sending cannot be deleted; cancel it first.
curl -X DELETE https://api.sendara.dev/v1/broadcasts/bc_a1b2c3 \
-H "Authorization: Bearer sk_live_xxx"One-shot bulk send
When you don't need a draft step, POST /v1/send/bulk creates a broadcast and starts fan-out in a single call — the create + send shortcut. It takes the same body (minus send_now and scheduled_at, which don't apply) and returns 202 Accepted with the broadcast to poll.
curl https://api.sendara.dev/v1/send/bulk \
-H "Authorization: Bearer sk_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"from_email": "news@mail.acme.com",
"subject": "News from Acme",
"body_html": "<h1>Hi {{ first_name }}</h1>",
"audience_list_id": "list_9f21"
}'/v1/send/bulk for fire-and-forget campaigns from a script or backend job; reach for the /v1/broadcasts create-then-send flow when you want to review, schedule, or cancel before anything goes out.Suppression, consent & idempotency
Because every recipient runs through the standard send pipeline, the same safety rails as POST /v1/sendapply automatically — you don't opt in:
- Suppression — addresses on your suppression list (hard bounces, complaints, manual entries) are skipped.
- Consent — for
marketingbroadcasts, contacts who have unsubscribed are excluded, and anUnsubscribeheader is added to every message. - Idempotency — each recipient gets a deterministic idempotency key derived from the broadcast id, so re-running a broadcast (or a worker retry) never double-sends to the same person.
- Spend caps — if a send would exceed your spend cap, that recipient fails with
spend_cap_exceededand is counted infailed_count; the rest still go out.
Testing broadcasts
Use a sandbox key (sk_test_) to dry-run a broadcast end to end without sending real email or being billed. Resolution, dedupe, personalization, and stats all run; including the sandbox addresses delivered@, bounced@, and complained@ in your recipients drives those outcomes (and fires webhooks) so you can verify your tracking. Sandbox broadcasts are flagged with test_mode: true.
recipients array of your own verified test addresses — a safe rehearsal for subject lines, rendering, and links.Errors
Broadcast endpoints use the standard error envelope. The codes you'll see most:
| Code | Status | When it fires |
|---|---|---|
| invalid_request | 400 | Missing from_email, no content (template or subject + body), or no audience (list or recipients). |
| invalid_state | 409 | Sending or cancelling a broadcast that isn't in a valid state for that action (e.g. cancelling one that's already sending). |
| not_found | 404 | No broadcast with that id exists for your account. |
| unauthorized | 401 | Missing or invalid API key. |
| forbidden | 403 | Key lacks the required scope — read to list/get, send to create/send/cancel/delete. |
from_email, a spend-cap hit) don't fail the broadcast — they're counted in failed_count. The broadcast only ends in failedwhen the audience can't be resolved at all, or every single recipient fails.Scopes
Listing and reading broadcasts requires a key with the read scope. Creating, sending, cancelling, deleting, and one-shot bulk sends require the send scope. See Authentication for how scopes work.