Broadcasts
One-off emails to an audience, sent now or at a time you choose.
List broadcasts
GET/projects/{project_id}/broadcasts
Newest first, without each email’s body; get one broadcast for its html.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| project_id | path | string | Required |
| status | query | "draft" | "scheduled" | "sending" | "sent" | "canceled" | null | Optional |
| cursor | query | string | null | Optional |
| limit | query | integer | Optional, default 50 |
curl "https://sendocado.com/api/v1/projects/$PROJECT_ID/broadcasts?status=draft" \
-H "Authorization: Bearer $SENDOCADO_API_KEY"{
"broadcasts": [
{
"id": "06i8j76b2lajlj4h9du7794g9dpmrcg6",
"project_id": "ujzde8gxd6ncf10epf91dhodzdoc9is0",
"name": "October product update",
"status": "draft",
"filter_groups": [
{
"filters": [
{
"type": "tag",
"op": "has",
"tag": "customer"
}
]
}
],
"subject": "What’s new in Acme this month",
"preview_text": "Faster exports and a new dashboard.",
"source_template_id": null,
"scheduled_for": null,
"has_open_sends": false,
"created_at": 1790172800000
}
],
"next_cursor": null
}Errors
| Status | Type | Example message |
|---|---|---|
| 400 | invalid_data | limit: Too big: expected number to be <=200 |
| 401 | unauthorized | Send a valid API key as "Authorization: Bearer sc_…", or sign in. |
| 404 | project_not_found | Project not found. |
| 429 | rate_limited | Too many requests. Try again in 12 seconds. |
Create a broadcast
POST/projects/{project_id}/broadcasts
A draft. Its content is html, markdown, or a copy of a template. Send or schedule it with an update.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| project_id | path | string | Required |
Body
Send JSON with every field; use null for a value you do not set.
| Field | Type | Notes |
|---|---|---|
| name | string | Required |
| filter_groups | object[] | Required |
| filter_groups[].filters | object[] | Required |
| filter_groups[].filters[].type | "tag" | Required, when type is "tag" |
| filter_groups[].filters[].op | "has" | "has_not" | Required, when type is "tag" |
| filter_groups[].filters[].tag | string | Required, when type is "tag" |
| filter_groups[].filters[].type | "attribute" | Required, when type is "attribute" |
| filter_groups[].filters[].op | "equals" | "not_equals" | Required, when type is "attribute" |
| filter_groups[].filters[].key | string | Required, when type is "attribute" |
| filter_groups[].filters[].value | string | Required, when type is "attribute" |
| filter_groups[].filters[].type | "added_days_ago" | Required, when type is "added_days_ago" |
| filter_groups[].filters[].op | "less_than" | "more_than" | Required, when type is "added_days_ago" |
| filter_groups[].filters[].days | integer | Required, when type is "added_days_ago" |
| filter_groups[].filters[].type | "opened" | "clicked" | Required, when type is "opened" | "clicked" |
| filter_groups[].filters[].op | "within" | "not_within" | Required, when type is "opened" | "clicked" |
| filter_groups[].filters[].days | integer | Required, when type is "opened" | "clicked" |
| filter_groups[].filters[].type | "campaign" | Required, when type is "campaign" |
| filter_groups[].filters[].op | "is_enrolled" | "is_not_enrolled" | "was_enrolled" | "was_never_enrolled" | Required, when type is "campaign" |
| filter_groups[].filters[].campaign_id | string | Required, when type is "campaign" |
| subject | string | null | Required |
| preview_text | string | null | Required |
| html | string | null | Required |
| markdown | string | null | Required |
| template_id | string | null | Required |
curl -X POST "https://sendocado.com/api/v1/projects/$PROJECT_ID/broadcasts" \
-H "Authorization: Bearer $SENDOCADO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "October product update",
"filter_groups": [
{
"filters": [
{
"type": "tag",
"op": "has",
"tag": "customer"
}
]
}
],
"subject": "What’s new in Acme this month",
"preview_text": "Faster exports and a new dashboard.",
"html": null,
"markdown": "# What’s new\n\nHi {{ first_name | there }}, exports are now twice as fast.",
"template_id": null
}'{
"id": "06i8j76b2lajlj4h9du7794g9dpmrcg6",
"project_id": "ujzde8gxd6ncf10epf91dhodzdoc9is0",
"name": "October product update",
"status": "draft",
"filter_groups": [
{
"filters": [
{
"type": "tag",
"op": "has",
"tag": "customer"
}
]
}
],
"subject": "What’s new in Acme this month",
"preview_text": "Faster exports and a new dashboard.",
"html": "<h1>What’s new</h1>\n<p>Hi {{ first_name | there }}, exports are now twice as fast.</p>\n",
"text": "What’s new\n\nHi {{ first_name | there }}, exports are now twice as fast.",
"merge_tags": ["first_name"],
"source_template_id": null,
"scheduled_for": null,
"has_open_sends": false,
"created_at": 1790172800000
}Errors
| Status | Type | Example message |
|---|---|---|
| 400 | invalid_data | Send a JSON request body. |
| 401 | unauthorized | Send a valid API key as "Authorization: Bearer sc_…", or sign in. |
| 404 | project_not_found | Project not found. |
| 422 | invalid_reference | No template with that id in this project. |
| 429 | rate_limited | Too many requests. Try again in 12 seconds. |
Get a broadcast
GET/broadcasts/{broadcast_id}
has_open_sends is true while emails of the broadcast still wait for the provider’s answer.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| broadcast_id | path | string | Required |
curl "https://sendocado.com/api/v1/broadcasts/$BROADCAST_ID" \
-H "Authorization: Bearer $SENDOCADO_API_KEY"{
"id": "06i8j76b2lajlj4h9du7794g9dpmrcg6",
"project_id": "ujzde8gxd6ncf10epf91dhodzdoc9is0",
"name": "October product update",
"status": "draft",
"filter_groups": [
{
"filters": [
{
"type": "tag",
"op": "has",
"tag": "customer"
}
]
}
],
"subject": "What’s new in Acme this month",
"preview_text": "Faster exports and a new dashboard.",
"html": "<h1>What’s new</h1>\n<p>Hi {{ first_name | there }}, exports are now twice as fast.</p>\n",
"text": "What’s new\n\nHi {{ first_name | there }}, exports are now twice as fast.",
"merge_tags": ["first_name"],
"source_template_id": null,
"scheduled_for": null,
"has_open_sends": false,
"created_at": 1790172800000
}Errors
| Status | Type | Example message |
|---|---|---|
| 401 | unauthorized | Send a valid API key as "Authorization: Bearer sc_…", or sign in. |
| 404 | broadcast_not_found | Broadcast not found. |
| 429 | rate_limited | Too many requests. Try again in 12 seconds. |
Update, schedule or send a broadcast
PUT/broadcasts/{broadcast_id}
Send every field, status included. draft → scheduled needs scheduled_for in the future; draft or scheduled → sending starts it within seconds; scheduled → draft unschedules; sending → canceled stops it (409 for any other move). Scheduling or sending first passes the readiness checks (422). Once it is sending, only the status can change.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| broadcast_id | path | string | Required |
Body
Send JSON with every field; use null for a value you do not set.
| Field | Type | Notes |
|---|---|---|
| name | string | Required |
| filter_groups | object[] | Required |
| filter_groups[].filters | object[] | Required |
| filter_groups[].filters[].type | "tag" | Required, when type is "tag" |
| filter_groups[].filters[].op | "has" | "has_not" | Required, when type is "tag" |
| filter_groups[].filters[].tag | string | Required, when type is "tag" |
| filter_groups[].filters[].type | "attribute" | Required, when type is "attribute" |
| filter_groups[].filters[].op | "equals" | "not_equals" | Required, when type is "attribute" |
| filter_groups[].filters[].key | string | Required, when type is "attribute" |
| filter_groups[].filters[].value | string | Required, when type is "attribute" |
| filter_groups[].filters[].type | "added_days_ago" | Required, when type is "added_days_ago" |
| filter_groups[].filters[].op | "less_than" | "more_than" | Required, when type is "added_days_ago" |
| filter_groups[].filters[].days | integer | Required, when type is "added_days_ago" |
| filter_groups[].filters[].type | "opened" | "clicked" | Required, when type is "opened" | "clicked" |
| filter_groups[].filters[].op | "within" | "not_within" | Required, when type is "opened" | "clicked" |
| filter_groups[].filters[].days | integer | Required, when type is "opened" | "clicked" |
| filter_groups[].filters[].type | "campaign" | Required, when type is "campaign" |
| filter_groups[].filters[].op | "is_enrolled" | "is_not_enrolled" | "was_enrolled" | "was_never_enrolled" | Required, when type is "campaign" |
| filter_groups[].filters[].campaign_id | string | Required, when type is "campaign" |
| subject | string | null | Required |
| preview_text | string | null | Required |
| html | string | null | Required |
| markdown | string | null | Required |
| template_id | string | null | Required |
| status | "draft" | "scheduled" | "sending" | "sent" | "canceled" | Required |
| scheduled_for | integer | null | Required |
curl -X PUT "https://sendocado.com/api/v1/broadcasts/$BROADCAST_ID" \
-H "Authorization: Bearer $SENDOCADO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "October product update",
"filter_groups": [
{
"filters": [
{
"type": "tag",
"op": "has",
"tag": "customer"
}
]
}
],
"subject": "What’s new in Acme this month",
"preview_text": "Faster exports and a new dashboard.",
"html": "<h1>What’s new</h1>\n<p>Hi {{ first_name | there }}, exports are now twice as fast.</p>\n",
"markdown": null,
"template_id": null,
"status": "scheduled",
"scheduled_for": 1790259200000
}'{
"id": "06i8j76b2lajlj4h9du7794g9dpmrcg6",
"project_id": "ujzde8gxd6ncf10epf91dhodzdoc9is0",
"name": "October product update",
"status": "scheduled",
"filter_groups": [
{
"filters": [
{
"type": "tag",
"op": "has",
"tag": "customer"
}
]
}
],
"subject": "What’s new in Acme this month",
"preview_text": "Faster exports and a new dashboard.",
"html": "<h1>What’s new</h1>\n<p>Hi {{ first_name | there }}, exports are now twice as fast.</p>\n",
"text": "What’s new\n\nHi {{ first_name | there }}, exports are now twice as fast.",
"merge_tags": ["first_name"],
"source_template_id": null,
"scheduled_for": 1790259200000,
"has_open_sends": false,
"created_at": 1790172800000
}Errors
| Status | Type | Example message |
|---|---|---|
| 400 | invalid_data | Send a JSON request body. |
| 401 | unauthorized | Send a valid API key as "Authorization: Bearer sc_…", or sign in. |
| 402 | plan_limit_reached | Your plan allows 5,000 contacts. Upgrade the plan to add more. |
| 404 | broadcast_not_found | Broadcast not found. |
| 409 | invalid_status_transition | A draft campaign cannot become paused. Set it live first. |
| 409 | conflict | This campaign has enrollments, so it cannot be deleted. Pause it instead. |
| 422 | campaign_incomplete | Add at least one email with a subject and content before going live. |
| 422 | sender_not_ready | Verify the domain of your from address before you send email. |
| 422 | postal_address_missing | Add a postal address to the project before you send marketing email. |
| 422 | invalid_reference | No template with that id in this project. |
| 429 | rate_limited | Too many requests. Try again in 12 seconds. |
Delete a broadcast
DELETE/broadcasts/{broadcast_id}
Only a draft can be deleted (409 otherwise); a sent broadcast stays as the record of what went out.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| broadcast_id | path | string | Required |
curl -X DELETE "https://sendocado.com/api/v1/broadcasts/$BROADCAST_ID" \
-H "Authorization: Bearer $SENDOCADO_API_KEY"Answers 204 with no body.
Errors
| Status | Type | Example message |
|---|---|---|
| 401 | unauthorized | Send a valid API key as "Authorization: Bearer sc_…", or sign in. |
| 404 | broadcast_not_found | Broadcast not found. |
| 409 | conflict | This campaign has enrollments, so it cannot be deleted. Pause it instead. |
| 429 | rate_limited | Too many requests. Try again in 12 seconds. |
Check whether a broadcast can be sent
GET/broadcasts/{broadcast_id}/readiness
What would refuse scheduling or sending this broadcast right now. An empty list means it can go.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| broadcast_id | path | string | Required |
curl "https://sendocado.com/api/v1/broadcasts/$BROADCAST_ID/readiness" \
-H "Authorization: Bearer $SENDOCADO_API_KEY"{
"problems": []
}Errors
| Status | Type | Example message |
|---|---|---|
| 401 | unauthorized | Send a valid API key as "Authorization: Bearer sc_…", or sign in. |
| 404 | broadcast_not_found | Broadcast not found. |
| 429 | rate_limited | Too many requests. Try again in 12 seconds. |
Get broadcast stats
GET/broadcasts/{broadcast_id}/stats
Every day the broadcast sent on. Rates are fractions of sent_count, null before anything was sent; numbers trail events by about a minute.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| broadcast_id | path | string | Required |
curl "https://sendocado.com/api/v1/broadcasts/$BROADCAST_ID/stats" \
-H "Authorization: Bearer $SENDOCADO_API_KEY"{
"totals": {
"sent_count": 1240,
"delivered_count": 1221,
"opened_count": 642,
"clicked_count": 187,
"replied_count": 41,
"bounced_count": 6,
"complained_count": 1,
"unsubscribed_count": 9,
"failed_count": 2
},
"rates": {
"delivery_rate": 0.9847,
"open_rate": 0.5177,
"click_rate": 0.1508,
"reply_rate": 0.0331,
"bounce_rate": 0.0048,
"complaint_rate": 0.0008,
"unsubscribe_rate": 0.0073
},
"daily": [
{
"sent_count": 1240,
"delivered_count": 1221,
"opened_count": 642,
"clicked_count": 187,
"replied_count": 41,
"bounced_count": 6,
"complained_count": 1,
"unsubscribed_count": 9,
"failed_count": 2,
"day": "2026-09-21"
}
],
"links": [
{
"url": "https://acme.example/changelog",
"click_count": 203,
"unique_click_count": 187
}
]
}Errors
| Status | Type | Example message |
|---|---|---|
| 401 | unauthorized | Send a valid API key as "Authorization: Bearer sc_…", or sign in. |
| 404 | broadcast_not_found | Broadcast not found. |
| 429 | rate_limited | Too many requests. Try again in 12 seconds. |