Broadcasts
A broadcast is one email sent to the subscribers of one list. Its content is a Maily document, written in the dashboard’s editor or converted from a markdown file by the CLI, and it renders in its app’s look (logo, colours and font, set in the app’s Brand tab or with apps.look.set) with the unsubscribe footer and the app’s postal address below it. A variable prints the subscriber’s trait of that name, or its fallback when they have none. Any member can draft, preview and send a test to their own address; sending to the list happens in the dashboard only. All routes require session-token authentication.
POST /v1/broadcasts
Section titled “POST /v1/broadcasts”Create a broadcast draft
from falls back to the app’s default sender, and is refused when there is neither; it must be on a verified sending domain. replyTo falls back the same way. A doc left out starts the draft blank, and a subject can wait until it is sent.
Auth: Session token.
Request body
{ "data": { "appId": "string", "list": "string", "subject": "string", "previewText": "string", "from": "string", "doc": { "type": "doc", "attrs": { "key": {} }, "content": [ {} ] }, "docHash": "string", "header": "logo_and_name" }}| Field | Type | Required | Constraints |
|---|---|---|---|
data |
object | yes | |
data.appId |
string | yes | length 0–200 |
data.list |
string | yes | length 1–∞ |
data.subject |
string | no | length 0–200 |
data.previewText |
string | null | no | |
data.from |
string | no | length 0–200 |
data.replyTo |
string (email) | null | no | |
data.doc |
object | no | |
data.doc.type |
“doc” | yes | |
data.doc.attrs |
record<string, any> | no | |
data.doc.content |
any[] | yes | |
data.docHash |
string | no | |
data.header |
“logo_and_name” | “name” | “logo” | “none” | no |
Response 201
{ "data": { "id": "string", "appId": "string", "list": "string", "subject": "string", "previewText": "string", "from": "string", "replyTo": "string", "doc": { "type": "doc", "attrs": { "key": {} }, "content": [ {} ] }, "docHash": "string", "header": "logo_and_name", "status": "draft", "scheduledAt": "2026-01-15T09:30:00.000Z", "startedAt": "2026-01-15T09:30:00.000Z", "finishedAt": "2026-01-15T09:30:00.000Z", "failureReason": "string", "counts": { "recipients": 42, "sent": 42, "delivered": 42, "skipped": 42, "failed": 42 }, "createdBy": "string", "createdAt": "2026-01-15T09:30:00.000Z", "updatedAt": "2026-01-15T09:30:00.000Z", "url": "string" }, "meta": { "requestId": "string", "nextCursor": "string", "hasMore": true, "warnings": [ { "code": "string", "message": "string", "suggestion": "string" } ] }}| Field | Type | Required | Constraints |
|---|---|---|---|
data |
object | yes | |
data.id |
string | yes | |
data.appId |
string | yes | |
data.list |
string | yes | |
data.subject |
string | yes | |
data.previewText |
string | null | yes | |
data.from |
string | yes | |
data.replyTo |
string | null | yes | |
data.doc |
object | yes | |
data.doc.type |
“doc” | yes | |
data.doc.attrs |
record<string, any> | no | |
data.doc.content |
any[] | yes | |
data.docHash |
string | null | yes | |
data.header |
“logo_and_name” | “name” | “logo” | “none” | yes | |
data.status |
“draft” | “scheduled” | “sending” | “sent” | “cancelled” | “failed” | yes | |
data.scheduledAt |
string (date-time) | null | yes | |
data.startedAt |
string (date-time) | null | yes | |
data.finishedAt |
string (date-time) | null | yes | |
data.failureReason |
string | null | yes | |
data.counts |
object | yes | |
data.counts.recipients |
integer | yes | |
data.counts.sent |
integer | yes | |
data.counts.delivered |
integer | yes | |
data.counts.skipped |
integer | yes | |
data.counts.failed |
integer | yes | |
data.createdBy |
string | yes | |
data.createdAt |
string (date-time) | yes | |
data.updatedAt |
string (date-time) | yes | |
data.url |
string | yes | |
meta |
object | no | |
meta.requestId |
string | no | |
meta.nextCursor |
string | null | no | |
meta.hasMore |
boolean | no | |
meta.warnings |
object[] | no | |
meta.warnings[] |
object | no | |
meta.warnings[].code |
string | yes | |
meta.warnings[].message |
string | yes | |
meta.warnings[].suggestion |
string | no |
Errors follow the shared envelope and code table.
GET /v1/broadcasts
Section titled “GET /v1/broadcasts”List broadcasts
Newest first.
Auth: Session token.
Query parameters
{ "cursor": "string", "limit": 50, "appId": "string", "status": "draft"}| Field | Type | Required | Constraints |
|---|---|---|---|
cursor |
string | no | |
limit |
integer | no | default 50; 1 ≤ x ≤ 100 |
appId |
string | no | length 0–200 |
status |
“draft” | “scheduled” | “sending” | “sent” | “cancelled” | “failed” | no |
Response 200
{ "data": [ { "id": "string", "appId": "string", "list": "string", "subject": "string", "previewText": "string", "from": "string", "replyTo": "string", "doc": { "type": "doc", "attrs": { "key": {} }, "content": [ {} ] }, "docHash": "string", "header": "logo_and_name", "status": "draft", "scheduledAt": "2026-01-15T09:30:00.000Z", "startedAt": "2026-01-15T09:30:00.000Z", "finishedAt": "2026-01-15T09:30:00.000Z", "failureReason": "string", "counts": { "recipients": 42, "sent": 42, "delivered": 42, "skipped": 42, "failed": 42 }, "createdBy": "string", "createdAt": "2026-01-15T09:30:00.000Z", "updatedAt": "2026-01-15T09:30:00.000Z", "url": "string" } ], "meta": { "requestId": "string", "nextCursor": "string", "hasMore": true, "warnings": [ { "code": "string", "message": "string", "suggestion": "string" } ] }}| Field | Type | Required | Constraints |
|---|---|---|---|
data |
object[] | yes | |
data[] |
object | yes | |
data[].id |
string | yes | |
data[].appId |
string | yes | |
data[].list |
string | yes | |
data[].subject |
string | yes | |
data[].previewText |
string | null | yes | |
data[].from |
string | yes | |
data[].replyTo |
string | null | yes | |
data[].doc |
object | yes | |
data[].doc.type |
“doc” | yes | |
data[].doc.attrs |
record<string, any> | no | |
data[].doc.content |
any[] | yes | |
data[].docHash |
string | null | yes | |
data[].header |
“logo_and_name” | “name” | “logo” | “none” | yes | |
data[].status |
“draft” | “scheduled” | “sending” | “sent” | “cancelled” | “failed” | yes | |
data[].scheduledAt |
string (date-time) | null | yes | |
data[].startedAt |
string (date-time) | null | yes | |
data[].finishedAt |
string (date-time) | null | yes | |
data[].failureReason |
string | null | yes | |
data[].counts |
object | yes | |
data[].counts.recipients |
integer | yes | |
data[].counts.sent |
integer | yes | |
data[].counts.delivered |
integer | yes | |
data[].counts.skipped |
integer | yes | |
data[].counts.failed |
integer | yes | |
data[].createdBy |
string | yes | |
data[].createdAt |
string (date-time) | yes | |
data[].updatedAt |
string (date-time) | yes | |
data[].url |
string | yes | |
meta |
object | no | |
meta.requestId |
string | no | |
meta.nextCursor |
string | null | no | |
meta.hasMore |
boolean | no | |
meta.warnings |
object[] | no | |
meta.warnings[] |
object | no | |
meta.warnings[].code |
string | yes | |
meta.warnings[].message |
string | yes | |
meta.warnings[].suggestion |
string | no |
Errors follow the shared envelope and code table.
POST /v1/broadcasts/preview
Section titled “POST /v1/broadcasts/preview”Render a doc without saving it
Renders a Maily doc as a broadcast would be, in the look of the app named (or the default look), with every variable at its fallback and the unsubscribe footer. Writes nothing.
Auth: Session token.
Request body
{ "data": { "doc": { "type": "doc", "attrs": { "key": {} }, "content": [ {} ] }, "appId": "string", "subject": "string", "previewText": "string", "header": "logo_and_name" }}| Field | Type | Required | Constraints |
|---|---|---|---|
data |
object | yes | |
data.doc |
object | yes | |
data.doc.type |
“doc” | yes | |
data.doc.attrs |
record<string, any> | no | |
data.doc.content |
any[] | yes | |
data.appId |
string | no | length 0–200 |
data.subject |
string | no | length 0–200 |
data.previewText |
string | no | length 0–200 |
data.header |
“logo_and_name” | “name” | “logo” | “none” | no |
Response 200
{ "data": { "subject": "string", "html": "string", "text": "string" }, "meta": { "requestId": "string", "nextCursor": "string", "hasMore": true, "warnings": [ { "code": "string", "message": "string", "suggestion": "string" } ] }}| Field | Type | Required | Constraints |
|---|---|---|---|
data |
object | yes | |
data.subject |
string | yes | |
data.html |
string | yes | |
data.text |
string | yes | |
meta |
object | no | |
meta.requestId |
string | no | |
meta.nextCursor |
string | null | no | |
meta.hasMore |
boolean | no | |
meta.warnings |
object[] | no | |
meta.warnings[] |
object | no | |
meta.warnings[].code |
string | yes | |
meta.warnings[].message |
string | yes | |
meta.warnings[].suggestion |
string | no |
Errors follow the shared envelope and code table.
GET /v1/broadcasts/{id}
Section titled “GET /v1/broadcasts/{id}”Fetch one broadcast
Auth: Session token.
Response 200
{ "data": { "id": "string", "appId": "string", "list": "string", "subject": "string", "previewText": "string", "from": "string", "replyTo": "string", "doc": { "type": "doc", "attrs": { "key": {} }, "content": [ {} ] }, "docHash": "string", "header": "logo_and_name", "status": "draft", "scheduledAt": "2026-01-15T09:30:00.000Z", "startedAt": "2026-01-15T09:30:00.000Z", "finishedAt": "2026-01-15T09:30:00.000Z", "failureReason": "string", "counts": { "recipients": 42, "sent": 42, "delivered": 42, "skipped": 42, "failed": 42 }, "createdBy": "string", "createdAt": "2026-01-15T09:30:00.000Z", "updatedAt": "2026-01-15T09:30:00.000Z", "url": "string" }, "meta": { "requestId": "string", "nextCursor": "string", "hasMore": true, "warnings": [ { "code": "string", "message": "string", "suggestion": "string" } ] }}| Field | Type | Required | Constraints |
|---|---|---|---|
data |
object | yes | |
data.id |
string | yes | |
data.appId |
string | yes | |
data.list |
string | yes | |
data.subject |
string | yes | |
data.previewText |
string | null | yes | |
data.from |
string | yes | |
data.replyTo |
string | null | yes | |
data.doc |
object | yes | |
data.doc.type |
“doc” | yes | |
data.doc.attrs |
record<string, any> | no | |
data.doc.content |
any[] | yes | |
data.docHash |
string | null | yes | |
data.header |
“logo_and_name” | “name” | “logo” | “none” | yes | |
data.status |
“draft” | “scheduled” | “sending” | “sent” | “cancelled” | “failed” | yes | |
data.scheduledAt |
string (date-time) | null | yes | |
data.startedAt |
string (date-time) | null | yes | |
data.finishedAt |
string (date-time) | null | yes | |
data.failureReason |
string | null | yes | |
data.counts |
object | yes | |
data.counts.recipients |
integer | yes | |
data.counts.sent |
integer | yes | |
data.counts.delivered |
integer | yes | |
data.counts.skipped |
integer | yes | |
data.counts.failed |
integer | yes | |
data.createdBy |
string | yes | |
data.createdAt |
string (date-time) | yes | |
data.updatedAt |
string (date-time) | yes | |
data.url |
string | yes | |
meta |
object | no | |
meta.requestId |
string | no | |
meta.nextCursor |
string | null | no | |
meta.hasMore |
boolean | no | |
meta.warnings |
object[] | no | |
meta.warnings[] |
object | no | |
meta.warnings[].code |
string | yes | |
meta.warnings[].message |
string | yes | |
meta.warnings[].suggestion |
string | no |
Errors follow the shared envelope and code table.
PATCH /v1/broadcasts/{id}
Section titled “PATCH /v1/broadcasts/{id}”Change a broadcast draft
Only a draft changes. With ifDocHash, the change is refused with conflict when the stored doc is not the one that hash names, which is the case once someone edited it in the dashboard.
Auth: Session token.
Request body
{ "data": { "list": "string", "subject": "string", "previewText": "string", "from": "string", "doc": { "type": "doc", "attrs": { "key": {} }, "content": [ {} ] }, "docHash": "string", "header": "logo_and_name", "ifDocHash": "string" }}| Field | Type | Required | Constraints |
|---|---|---|---|
data |
object | yes | |
data.list |
string | no | length 1–∞ |
data.subject |
string | no | length 0–200 |
data.previewText |
string | null | no | |
data.from |
string | no | length 0–200 |
data.replyTo |
string (email) | null | no | |
data.doc |
object | no | |
data.doc.type |
“doc” | yes | |
data.doc.attrs |
record<string, any> | no | |
data.doc.content |
any[] | yes | |
data.docHash |
string | no | |
data.header |
“logo_and_name” | “name” | “logo” | “none” | no | |
data.ifDocHash |
string | no |
Response 200
{ "data": { "id": "string", "appId": "string", "list": "string", "subject": "string", "previewText": "string", "from": "string", "replyTo": "string", "doc": { "type": "doc", "attrs": { "key": {} }, "content": [ {} ] }, "docHash": "string", "header": "logo_and_name", "status": "draft", "scheduledAt": "2026-01-15T09:30:00.000Z", "startedAt": "2026-01-15T09:30:00.000Z", "finishedAt": "2026-01-15T09:30:00.000Z", "failureReason": "string", "counts": { "recipients": 42, "sent": 42, "delivered": 42, "skipped": 42, "failed": 42 }, "createdBy": "string", "createdAt": "2026-01-15T09:30:00.000Z", "updatedAt": "2026-01-15T09:30:00.000Z", "url": "string" }, "meta": { "requestId": "string", "nextCursor": "string", "hasMore": true, "warnings": [ { "code": "string", "message": "string", "suggestion": "string" } ] }}| Field | Type | Required | Constraints |
|---|---|---|---|
data |
object | yes | |
data.id |
string | yes | |
data.appId |
string | yes | |
data.list |
string | yes | |
data.subject |
string | yes | |
data.previewText |
string | null | yes | |
data.from |
string | yes | |
data.replyTo |
string | null | yes | |
data.doc |
object | yes | |
data.doc.type |
“doc” | yes | |
data.doc.attrs |
record<string, any> | no | |
data.doc.content |
any[] | yes | |
data.docHash |
string | null | yes | |
data.header |
“logo_and_name” | “name” | “logo” | “none” | yes | |
data.status |
“draft” | “scheduled” | “sending” | “sent” | “cancelled” | “failed” | yes | |
data.scheduledAt |
string (date-time) | null | yes | |
data.startedAt |
string (date-time) | null | yes | |
data.finishedAt |
string (date-time) | null | yes | |
data.failureReason |
string | null | yes | |
data.counts |
object | yes | |
data.counts.recipients |
integer | yes | |
data.counts.sent |
integer | yes | |
data.counts.delivered |
integer | yes | |
data.counts.skipped |
integer | yes | |
data.counts.failed |
integer | yes | |
data.createdBy |
string | yes | |
data.createdAt |
string (date-time) | yes | |
data.updatedAt |
string (date-time) | yes | |
data.url |
string | yes | |
meta |
object | no | |
meta.requestId |
string | no | |
meta.nextCursor |
string | null | no | |
meta.hasMore |
boolean | no | |
meta.warnings |
object[] | no | |
meta.warnings[] |
object | no | |
meta.warnings[].code |
string | yes | |
meta.warnings[].message |
string | yes | |
meta.warnings[].suggestion |
string | no |
Errors follow the shared envelope and code table.
DELETE /v1/broadcasts/{id}
Section titled “DELETE /v1/broadcasts/{id}”Delete a broadcast draft
Only a draft can be deleted.
Auth: Session token.
Response 200
{ "data": { "deleted": true }, "meta": { "requestId": "string", "nextCursor": "string", "hasMore": true, "warnings": [ { "code": "string", "message": "string", "suggestion": "string" } ] }}| Field | Type | Required | Constraints |
|---|---|---|---|
data |
object | yes | |
data.deleted |
true | yes | |
meta |
object | no | |
meta.requestId |
string | no | |
meta.nextCursor |
string | null | no | |
meta.hasMore |
boolean | no | |
meta.warnings |
object[] | no | |
meta.warnings[] |
object | no | |
meta.warnings[].code |
string | yes | |
meta.warnings[].message |
string | yes | |
meta.warnings[].suggestion |
string | no |
Errors follow the shared envelope and code table.
POST /v1/broadcasts/{id}/duplicate
Section titled “POST /v1/broadcasts/{id}/duplicate”Copy a broadcast into a new draft
The copy keeps the list, sender and content, and its subject starts with “Copy of “ (a draft without one copies without one).
Auth: Session token.
Response 201
{ "data": { "id": "string", "appId": "string", "list": "string", "subject": "string", "previewText": "string", "from": "string", "replyTo": "string", "doc": { "type": "doc", "attrs": { "key": {} }, "content": [ {} ] }, "docHash": "string", "header": "logo_and_name", "status": "draft", "scheduledAt": "2026-01-15T09:30:00.000Z", "startedAt": "2026-01-15T09:30:00.000Z", "finishedAt": "2026-01-15T09:30:00.000Z", "failureReason": "string", "counts": { "recipients": 42, "sent": 42, "delivered": 42, "skipped": 42, "failed": 42 }, "createdBy": "string", "createdAt": "2026-01-15T09:30:00.000Z", "updatedAt": "2026-01-15T09:30:00.000Z", "url": "string" }, "meta": { "requestId": "string", "nextCursor": "string", "hasMore": true, "warnings": [ { "code": "string", "message": "string", "suggestion": "string" } ] }}| Field | Type | Required | Constraints |
|---|---|---|---|
data |
object | yes | |
data.id |
string | yes | |
data.appId |
string | yes | |
data.list |
string | yes | |
data.subject |
string | yes | |
data.previewText |
string | null | yes | |
data.from |
string | yes | |
data.replyTo |
string | null | yes | |
data.doc |
object | yes | |
data.doc.type |
“doc” | yes | |
data.doc.attrs |
record<string, any> | no | |
data.doc.content |
any[] | yes | |
data.docHash |
string | null | yes | |
data.header |
“logo_and_name” | “name” | “logo” | “none” | yes | |
data.status |
“draft” | “scheduled” | “sending” | “sent” | “cancelled” | “failed” | yes | |
data.scheduledAt |
string (date-time) | null | yes | |
data.startedAt |
string (date-time) | null | yes | |
data.finishedAt |
string (date-time) | null | yes | |
data.failureReason |
string | null | yes | |
data.counts |
object | yes | |
data.counts.recipients |
integer | yes | |
data.counts.sent |
integer | yes | |
data.counts.delivered |
integer | yes | |
data.counts.skipped |
integer | yes | |
data.counts.failed |
integer | yes | |
data.createdBy |
string | yes | |
data.createdAt |
string (date-time) | yes | |
data.updatedAt |
string (date-time) | yes | |
data.url |
string | yes | |
meta |
object | no | |
meta.requestId |
string | no | |
meta.nextCursor |
string | null | no | |
meta.hasMore |
boolean | no | |
meta.warnings |
object[] | no | |
meta.warnings[] |
object | no | |
meta.warnings[].code |
string | yes | |
meta.warnings[].message |
string | yes | |
meta.warnings[].suggestion |
string | no |
Errors follow the shared envelope and code table.
POST /v1/broadcasts/{id}/test
Section titled “POST /v1/broadcasts/{id}/test”Send test to me
Sends the broadcast as rendered to your own address, with every variable at its fallback. Costs nothing, counts toward nothing, and shows as a test. Its subject starts with [test] , and reads [test] No subject set yet while the draft has none.
Auth: Session token.
Response 201
{ "data": { "id": "string", "orgId": "string", "profileId": "string", "automation": "string", "executionId": "string", "appId": "string", "broadcastId": "string", "kind": "automation", "step": "string", "channel": "email", "status": "sent", "dryRun": true, "recipient": "string", "providerId": "string", "error": "string", "createdAt": "2026-01-15T09:30:00.000Z", "updatedAt": "2026-01-15T09:30:00.000Z", "sentAt": "2026-01-15T09:30:00.000Z", "settledAt": "2026-01-15T09:30:00.000Z", "openedAt": "2026-01-15T09:30:00.000Z", "openCount": 42, "spamScore": 42, "spamSymbols": [ "string" ] }, "meta": { "requestId": "string", "nextCursor": "string", "hasMore": true, "warnings": [ { "code": "string", "message": "string", "suggestion": "string" } ] }}| Field | Type | Required | Constraints |
|---|---|---|---|
data |
object | yes | |
data.id |
string | yes | |
data.orgId |
string | yes | |
data.profileId |
string | null | yes | |
data.automation |
string | null | yes | |
data.executionId |
string | null | yes | |
data.appId |
string | null | yes | |
data.broadcastId |
string | null | yes | |
data.kind |
“automation” | “test” | “api” | “broadcast” | yes | |
data.step |
string | yes | |
data.channel |
“email” | “webhook” | yes | |
data.status |
“sent” | “delivered” | “bounced” | “complained” | “failed” | “skipped_disabled” | “skipped_paused” | “skipped_suspended” | “skipped_suppressed” | “skipped_quota” | “skipped_unknown_purpose” | “skipped_frequency_cap” | “skipped_consent” | “skipped_domain” | “skipped_ssrf” | “skipped_spam” | “would_send” | “would_skip_disabled” | “would_skip_paused” | “would_skip_suspended” | “would_skip_suppressed” | “would_skip_quota” | “would_skip_unknown_purpose” | “would_skip_frequency_cap” | “would_skip_consent” | “would_skip_domain” | “would_skip_ssrf” | “would_skip_spam” | yes | |
data.dryRun |
boolean | yes | |
data.recipient |
string | null | yes | |
data.providerId |
string | null | yes | |
data.error |
string | null | yes | |
data.createdAt |
string (date-time) | yes | |
data.updatedAt |
string (date-time) | yes | |
data.sentAt |
string (date-time) | null | yes | |
data.settledAt |
string (date-time) | null | yes | |
data.openedAt |
string (date-time) | null | yes | |
data.openCount |
integer | yes | -2147483648 ≤ x ≤ 2147483647 |
data.spamScore |
number | null | yes | |
data.spamSymbols |
string[] | null | yes | |
meta |
object | no | |
meta.requestId |
string | no | |
meta.nextCursor |
string | null | no | |
meta.hasMore |
boolean | no | |
meta.warnings |
object[] | no | |
meta.warnings[] |
object | no | |
meta.warnings[].code |
string | yes | |
meta.warnings[].message |
string | yes | |
meta.warnings[].suggestion |
string | no |
Errors follow the shared envelope and code table.
POST /v1/broadcasts/{id}/preview
Section titled “POST /v1/broadcasts/{id}/preview”Render a broadcast
The broadcast in its app’s look, with every variable at its fallback and the unsubscribe footer.
Auth: Session token.
Response 200
{ "data": { "subject": "string", "html": "string", "text": "string" }, "meta": { "requestId": "string", "nextCursor": "string", "hasMore": true, "warnings": [ { "code": "string", "message": "string", "suggestion": "string" } ] }}| Field | Type | Required | Constraints |
|---|---|---|---|
data |
object | yes | |
data.subject |
string | yes | |
data.html |
string | yes | |
data.text |
string | yes | |
meta |
object | no | |
meta.requestId |
string | no | |
meta.nextCursor |
string | null | no | |
meta.hasMore |
boolean | no | |
meta.warnings |
object[] | no | |
meta.warnings[] |
object | no | |
meta.warnings[].code |
string | yes | |
meta.warnings[].message |
string | yes | |
meta.warnings[].suggestion |
string | no |
Errors follow the shared envelope and code table.
POST /v1/broadcasts/{id}/send
Section titled “POST /v1/broadcasts/{id}/send”Send or schedule a broadcast
Admin only, from the dashboard. Sends the draft to everyone on its list now, or at at when given (a time in the future). Who is on the list is decided when sending starts, and it does not start unless the organization’s credit covers all of them. Refused while the draft has no subject.
Auth: Session token.
Request body
{ "data": { "at": "2026-01-15T09:30:00.000Z" }}| Field | Type | Required | Constraints |
|---|---|---|---|
data |
object | yes | |
data.at |
string (date-time) | no |
Response 200
{ "data": { "id": "string", "appId": "string", "list": "string", "subject": "string", "previewText": "string", "from": "string", "replyTo": "string", "doc": { "type": "doc", "attrs": { "key": {} }, "content": [ {} ] }, "docHash": "string", "header": "logo_and_name", "status": "draft", "scheduledAt": "2026-01-15T09:30:00.000Z", "startedAt": "2026-01-15T09:30:00.000Z", "finishedAt": "2026-01-15T09:30:00.000Z", "failureReason": "string", "counts": { "recipients": 42, "sent": 42, "delivered": 42, "skipped": 42, "failed": 42 }, "createdBy": "string", "createdAt": "2026-01-15T09:30:00.000Z", "updatedAt": "2026-01-15T09:30:00.000Z", "url": "string" }, "meta": { "requestId": "string", "nextCursor": "string", "hasMore": true, "warnings": [ { "code": "string", "message": "string", "suggestion": "string" } ] }}| Field | Type | Required | Constraints |
|---|---|---|---|
data |
object | yes | |
data.id |
string | yes | |
data.appId |
string | yes | |
data.list |
string | yes | |
data.subject |
string | yes | |
data.previewText |
string | null | yes | |
data.from |
string | yes | |
data.replyTo |
string | null | yes | |
data.doc |
object | yes | |
data.doc.type |
“doc” | yes | |
data.doc.attrs |
record<string, any> | no | |
data.doc.content |
any[] | yes | |
data.docHash |
string | null | yes | |
data.header |
“logo_and_name” | “name” | “logo” | “none” | yes | |
data.status |
“draft” | “scheduled” | “sending” | “sent” | “cancelled” | “failed” | yes | |
data.scheduledAt |
string (date-time) | null | yes | |
data.startedAt |
string (date-time) | null | yes | |
data.finishedAt |
string (date-time) | null | yes | |
data.failureReason |
string | null | yes | |
data.counts |
object | yes | |
data.counts.recipients |
integer | yes | |
data.counts.sent |
integer | yes | |
data.counts.delivered |
integer | yes | |
data.counts.skipped |
integer | yes | |
data.counts.failed |
integer | yes | |
data.createdBy |
string | yes | |
data.createdAt |
string (date-time) | yes | |
data.updatedAt |
string (date-time) | yes | |
data.url |
string | yes | |
meta |
object | no | |
meta.requestId |
string | no | |
meta.nextCursor |
string | null | no | |
meta.hasMore |
boolean | no | |
meta.warnings |
object[] | no | |
meta.warnings[] |
object | no | |
meta.warnings[].code |
string | yes | |
meta.warnings[].message |
string | yes | |
meta.warnings[].suggestion |
string | no |
Errors follow the shared envelope and code table.
POST /v1/broadcasts/{id}/cancel
Section titled “POST /v1/broadcasts/{id}/cancel”Cancel a scheduled broadcast
Admin only, from the dashboard. Works until sending starts; after that the broadcast runs to the end of its list.
Auth: Session token.
Response 200
{ "data": { "id": "string", "appId": "string", "list": "string", "subject": "string", "previewText": "string", "from": "string", "replyTo": "string", "doc": { "type": "doc", "attrs": { "key": {} }, "content": [ {} ] }, "docHash": "string", "header": "logo_and_name", "status": "draft", "scheduledAt": "2026-01-15T09:30:00.000Z", "startedAt": "2026-01-15T09:30:00.000Z", "finishedAt": "2026-01-15T09:30:00.000Z", "failureReason": "string", "counts": { "recipients": 42, "sent": 42, "delivered": 42, "skipped": 42, "failed": 42 }, "createdBy": "string", "createdAt": "2026-01-15T09:30:00.000Z", "updatedAt": "2026-01-15T09:30:00.000Z", "url": "string" }, "meta": { "requestId": "string", "nextCursor": "string", "hasMore": true, "warnings": [ { "code": "string", "message": "string", "suggestion": "string" } ] }}| Field | Type | Required | Constraints |
|---|---|---|---|
data |
object | yes | |
data.id |
string | yes | |
data.appId |
string | yes | |
data.list |
string | yes | |
data.subject |
string | yes | |
data.previewText |
string | null | yes | |
data.from |
string | yes | |
data.replyTo |
string | null | yes | |
data.doc |
object | yes | |
data.doc.type |
“doc” | yes | |
data.doc.attrs |
record<string, any> | no | |
data.doc.content |
any[] | yes | |
data.docHash |
string | null | yes | |
data.header |
“logo_and_name” | “name” | “logo” | “none” | yes | |
data.status |
“draft” | “scheduled” | “sending” | “sent” | “cancelled” | “failed” | yes | |
data.scheduledAt |
string (date-time) | null | yes | |
data.startedAt |
string (date-time) | null | yes | |
data.finishedAt |
string (date-time) | null | yes | |
data.failureReason |
string | null | yes | |
data.counts |
object | yes | |
data.counts.recipients |
integer | yes | |
data.counts.sent |
integer | yes | |
data.counts.delivered |
integer | yes | |
data.counts.skipped |
integer | yes | |
data.counts.failed |
integer | yes | |
data.createdBy |
string | yes | |
data.createdAt |
string (date-time) | yes | |
data.updatedAt |
string (date-time) | yes | |
data.url |
string | yes | |
meta |
object | no | |
meta.requestId |
string | no | |
meta.nextCursor |
string | null | no | |
meta.hasMore |
boolean | no | |
meta.warnings |
object[] | no | |
meta.warnings[] |
object | no | |
meta.warnings[].code |
string | yes | |
meta.warnings[].message |
string | yes | |
meta.warnings[].suggestion |
string | no |
Errors follow the shared envelope and code table.
GET /v1/broadcasts/{id}/audience
Section titled “GET /v1/broadcasts/{id}/audience”Who a broadcast would reach now
The list’s subscribers who would be mailed if it started now, and the rest counted by why not: suppressed (the address bounced, complained or was removed by hand) and no_email.
Auth: Session token.
Response 200
{ "data": { "willReceive": 42, "skipped": { "suppressed": 42 } }, "meta": { "requestId": "string", "nextCursor": "string", "hasMore": true, "warnings": [ { "code": "string", "message": "string", "suggestion": "string" } ] }}| Field | Type | Required | Constraints |
|---|---|---|---|
data |
object | yes | |
data.willReceive |
integer | yes | |
data.skipped |
record<suppressed | no_email, integer> | yes | |
meta |
object | no | |
meta.requestId |
string | no | |
meta.nextCursor |
string | null | no | |
meta.hasMore |
boolean | no | |
meta.warnings |
object[] | no | |
meta.warnings[] |
object | no | |
meta.warnings[].code |
string | yes | |
meta.warnings[].message |
string | yes | |
meta.warnings[].suggestion |
string | no |
Errors follow the shared envelope and code table.
GET /v1/broadcasts/{id}/stats
Section titled “GET /v1/broadcasts/{id}/stats”What a broadcast did
Sent, delivered, opens (approximate: a tracking pixel that mail privacy features pre-load), clicks, unsubscribes from this broadcast’s link, bounces and complaints, skips by reason, opens and clicks per hour over the first 48 hours, and the clicked links.
Auth: Session token.
Response 200
{ "data": { "sent": 42, "delivered": 42, "opens": 42, "clicks": 42, "unsubscribed": 42, "bounced": 42, "complained": 42, "skipped": { "paused": 42 }, "hourly": [ { "hour": "2026-01-15T09:30:00.000Z", "opens": 42, "clicks": 42 } ], "links": [ { "url": "string", "clicks": 42 } ] }, "meta": { "requestId": "string", "nextCursor": "string", "hasMore": true, "warnings": [ { "code": "string", "message": "string", "suggestion": "string" } ] }}| Field | Type | Required | Constraints |
|---|---|---|---|
data |
object | yes | |
data.sent |
integer | yes | |
data.delivered |
integer | yes | |
data.opens |
integer | yes | |
data.clicks |
integer | yes | |
data.unsubscribed |
integer | yes | |
data.bounced |
integer | yes | |
data.complained |
integer | yes | |
data.skipped |
record<paused | suppressed | quota | frequency_cap | consent | domain | spam | no_email | gone, integer> | yes | |
data.hourly |
object[] | yes | |
data.hourly[] |
object | yes | |
data.hourly[].hour |
string (date-time) | yes | |
data.hourly[].opens |
integer | yes | |
data.hourly[].clicks |
integer | yes | |
data.links |
object[] | yes | |
data.links[] |
object | yes | |
data.links[].url |
string | yes | |
data.links[].clicks |
integer | yes | |
meta |
object | no | |
meta.requestId |
string | no | |
meta.nextCursor |
string | null | no | |
meta.hasMore |
boolean | no | |
meta.warnings |
object[] | no | |
meta.warnings[] |
object | no | |
meta.warnings[].code |
string | yes | |
meta.warnings[].message |
string | yes | |
meta.warnings[].suggestion |
string | no |
Errors follow the shared envelope and code table.