Create a broadcast

Search Documentation

Search across all developer documentation

broadcasts

Create a broadcast

POST /v1/broadcasts
Auth required api.broadcasts.manage

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 optionally layout (a layout template's slug from GET /v1/templates?kind=layout, builtin for the SendOps built-in, or default/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 to template_data, then to the field's own {{else}} fallback; set a template_data default 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 coded 422.
  • Template mode — supply template_id (the template slug / file name from GET /v1/templates, e.g. welcome.html, not a UUID) plus optional template_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 optional text. 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@ sender. Accepts a bare address (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.

201 Broadcast created (draft) application/json
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@ sender. Always a bare address — a formatted 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.

401 Missing, malformed, or unknown API key application/problem+json
403 Key lacks the required scope or plan limit violated application/problem+json
422 A query parameter, path value or body field failed validation. The body is a 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
429 Per-org rate limit exceeded application/problem+json
500 Unexpected server-side failure. The code is internal_error. The request_id field can be quoted to SendOps support to investigate. application/problem+json