Campaigns
Automated series of emails: the audience, the steps and their branches, status changes, enrollments, stats and AI drafts.
List campaigns
GET/projects/{project_id}/campaigns
Newest first, without the steps; get one campaign for its emails.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| project_id | path | string | Required |
| status | query | "draft" | "live" | "paused" | null | Optional |
| cursor | query | string | null | Optional |
| limit | query | integer | Optional, default 50 |
curl "https://sendocado.com/api/v1/projects/$PROJECT_ID/campaigns?status=live" \
-H "Authorization: Bearer $SENDOCADO_API_KEY"{
"campaigns": [
{
"id": "h5dnsipzz5fk2z9ri19r0wyojfljooa5",
"project_id": "ujzde8gxd6ncf10epf91dhodzdoc9is0",
"name": "Trial welcome",
"status": "live",
"filter_groups": [
{
"filters": [
{
"type": "tag",
"op": "has",
"tag": "trial"
},
{
"type": "added_days_ago",
"op": "less_than",
"days": 14
}
]
}
],
"timezone": "America/New_York",
"send_window": {
"days": [1, 2, 3, 4, 5],
"start_hour": 9,
"end_hour": 17
},
"is_stopped_on_reply": true,
"num_steps": 3,
"enrollment_count": 412,
"num_active": 158,
"created_at": 1790000000000
}
],
"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 campaign
POST/projects/{project_id}/campaigns
A draft whose steps follow each other in the order sent. Each step is html, markdown, or a copy of a template. Add branches and set it live with the step and campaign routes.
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 |
| timezone | string | Required |
| send_window | object | null | Required |
| send_window.days | integer[] | Required |
| send_window.start_hour | integer | Required |
| send_window.end_hour | integer | Required |
| is_stopped_on_reply | boolean | 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" |
| steps | object[] | Required |
| steps[].delay_days | integer | Required |
| steps[].subject | string | null | Required |
| steps[].preview_text | string | null | Required |
| steps[].html | string | null | Required |
| steps[].markdown | string | null | Required |
| steps[].template_id | string | null | Required |
curl -X POST "https://sendocado.com/api/v1/projects/$PROJECT_ID/campaigns" \
-H "Authorization: Bearer $SENDOCADO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Trial welcome",
"timezone": "America/New_York",
"send_window": {
"days": [1, 2, 3, 4, 5],
"start_hour": 9,
"end_hour": 17
},
"is_stopped_on_reply": true,
"filter_groups": [
{
"filters": [
{
"type": "tag",
"op": "has",
"tag": "trial"
},
{
"type": "added_days_ago",
"op": "less_than",
"days": 14
}
]
}
],
"steps": [
{
"delay_days": 0,
"subject": "Welcome to Acme, {{ first_name | there }}",
"preview_text": "The one thing to do first.",
"html": null,
"markdown": "Hi {{ first_name | there }},\n\nThanks for trying Acme. Start with the [two-minute setup](https://acme.example/setup).",
"template_id": null
},
{
"delay_days": 3,
"subject": "Still there, {{ first_name | there }}?",
"preview_text": "Most people get stuck on the same step.",
"html": null,
"markdown": "Most people get stuck on the same step. Here is the shortcut.",
"template_id": null
}
]
}'{
"id": "h5dnsipzz5fk2z9ri19r0wyojfljooa5",
"project_id": "ujzde8gxd6ncf10epf91dhodzdoc9is0",
"name": "Trial welcome",
"status": "draft",
"filter_groups": [
{
"filters": [
{
"type": "tag",
"op": "has",
"tag": "trial"
},
{
"type": "added_days_ago",
"op": "less_than",
"days": 14
}
]
}
],
"timezone": "America/New_York",
"send_window": {
"days": [1, 2, 3, 4, 5],
"start_hour": 9,
"end_hour": 17
},
"is_stopped_on_reply": true,
"num_steps": 2,
"enrollment_count": 0,
"num_active": 0,
"first_step_id": "j8gxbenyjqwx4hh5344tfjgvq4k7bn7x",
"steps": [
{
"id": "j8gxbenyjqwx4hh5344tfjgvq4k7bn7x",
"campaign_id": "h5dnsipzz5fk2z9ri19r0wyojfljooa5",
"delay_days": 0,
"subject": "Welcome to Acme, {{ first_name | there }}",
"preview_text": "The one thing to do first.",
"html": "<p>Hi {{ first_name | there }},</p>\n<p>Thanks for trying Acme. Start with the <a href=\"https://acme.example/setup\">two-minute setup</a>.</p>\n",
"text": "Hi {{ first_name | there }},\n\nThanks for trying Acme. Start with the two-minute setup (https://acme.example/setup).",
"merge_tags": ["first_name"],
"next": {
"type": "step",
"step_id": "2wxfogo4mvn4a4wfhym4l1vfz3zfkkib"
},
"source_template_id": null,
"created_at": 1790000000000
},
{
"id": "2wxfogo4mvn4a4wfhym4l1vfz3zfkkib",
"campaign_id": "h5dnsipzz5fk2z9ri19r0wyojfljooa5",
"delay_days": 3,
"subject": "Still there, {{ first_name | there }}?",
"preview_text": "Most people get stuck on the same step.",
"html": "<p>Most people get stuck on the same step. Here is the shortcut.</p>\n",
"text": "Most people get stuck on the same step. Here is the shortcut.",
"merge_tags": ["first_name"],
"next": {
"type": "end"
},
"source_template_id": null,
"created_at": 1790000000000
}
],
"created_at": 1790000000000
}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 campaign
GET/campaigns/{campaign_id}
Steps come in walk order: from the first step along next, and at a branch the true path before the false path.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| campaign_id | path | string | Required |
curl "https://sendocado.com/api/v1/campaigns/$CAMPAIGN_ID" \
-H "Authorization: Bearer $SENDOCADO_API_KEY"{
"id": "h5dnsipzz5fk2z9ri19r0wyojfljooa5",
"project_id": "ujzde8gxd6ncf10epf91dhodzdoc9is0",
"name": "Trial welcome",
"status": "live",
"filter_groups": [
{
"filters": [
{
"type": "tag",
"op": "has",
"tag": "trial"
},
{
"type": "added_days_ago",
"op": "less_than",
"days": 14
}
]
}
],
"timezone": "America/New_York",
"send_window": {
"days": [1, 2, 3, 4, 5],
"start_hour": 9,
"end_hour": 17
},
"is_stopped_on_reply": true,
"num_steps": 3,
"enrollment_count": 412,
"num_active": 158,
"first_step_id": "j8gxbenyjqwx4hh5344tfjgvq4k7bn7x",
"steps": [
{
"id": "j8gxbenyjqwx4hh5344tfjgvq4k7bn7x",
"campaign_id": "h5dnsipzz5fk2z9ri19r0wyojfljooa5",
"delay_days": 0,
"subject": "Welcome to Acme, {{ first_name | there }}",
"preview_text": "The one thing to do first.",
"html": "<p>Hi {{ first_name | there }},</p>\n<p>Thanks for trying Acme. Start with the <a href=\"https://acme.example/setup\">two-minute setup</a>.</p>\n",
"text": "Hi {{ first_name | there }},\n\nThanks for trying Acme. Start with the two-minute setup (https://acme.example/setup).",
"merge_tags": ["first_name"],
"next": {
"type": "branch",
"delay_days": 3,
"condition": {
"type": "opened_step",
"step_id": "j8gxbenyjqwx4hh5344tfjgvq4k7bn7x"
},
"if_true_step_id": "j8b7tfq7xkwo886vompzom75wbbr4qmw",
"if_false_step_id": "2wxfogo4mvn4a4wfhym4l1vfz3zfkkib"
},
"source_template_id": null,
"created_at": 1790000000000
},
{
"id": "j8b7tfq7xkwo886vompzom75wbbr4qmw",
"campaign_id": "h5dnsipzz5fk2z9ri19r0wyojfljooa5",
"delay_days": 0,
"subject": "Three things Acme does that you might miss",
"preview_text": null,
"html": "<p>Hi {{ first_name | there }}, here are three shortcuts.</p>\n",
"text": "Hi {{ first_name | there }}, here are three shortcuts.",
"merge_tags": ["first_name"],
"next": {
"type": "end"
},
"source_template_id": null,
"created_at": 1790000000000
},
{
"id": "2wxfogo4mvn4a4wfhym4l1vfz3zfkkib",
"campaign_id": "h5dnsipzz5fk2z9ri19r0wyojfljooa5",
"delay_days": 0,
"subject": "Still there, {{ first_name | there }}?",
"preview_text": "Most people get stuck on the same step.",
"html": "<p>Most people get stuck on the same step. Here is the shortcut.</p>\n",
"text": "Most people get stuck on the same step. Here is the shortcut.",
"merge_tags": ["first_name"],
"next": {
"type": "end"
},
"source_template_id": null,
"created_at": 1790000000000
}
],
"created_at": 1790000000000
}Errors
| Status | Type | Example message |
|---|---|---|
| 401 | unauthorized | Send a valid API key as "Authorization: Bearer sc_…", or sign in. |
| 404 | campaign_not_found | Campaign not found. |
| 429 | rate_limited | Too many requests. Try again in 12 seconds. |
Update a campaign or change its status
PUT/campaigns/{campaign_id}
Send every field, status included. draft → live, live → paused and paused → live are allowed (409 otherwise). Going live first passes the readiness checks (422 with the first problem). Steps change through the step routes.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| campaign_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 |
| status | "draft" | "live" | "paused" | Required |
| timezone | string | Required |
| send_window | object | null | Required |
| send_window.days | integer[] | Required |
| send_window.start_hour | integer | Required |
| send_window.end_hour | integer | Required |
| is_stopped_on_reply | boolean | 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" |
curl -X PUT "https://sendocado.com/api/v1/campaigns/$CAMPAIGN_ID" \
-H "Authorization: Bearer $SENDOCADO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Trial welcome",
"status": "live",
"timezone": "America/New_York",
"send_window": {
"days": [1, 2, 3, 4, 5],
"start_hour": 9,
"end_hour": 17
},
"is_stopped_on_reply": true,
"filter_groups": [
{
"filters": [
{
"type": "tag",
"op": "has",
"tag": "trial"
},
{
"type": "added_days_ago",
"op": "less_than",
"days": 14
}
]
}
]
}'{
"id": "h5dnsipzz5fk2z9ri19r0wyojfljooa5",
"project_id": "ujzde8gxd6ncf10epf91dhodzdoc9is0",
"name": "Trial welcome",
"status": "live",
"filter_groups": [
{
"filters": [
{
"type": "tag",
"op": "has",
"tag": "trial"
},
{
"type": "added_days_ago",
"op": "less_than",
"days": 14
}
]
}
],
"timezone": "America/New_York",
"send_window": {
"days": [1, 2, 3, 4, 5],
"start_hour": 9,
"end_hour": 17
},
"is_stopped_on_reply": true,
"num_steps": 3,
"enrollment_count": 412,
"num_active": 158,
"first_step_id": "j8gxbenyjqwx4hh5344tfjgvq4k7bn7x",
"steps": [
{
"id": "j8gxbenyjqwx4hh5344tfjgvq4k7bn7x",
"campaign_id": "h5dnsipzz5fk2z9ri19r0wyojfljooa5",
"delay_days": 0,
"subject": "Welcome to Acme, {{ first_name | there }}",
"preview_text": "The one thing to do first.",
"html": "<p>Hi {{ first_name | there }},</p>\n<p>Thanks for trying Acme. Start with the <a href=\"https://acme.example/setup\">two-minute setup</a>.</p>\n",
"text": "Hi {{ first_name | there }},\n\nThanks for trying Acme. Start with the two-minute setup (https://acme.example/setup).",
"merge_tags": ["first_name"],
"next": {
"type": "branch",
"delay_days": 3,
"condition": {
"type": "opened_step",
"step_id": "j8gxbenyjqwx4hh5344tfjgvq4k7bn7x"
},
"if_true_step_id": "j8b7tfq7xkwo886vompzom75wbbr4qmw",
"if_false_step_id": "2wxfogo4mvn4a4wfhym4l1vfz3zfkkib"
},
"source_template_id": null,
"created_at": 1790000000000
},
{
"id": "j8b7tfq7xkwo886vompzom75wbbr4qmw",
"campaign_id": "h5dnsipzz5fk2z9ri19r0wyojfljooa5",
"delay_days": 0,
"subject": "Three things Acme does that you might miss",
"preview_text": null,
"html": "<p>Hi {{ first_name | there }}, here are three shortcuts.</p>\n",
"text": "Hi {{ first_name | there }}, here are three shortcuts.",
"merge_tags": ["first_name"],
"next": {
"type": "end"
},
"source_template_id": null,
"created_at": 1790000000000
},
{
"id": "2wxfogo4mvn4a4wfhym4l1vfz3zfkkib",
"campaign_id": "h5dnsipzz5fk2z9ri19r0wyojfljooa5",
"delay_days": 0,
"subject": "Still there, {{ first_name | there }}?",
"preview_text": "Most people get stuck on the same step.",
"html": "<p>Most people get stuck on the same step. Here is the shortcut.</p>\n",
"text": "Most people get stuck on the same step. Here is the shortcut.",
"merge_tags": ["first_name"],
"next": {
"type": "end"
},
"source_template_id": null,
"created_at": 1790000000000
}
],
"created_at": 1790000000000
}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 | campaign_not_found | Campaign not found. |
| 409 | invalid_status_transition | A draft campaign cannot become paused. Set it live first. |
| 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 campaign
DELETE/campaigns/{campaign_id}
Only a campaign nobody was ever enrolled in can be deleted (409 otherwise); pause the others.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| campaign_id | path | string | Required |
curl -X DELETE "https://sendocado.com/api/v1/campaigns/$CAMPAIGN_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 | campaign_not_found | Campaign 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 campaign can go live
GET/campaigns/{campaign_id}/readiness
What would refuse setting this campaign live right now. An empty list means it can go live.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| campaign_id | path | string | Required |
curl "https://sendocado.com/api/v1/campaigns/$CAMPAIGN_ID/readiness" \
-H "Authorization: Bearer $SENDOCADO_API_KEY"{
"problems": ["domain_not_verified"]
}Errors
| Status | Type | Example message |
|---|---|---|
| 401 | unauthorized | Send a valid API key as "Authorization: Bearer sc_…", or sign in. |
| 404 | campaign_not_found | Campaign not found. |
| 429 | rate_limited | Too many requests. Try again in 12 seconds. |
Add a step
POST/campaigns/{campaign_id}/steps
insert_after says where the step goes: at the start, after a step, or first on a branch’s true or false path. What followed that point now follows the new step.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| campaign_id | path | string | Required |
Body
Send JSON with every field; use null for a value you do not set.
| Field | Type | Notes |
|---|---|---|
| insert_after | object | Required |
| insert_after.type | "start" | Required, when type is "start" |
| insert_after.type | "step" | "if_true" | "if_false" | Required, when type is "step" | "if_true" | "if_false" |
| insert_after.step_id | string | Required, when type is "step" | "if_true" | "if_false" |
| delay_days | integer | Required |
| 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/campaigns/$CAMPAIGN_ID/steps" \
-H "Authorization: Bearer $SENDOCADO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"insert_after": {
"type": "if_true",
"step_id": "j8gxbenyjqwx4hh5344tfjgvq4k7bn7x"
},
"delay_days": 0,
"subject": "Three things Acme does that you might miss",
"preview_text": null,
"html": null,
"markdown": "Hi {{ first_name | there }}, here are three shortcuts.",
"template_id": null
}'{
"id": "j8b7tfq7xkwo886vompzom75wbbr4qmw",
"campaign_id": "h5dnsipzz5fk2z9ri19r0wyojfljooa5",
"delay_days": 0,
"subject": "Three things Acme does that you might miss",
"preview_text": null,
"html": "<p>Hi {{ first_name | there }}, here are three shortcuts.</p>\n",
"text": "Hi {{ first_name | there }}, here are three shortcuts.",
"merge_tags": ["first_name"],
"next": {
"type": "end"
},
"source_template_id": null,
"created_at": 1790000000000
}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 | campaign_not_found | Campaign not found. |
| 409 | conflict | This campaign has enrollments, so it cannot be deleted. Pause it instead. |
| 422 | invalid_reference | No template with that id in this project. |
| 429 | rate_limited | Too many requests. Try again in 12 seconds. |
Get a step
GET/campaign-steps/{step_id}
A step’s id never changes, so its stats and the people waiting for it survive edits and moves.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| step_id | path | string | Required |
curl "https://sendocado.com/api/v1/campaign-steps/$STEP_ID" \
-H "Authorization: Bearer $SENDOCADO_API_KEY"{
"id": "j8gxbenyjqwx4hh5344tfjgvq4k7bn7x",
"campaign_id": "h5dnsipzz5fk2z9ri19r0wyojfljooa5",
"delay_days": 0,
"subject": "Welcome to Acme, {{ first_name | there }}",
"preview_text": "The one thing to do first.",
"html": "<p>Hi {{ first_name | there }},</p>\n<p>Thanks for trying Acme. Start with the <a href=\"https://acme.example/setup\">two-minute setup</a>.</p>\n",
"text": "Hi {{ first_name | there }},\n\nThanks for trying Acme. Start with the two-minute setup (https://acme.example/setup).",
"merge_tags": ["first_name"],
"next": {
"type": "branch",
"delay_days": 3,
"condition": {
"type": "opened_step",
"step_id": "j8gxbenyjqwx4hh5344tfjgvq4k7bn7x"
},
"if_true_step_id": "j8b7tfq7xkwo886vompzom75wbbr4qmw",
"if_false_step_id": "2wxfogo4mvn4a4wfhym4l1vfz3zfkkib"
},
"source_template_id": null,
"created_at": 1790000000000
}Errors
| Status | Type | Example message |
|---|---|---|
| 401 | unauthorized | Send a valid API key as "Authorization: Bearer sc_…", or sign in. |
| 404 | campaign_step_not_found | Step not found. |
| 429 | rate_limited | Too many requests. Try again in 12 seconds. |
Update a step
PUT/campaign-steps/{step_id}
Send every field. branch null ends the path after this step, or keeps the step it already leads to; a branch adds an if/else check delay_days after the email, on whether the person opened or clicked a step, has a tag, or has an attribute value.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| step_id | path | string | Required |
Body
Send JSON with every field; use null for a value you do not set.
| Field | Type | Notes |
|---|---|---|
| delay_days | integer | Required |
| subject | string | null | Required |
| preview_text | string | null | Required |
| html | string | null | Required |
| markdown | string | null | Required |
| template_id | string | null | Required |
| branch | object | null | Required |
| branch.delay_days | integer | Required |
| branch.condition | object | Required |
| branch.condition.type | "opened_step" | "clicked_step" | Required, when type is "opened_step" | "clicked_step" |
| branch.condition.step_id | string | Required, when type is "opened_step" | "clicked_step" |
| branch.condition.type | "has_tag" | Required, when type is "has_tag" |
| branch.condition.tag | string | Required, when type is "has_tag" |
| branch.condition.type | "attribute_equals" | Required, when type is "attribute_equals" |
| branch.condition.key | string | Required, when type is "attribute_equals" |
| branch.condition.value | string | Required, when type is "attribute_equals" |
curl -X PUT "https://sendocado.com/api/v1/campaign-steps/$STEP_ID" \
-H "Authorization: Bearer $SENDOCADO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"delay_days": 0,
"subject": "Welcome to Acme, {{ first_name | there }}",
"preview_text": "The one thing to do first.",
"html": "<p>Hi {{ first_name | there }},</p>\n<p>Thanks for trying Acme. Start with the <a href=\"https://acme.example/setup\">two-minute setup</a>.</p>\n",
"markdown": null,
"template_id": null,
"branch": {
"delay_days": 3,
"condition": {
"type": "opened_step",
"step_id": "j8gxbenyjqwx4hh5344tfjgvq4k7bn7x"
}
}
}'{
"id": "j8gxbenyjqwx4hh5344tfjgvq4k7bn7x",
"campaign_id": "h5dnsipzz5fk2z9ri19r0wyojfljooa5",
"delay_days": 0,
"subject": "Welcome to Acme, {{ first_name | there }}",
"preview_text": "The one thing to do first.",
"html": "<p>Hi {{ first_name | there }},</p>\n<p>Thanks for trying Acme. Start with the <a href=\"https://acme.example/setup\">two-minute setup</a>.</p>\n",
"text": "Hi {{ first_name | there }},\n\nThanks for trying Acme. Start with the two-minute setup (https://acme.example/setup).",
"merge_tags": ["first_name"],
"next": {
"type": "branch",
"delay_days": 3,
"condition": {
"type": "opened_step",
"step_id": "j8gxbenyjqwx4hh5344tfjgvq4k7bn7x"
},
"if_true_step_id": "j8b7tfq7xkwo886vompzom75wbbr4qmw",
"if_false_step_id": "2wxfogo4mvn4a4wfhym4l1vfz3zfkkib"
},
"source_template_id": null,
"created_at": 1790000000000
}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 | campaign_step_not_found | Step not found. |
| 409 | conflict | This campaign has enrollments, so it cannot be deleted. Pause it instead. |
| 422 | invalid_reference | No template with that id in this project. |
| 429 | rate_limited | Too many requests. Try again in 12 seconds. |
Remove a step
DELETE/campaign-steps/{step_id}
People waiting for it continue with the step after it. A step with a branch, or one a branch condition names, cannot be removed (409).
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| step_id | path | string | Required |
curl -X DELETE "https://sendocado.com/api/v1/campaign-steps/$STEP_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 | campaign_step_not_found | Step 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. |
Move a step
POST/campaign-steps/{step_id}/moves
Moves the step, with its id and stats, to another point in the campaign; the steps around its old place close the gap.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| step_id | path | string | Required |
Body
Send JSON with every field; use null for a value you do not set.
| Field | Type | Notes |
|---|---|---|
| insert_after | object | Required |
| insert_after.type | "start" | Required, when type is "start" |
| insert_after.type | "step" | "if_true" | "if_false" | Required, when type is "step" | "if_true" | "if_false" |
| insert_after.step_id | string | Required, when type is "step" | "if_true" | "if_false" |
curl -X POST "https://sendocado.com/api/v1/campaign-steps/$STEP_ID/moves" \
-H "Authorization: Bearer $SENDOCADO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"insert_after": {
"type": "start"
}
}'{
"id": "2wxfogo4mvn4a4wfhym4l1vfz3zfkkib",
"campaign_id": "h5dnsipzz5fk2z9ri19r0wyojfljooa5",
"delay_days": 0,
"subject": "Still there, {{ first_name | there }}?",
"preview_text": "Most people get stuck on the same step.",
"html": "<p>Most people get stuck on the same step. Here is the shortcut.</p>\n",
"text": "Most people get stuck on the same step. Here is the shortcut.",
"merge_tags": ["first_name"],
"next": {
"type": "end"
},
"source_template_id": null,
"created_at": 1790000000000
}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 | campaign_step_not_found | Step not found. |
| 409 | conflict | This campaign has enrollments, so it cannot be deleted. Pause it instead. |
| 422 | invalid_reference | No template with that id in this project. |
| 429 | rate_limited | Too many requests. Try again in 12 seconds. |
List the people in a campaign
GET/campaigns/{campaign_id}/enrollments
active lists people still in the campaign by when their next email is due; the other statuses list exits, newest first.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| campaign_id | path | string | Required |
| status | query | "active" | "completed" | "unsubscribed" | "blocked" | "exited" | Optional, default "active" |
| cursor | query | string | null | Optional |
| limit | query | integer | Optional, default 50 |
curl "https://sendocado.com/api/v1/campaigns/$CAMPAIGN_ID/enrollments?status=active" \
-H "Authorization: Bearer $SENDOCADO_API_KEY"{
"enrollments": [
{
"id": "j3j4wj99ibag7i1mnbqns6puq80idw37",
"campaign_id": "h5dnsipzz5fk2z9ri19r0wyojfljooa5",
"contact": {
"id": "j8ht9lgmxg9edn581u33xtplpft75v2s",
"email": "maya@example.com",
"first_name": "Maya",
"last_name": "Patel"
},
"status": "active",
"exit_reason": null,
"next_action": {
"type": "check_branch",
"step_id": "j8gxbenyjqwx4hh5344tfjgvq4k7bn7x",
"due_at": 1790259200000
},
"created_at": 1790000060000,
"exited_at": null
}
],
"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 | campaign_not_found | Campaign not found. |
| 429 | rate_limited | Too many requests. Try again in 12 seconds. |
Take someone out of a campaign
PUT/enrollments/{enrollment_id}
status exited ends the enrollment now, with exit reason removed. The person does not join that campaign again.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| enrollment_id | path | string | Required |
Body
Send JSON with every field; use null for a value you do not set.
| Field | Type | Notes |
|---|---|---|
| status | "exited" | Required |
curl -X PUT "https://sendocado.com/api/v1/enrollments/$ENROLLMENT_ID" \
-H "Authorization: Bearer $SENDOCADO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"status": "exited"
}'{
"id": "j3j4wj99ibag7i1mnbqns6puq80idw37",
"campaign_id": "h5dnsipzz5fk2z9ri19r0wyojfljooa5",
"contact": {
"id": "j8ht9lgmxg9edn581u33xtplpft75v2s",
"email": "maya@example.com",
"first_name": "Maya",
"last_name": "Patel"
},
"status": "exited",
"exit_reason": "removed",
"next_action": null,
"created_at": 1790000060000,
"exited_at": 1790086400000
}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 | enrollment_not_found | Enrollment not found. |
| 409 | invalid_status_transition | A draft campaign cannot become paused. Set it live first. |
| 429 | rate_limited | Too many requests. Try again in 12 seconds. |
Get campaign stats
GET/campaigns/{campaign_id}/stats
Counts land on the UTC day each email was sent, so a range’s rates are that range’s emails and what they led to. Without days, the last 30. Rates are fractions of sent_count, null when nothing was sent.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| campaign_id | path | string | Required |
| from_day | query | string | null | Optional |
| to_day | query | string | null | Optional |
curl "https://sendocado.com/api/v1/campaigns/$CAMPAIGN_ID/stats?from_day=2026-09-21&to_day=2026-09-21" \
-H "Authorization: Bearer $SENDOCADO_API_KEY"{
"from_day": "2026-09-21",
"to_day": "2026-09-21",
"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
},
"enrollments": {
"enrollment_count": 412,
"num_active": 158,
"completed_count": 201,
"exit_counts": {
"completed": 201,
"replied": 38,
"unsubscribed": 9,
"blocked": 3,
"filter_mismatch": 2,
"send_failed": 1,
"step_removed": 0,
"contact_deleted": 0,
"removed": 0
}
},
"steps": [
{
"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,
"step_id": "j8gxbenyjqwx4hh5344tfjgvq4k7bn7x"
}
],
"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": [
{
"step_id": "j8gxbenyjqwx4hh5344tfjgvq4k7bn7x",
"url": "https://acme.example/setup",
"click_count": 203,
"unique_click_count": 187
}
]
}Errors
| Status | Type | Example message |
|---|---|---|
| 400 | invalid_data | The query string is not valid: check each field against the reference. |
| 401 | unauthorized | Send a valid API key as "Authorization: Bearer sc_…", or sign in. |
| 404 | campaign_not_found | Campaign not found. |
| 429 | rate_limited | Too many requests. Try again in 12 seconds. |
Preview who an audience matches
POST/projects/{project_id}/audience-previews
Counts the contacts who match filter_groups now, with a few of them. Large projects are counted in parts: while is_complete is false, send the returned cursor back and add the counts up.
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 |
|---|---|---|
| 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" |
| cursor | string | null | Required |
curl -X POST "https://sendocado.com/api/v1/projects/$PROJECT_ID/audience-previews" \
-H "Authorization: Bearer $SENDOCADO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"filter_groups": [
{
"filters": [
{
"type": "tag",
"op": "has",
"tag": "trial"
},
{
"type": "added_days_ago",
"op": "less_than",
"days": 14
}
]
}
],
"cursor": null
}'{
"matched_count": 158,
"scanned_count": 2418,
"is_complete": true,
"cursor": null,
"sample": [
{
"id": "j8ht9lgmxg9edn581u33xtplpft75v2s",
"email": "maya@example.com",
"first_name": "Maya",
"last_name": "Patel"
}
]
}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. |
Draft a campaign with AI
POST/projects/{project_id}/campaign-drafts
Describe who should get what; Claude writes a draft in the background. The draft starts pending; read it until it is ready or failed. A ready draft’s result is the body of a campaign create, without the schedule.
Limited to 10 requests every 60 minutes, on top of the API limit.
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 |
|---|---|---|
| prompt | string | Required |
curl -X POST "https://sendocado.com/api/v1/projects/$PROJECT_ID/campaign-drafts" \
-H "Authorization: Bearer $SENDOCADO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"prompt": "Welcome new trial users over their first week and get them to finish setup."
}'{
"id": "b0y07nyrvd5rxi67nfrpyz21tbic145a",
"project_id": "ujzde8gxd6ncf10epf91dhodzdoc9is0",
"prompt": "Welcome new trial users over their first week and get them to finish setup.",
"status": "pending",
"result": null,
"error": null,
"created_at": 1790000000000
}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. |
| 429 | rate_limited | Too many requests. Try again in 12 seconds. |
Get an AI campaign draft
GET/campaign-drafts/{campaign_draft_id}
status is pending while Claude writes, then ready with result, or failed with error.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| campaign_draft_id | path | string | Required |
curl "https://sendocado.com/api/v1/campaign-drafts/$CAMPAIGN_DRAFT_ID" \
-H "Authorization: Bearer $SENDOCADO_API_KEY"{
"id": "b0y07nyrvd5rxi67nfrpyz21tbic145a",
"project_id": "ujzde8gxd6ncf10epf91dhodzdoc9is0",
"prompt": "Welcome new trial users over their first week and get them to finish setup.",
"status": "ready",
"result": {
"name": "Trial welcome",
"filter_groups": [
{
"filters": [
{
"type": "tag",
"op": "has",
"tag": "trial"
},
{
"type": "added_days_ago",
"op": "less_than",
"days": 14
}
]
}
],
"steps": [
{
"delay_days": 0,
"subject": "Welcome to Acme, {{ first_name | there }}",
"preview_text": "The one thing to do first.",
"html": "<p>Hi {{ first_name | there }},</p>\n<p>Thanks for trying Acme. Start with the <a href=\"https://acme.example/setup\">two-minute setup</a>.</p>\n"
}
]
},
"error": null,
"created_at": 1790000000000
}Errors
| Status | Type | Example message |
|---|---|---|
| 401 | unauthorized | Send a valid API key as "Authorization: Bearer sc_…", or sign in. |
| 404 | campaign_draft_not_found | Campaign draft not found. |
| 429 | rate_limited | Too many requests. Try again in 12 seconds. |