Create a broadcast
Composes a broadcast in the draft state, in one of three
mutually-exclusive content modes:
- Composed mode — to write a new email, supply
body(the message: HTML, Handlebars merge fields allowed) and optionallylayout(a layout template's slug fromGET /v1/templates?kind=layout,builtinfor the SendOps built-in, ordefault/omitted for the org's default layout). The body is placed into the layout's{{> content}}slot; the unsubscribe link and postal address are added automatically when the organization has set an unsubscribe base URL and a postal address. Merge fields bind to the contact attribute of the same name, falling back totemplate_data, then to the field's own{{else}}fallback; set atemplate_datadefault or a fallback for a field, not both, because the default always wins. Unsafe markup (<script>, event handlers, partials, a<body>of its own) is refused with a coded422. - Template mode — supply
template_id(the template slug / file name fromGET /v1/templates, e.g.welcome.html, not a UUID) plus optionaltemplate_version(defaults to latest). Only an email template (kind: email) can be sent this way. Inject a large rendered HTML block as a triple-stache variable ({{{body_html}}}) so it is not HTML-escaped. - Inline mode — supply
html(raw, fully-rendered content) plus optionaltext. Inline HTML must satisfy the same caps as templates (≤500 KB, valid UTF-8, at least one HTML tag).
Supply exactly one of template_id, html and body; none or more
than one is a 422 validation_failed. layout without body is a
422. template_data is the merge defaults in every mode (shared
merge data; per-recipient contact attributes are overlaid at send). subject and preview_text are owned by the
broadcast (overridable per send). topic_id, when given, is the topic
name from GET /v1/topics (not a UUID).
Sender. from_identity, when given, must be one of the org's verified
identities (see GET /v1/identities); omit it to send from the default
no-reply@<verified-domain> address. It may be a bare address
(no-reply@example.com) or an RFC 5322 formatted one
(Acme <no-reply@example.com>) — a formatted value is split, and its
display name is used as from_name when from_name is not given
separately. from_name is the sender name the recipient sees in their
inbox; without it an explicit from_identity is labelled with your
organization's name, so a broadcast is never sent as a bare address.
Replies. reply_to, when given, is written as an RFC 5322
Reply-To: header on every message, so replies go there instead of to
the from-address. Omit it and no Reply-To header is sent at all — mail
clients then reply to the from-address, which is the behaviour that
predates the field. Unlike from_identity it does NOT have to be a
verified identity: point it at an address on one of your receiving
domains and SendOps matches the replies back to this broadcast.
Does not send — call POST /v1/broadcasts/{id}/send. Supports the
Idempotency-Key header: a duplicate key within 24 h replays the original
response.
Request body
Content type: application/json
name string required A label for the broadcast.
template_id string optional Template mode — the template slug (file name) the broadcast renders, i.e. the slug from GET /v1/templates (e.g. welcome.html), not an internal UUID. An unknown slug — or a layout's — is a 422. Mutually exclusive with html and body.
template_version integer<int32> optional Template mode — the integer version to pin. Optional: omit (or send 0) to pin the latest version.
template_data object optional Merge defaults, in every mode — shared merge data overlaid under each recipient's contact attributes. In template mode, inject a large rendered HTML block as a triple-stache variable ({{{body_html}}}) to avoid HTML-escaping.
body string optional Composed mode — the email body: HTML with Handlebars merge fields ({{first_name}}, {{#if plan}}…{{/if}}, and fallbacks written as {{#if first_name}}{{first_name}}{{else}}there{{/if}}), placed into layout. Not a whole document: no <html>/<body>, no <script>, no event handlers, no partials. A refusal is a 422 carrying the code, field and position. Mutually exclusive with template_id and html.
layout string optional Composed mode only — the layout body is placed into: a layout template's slug (GET /v1/templates?kind=layout), builtin for the SendOps built-in, or default (the same as omitting it) for the org's default layout. Given without body is a 422.
html string optional Inline mode — raw, fully-rendered HTML. Mutually exclusive with template_id and body. Same caps as templates (≤500 KB, valid UTF-8, ≥1 HTML tag).
text string optional Inline mode — optional plain-text alternative.
audience_kind string enum required Whether audience_id references a List or a Segment.
One of: list, segment
audience_id string required The List or Segment to send to, given by its org-unique key (the identifier GET /v1/lists and GET /v1/segments expose) or its UUID. audience_kind scopes the lookup, so a List key is not accepted when the kind is segment and vice versa — a List and a Segment may hold the same key. An unknown handle is a 422.
topic_id string | null optional Optional opt-out topic, given by its name (the identifier GET /v1/topics exposes), not an internal UUID. An unknown name is a 422. Omit it to use the workspace default topic; send null to send with no topic (recipients can then only unsubscribe from everything). A workspace not yet on SendOps-owned topics has no default topic, and there omitting it also means no topic.
from_identity string optional Optional. A verified identity to send from (see GET /v1/identities); omit for the default no-reply@no-reply@example.com) or an RFC 5322 formatted one (Acme <no-reply@example.com>); a formatted value is split into the address and from_name, and an explicit from_name wins over the name parsed here.
from_name string optional Optional. The sender name the recipient sees in their inbox, e.g. Acme. Without it an explicit from_identity is labelled with your organization's name, so a broadcast never arrives from a bare address. At most 128 characters and must not contain line breaks.
Constraints: length 0–128
reply_to string optional Optional. Where replies go, e.g. replies@example.com. Sent as an RFC 5322 Reply-To: header on every message; omit it and no such header is written, so replies reach the from-address. Accepts a bare address or a formatted Name <address>. Unlike from_identity it need NOT be a verified identity — use an address on one of your receiving domains so SendOps can capture the replies. At most 320 characters and must not contain line breaks.
Constraints: length 0–320
subject string required The email subject (overrides any template subject in inline mode).
preview_text string optional Optional inbox preview text. Sent as a hidden preheader for inline and composed content. Ignored in template mode.
acknowledge_no_topic boolean optional Deprecated. Workspaces moving to SendOps-owned topics no longer need it and ignore it; until that change reaches your workspace, a topic-less broadcast still needs it set to true.
Responses
Errors follow the RFC 7807 problem format — see the error reference.
id string<uuid> required name string required content_mode string enum required The content source. template renders a pinned workspace template; inline sends caller-supplied raw HTML (not echoed back); composed places a body (not echoed back) into layout.
One of: template, inline, composed
template_id string | null optional The template slug (file name) the broadcast renders — the same identifier GET /v1/templates exposes as slug, not an internal UUID. Null outside template mode.
layout string | null optional Composed mode — the layout the body is placed into: a layout template's slug, or builtin for the SendOps built-in. Resolved when the broadcast was created, so a later change of the org's default does not move it. Null outside composed mode, and when the layout template has since been deleted.
template_version integer<int32> | null optional The pinned integer template version (null in inline mode).
audience_kind string enum required Whether audience_id references a List or a Segment.
One of: list, segment
audience_id string required The List or Segment id. Responses echo the id; either the id or the org-unique key is accepted on the way in.
subject string required The broadcast owns the subject (overridable per send).
preview_text string optional from_identity string | null optional The explicit verified from-address, or null for the default no-reply@Name <address> supplied at create is split on the way in.
from_name string | null optional The sender display name shown in the recipient's inbox, or null when the organization's name is used as the label.
reply_to string | null optional Where replies go, or null when the broadcast has none — then no Reply-To header is sent and replies reach the from-address. Stored normalized: a bare address, or Name <address> when a display name was supplied.
default_template_data object optional DefaultTemplateData applied where a recipient lacks an attribute value.
topic_id string | null optional The topic name (the same identifier GET /v1/topics exposes), or null for a topic-less broadcast. Not an internal UUID.
schedule_at string<date-time> | null optional The scheduled UTC fire time, or null for a send-now broadcast.
state string enum required failed: the send could not start and nothing was sent; send it
again with POST /v1/broadcasts/{id}/send. paused: the send
stopped part-way on an SES account or configuration problem
(failure_reason says which) and is resumed from the dashboard once
it is fixed. partially_failed: the send completed, but some
recipients failed or their delivery could not be confirmed
(unknown_count).
One of: draft, scheduled, sending, sent, partially_failed, cancelled, failed, paused
failure_reason string | null optional Why a failed or paused broadcast stopped, as a plain-English sentence safe to show to people. Null in every other state.
recipient_count integer<int64> | null optional Mailable snapshot size, stamped at send (null before send).
sent_count integer<int64> | null optional failed_count integer<int64> | null optional unknown_count integer<int64> | null optional Recipients whose delivery could not be confirmed. They are never sent again automatically. Null for a broadcast sent before this count existed, and until a send completes.
started_at string<date-time> | null optional completed_at string<date-time> | null optional created_at string<date-time> required updated_at string<date-time> required summary object | null optional The outcome rollup, present once a send has finalized the cached counts; null on a draft, scheduled or failed broadcast that has never sent.
Fields of summary:
audience_size integer<int64> required Snapshot audience size before the consent filter.
mailable integer<int64> required Recipients that passed the consent filter and were attempted (sent + failed + unknown).
sent integer<int64> required failed integer<int64> required unknown integer<int64> required Recipients whose delivery could not be confirmed. 0 for a broadcast sent before this count existed.
filtered integer<int64> required Recipients dropped by the consent filter.
replies integer<int64> required How many distinct people replied to this broadcast, auto-replies excluded. The odd one out here: the other six are derived from the cached counts, this one is counted live from the inbound message log on every detail read, because replies keep arriving for days after a send is finalized. Requires inbound receiving; 0 without it.
tags array<object> optional Org-side tags/folders applied to this broadcast (managed via the dashboard). Editable regardless of send state, so historical broadcasts stay organizable. Filter the collection with ?tag=.
Each entry in tags:
id string<uuid> required name string required Full folder path (e.g. Marketing/Newsletters).
key string required Normalized slug handle of the name.
color string optional Chip colour token (chart-1..chart-5), or empty.
description string optional created_at string<date-time> required updated_at string<date-time> required requires array<object> optional Everything this definition still depends on, with each dependency's state. Present on the write responses and on the detail read (the polling surface); absent on the collection read. An empty array means nothing is outstanding — see V1Requirement.
Each entry in requires:
kind string enum required The namespace key is looked up in. Branch on this; never parse detail.
One of: template, segment, list, topic, identity, channel, attribute, activity_property, asset
key string required The identifier as the definition names it — a template name, a segment id or key, a from-address. Empty for a requirement about the ABSENCE of a reference: a send with no topic at all names no topic.
status string enum required ok — resolves and is usable now.
missing — nothing in this organization answers to that key. Correct
the reference, or create the thing.
not_ready — it exists, in a state that cannot be used yet. Usually
a wait, and retrying the identical request is the move. Treating this
as missing sends you hunting for a typo that is not there.
One of: ok, missing, not_ready
detail string required The state, in the words that say which state — deploy_state=pending, member_count=null (evaluation queued). For display and logs; do not parse it.
fix string required The call that clears this requirement, as an HTTP route where there is one and as a plain instruction where there is not (a template deploy is waited for, not called). Empty on an ok entry.
validation_failed Problem. When the refusal is about a
reference — a segment key, template slug, topic or attribute name the
organization does not have — it additionally carries validation_code,
field, line/column, missing, candidates and next_step, so a
client can correct the call without a second round of guessing. See
ValidationProblem.
application/problem+json code is internal_error. The
request_id field can be quoted to SendOps support to investigate.
application/problem+json