Broadcasts
A broadcast sends one message to every subscribed contact of an audience. SpacePost fans it out into one personalized email per contact, adds unsubscribe links and the List-Unsubscribe headers, skips suppressed addresses and tracks everything like any other email.
Lifecycle
Section titled “Lifecycle”| Status | Meaning |
|---|---|
draft |
Created, editable, not sent. |
queued |
send was called; waiting for its start time (immediately, or scheduled_at). |
sending |
Being fanned out to contacts. |
sent |
Every email has been accepted for delivery. sent_at is set. |
failed |
Fan-out stopped, for example because a quota was reached or the team was paused. |
canceled |
Canceled before sending. |
import { SpacePost } from "@spacepost/node";
const spacepost = new SpacePost(process.env.SPACEPOST_API_KEY);const audienceId = "0192e1c4-0000-7000-8000-0000000000a1";
// 1. Create a draftconst { data: draft, error } = await spacepost.broadcasts.create({ audienceId, name: "October newsletter", from: "Acme <news@acme.com>", replyTo: "support@acme.com", subject: "What's new at Acme, {{{FIRST_NAME|friend}}}", previewText: "Three things we shipped this month", html: ` <h1>Hi {{{FIRST_NAME|there}}},</h1> <p>Here is what is new.</p> <p><a href="{{{SPACEPOST_UNSUBSCRIBE_URL}}}">Unsubscribe</a></p> `,});if (error) throw new Error(error.message);
// 2. Send it now, or schedule itconst { error: sendError } = await spacepost.broadcasts.send(draft.id, { scheduledAt: "in 1 hour" });if (sendError) throw new Error(sendError.message);
// 3. Check progressconst { data: broadcast } = await spacepost.broadcasts.get(draft.id);console.log(broadcast?.status); // "queued" | "sending" | "sent" | "failed"Create a broadcast
Section titled “Create a broadcast”POST /broadcasts creates a draft and returns { "id": "..." }.
audience_idstring (uuid)requiredThe audience to send to.
fromstringrequiredSender on a verified domain.
subjectstringrequired1 to 998 characters. Placeholders are supported.
htmlstringoptionalHTML body. Provide html, text or both before sending.
textstringoptionalPlain-text body.
reply_tostring | string[]optionalUp to 50 addresses.
preview_textstringoptionalUp to 200 characters. Shown after the subject in inboxes; inserted as a hidden preheader.
namestringoptionalUp to 100 characters. An internal label.
Update and delete
Section titled “Update and delete”PATCH /broadcasts/{id} accepts the same fields (all optional) and works only on drafts. DELETE /broadcasts/{id} works on drafts and on broadcasts that are scheduled for the future.
# nowcurl -X POST https://api.spacepost.dev/broadcasts/$ID/send -H "Authorization: Bearer $SPACEPOST_API_KEY"
# scheduled (ISO or natural language, up to 30 days ahead)curl -X POST https://api.spacepost.dev/broadcasts/$ID/send \ -H "Authorization: Bearer $SPACEPOST_API_KEY" -H "Content-Type: application/json" \ -d '{ "scheduled_at": "2026-11-01T09:00:00Z" }'await spacepost.broadcasts.send(id); // nowawait spacepost.broadcasts.send(id, { scheduledAt: "in 1 hour" }); // laterOnly drafts with an html or text body can be sent, from a verified domain. The response is { "id": "..." }.
Readiness checks
Section titled “Readiness checks”Before you send, GET /broadcasts/{id}/readiness returns a deliverability checklist (the dashboard shows the same list in the editor). It is a SpacePost extension and not part of Resend’s API.
{ "ready": true, "checks": [{ "id": "html_size", "level": "warn", "title": "Gmail will clip this message", "detail": "..." }] }| Check | Level | When |
|---|---|---|
sender_domain |
error | The from domain is missing or not verified. |
dmarc |
warn | No v=DMARC1 record at _dmarc.<domain>, or the lookup failed. p=none passes. |
subject |
error / warn | Error when empty. Warning for ALL CAPS, !!! or $$$, or more than 90 characters. |
html_size |
warn | HTML over 102 KB, which Gmail clips. |
images |
warn | Image-only, or four or more images with little text. |
shorteners |
warn | Links through bit.ly, tinyurl.com, t.co and similar. |
spam_phrases |
warn | Common spam wording in the subject or body. |
unsubscribe |
ok | An unsubscribe footer is added automatically when your body has no placeholder, so this never blocks. |
audience |
error | No subscribed contacts. |
role_addresses |
warn | More than 20% of contacts are addresses like info@ or admin@. |
ready is false when any check has level error. Sending then fails with a validation_error (422) that lists what to fix. Warnings never block a send.
Broadcast emails also carry List-Id and Feedback-ID headers (<broadcast id>:<team id>:broadcast:spacepost) so Gmail Postmaster Tools can attribute complaints to a broadcast. Transactional emails do not.
What a recipient gets
Section titled “What a recipient gets”- Personalization is applied to
subject,htmlandtext(Personalization). - Unsubscribe: if your body has no
{{{SPACEPOST_UNSUBSCRIBE_URL}}}/{{{RESEND_UNSUBSCRIBE_URL}}}placeholder, an unsubscribe footer is appended automatically. TheList-UnsubscribeandList-Unsubscribe-Post: List-Unsubscribe=One-Clickheaders are always added. - Preview text becomes a hidden preheader at the top of the HTML.
- Contacts that are unsubscribed, and addresses on your suppression list, are skipped.
Quota and limits
Section titled “Quota and limits”Every recipient counts as one email against your plan quota, and the 100-per-day sandbox applies to new accounts. If the quota is exhausted mid-send the broadcast ends as failed; emails already created are still delivered. Fan-out is resumable if a worker restarts.
Each broadcast email is a normal email: it appears in GET /emails, carries broadcast_id in webhook payloads and can bounce or be opened.