Skip to content

Campaigns

Automated series of emails: the audience, the steps and their branches, status changes, enrollments, stats and AI drafts.

View as Markdown

List campaigns

GET/projects/{project_id}/campaigns

Newest first, without the steps; get one campaign for its emails.

Parameters

NameInTypeNotes
project_idpathstringRequired
statusquery"draft" | "live" | "paused" | nullOptional
cursorquerystring | nullOptional
limitqueryintegerOptional, default 50
Request
curl "https://sendocado.com/api/v1/projects/$PROJECT_ID/campaigns?status=live" \
  -H "Authorization: Bearer $SENDOCADO_API_KEY"
Response 200
{
  "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

StatusTypeExample message
400invalid_datalimit: Too big: expected number to be <=200
401unauthorizedSend a valid API key as "Authorization: Bearer sc_…", or sign in.
404project_not_foundProject not found.
429rate_limitedToo 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

NameInTypeNotes
project_idpathstringRequired

Body

Send JSON with every field; use null for a value you do not set.

FieldTypeNotes
namestringRequired
timezonestringRequired
send_windowobject | nullRequired
send_window.daysinteger[]Required
send_window.start_hourintegerRequired
send_window.end_hourintegerRequired
is_stopped_on_replybooleanRequired
filter_groupsobject[]Required
filter_groups[].filtersobject[]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[].tagstringRequired, 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[].keystringRequired, when type is "attribute"
filter_groups[].filters[].valuestringRequired, 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[].daysintegerRequired, 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[].daysintegerRequired, 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_idstringRequired, when type is "campaign"
stepsobject[]Required
steps[].delay_daysintegerRequired
steps[].subjectstring | nullRequired
steps[].preview_textstring | nullRequired
steps[].htmlstring | nullRequired
steps[].markdownstring | nullRequired
steps[].template_idstring | nullRequired
Request
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
    }
  ]
}'
Response 201
{
  "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

StatusTypeExample message
400invalid_dataSend a JSON request body.
401unauthorizedSend a valid API key as "Authorization: Bearer sc_…", or sign in.
404project_not_foundProject not found.
422invalid_referenceNo template with that id in this project.
429rate_limitedToo 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

NameInTypeNotes
campaign_idpathstringRequired
Request
curl "https://sendocado.com/api/v1/campaigns/$CAMPAIGN_ID" \
  -H "Authorization: Bearer $SENDOCADO_API_KEY"
Response 200
{
  "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

StatusTypeExample message
401unauthorizedSend a valid API key as "Authorization: Bearer sc_…", or sign in.
404campaign_not_foundCampaign not found.
429rate_limitedToo 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

NameInTypeNotes
campaign_idpathstringRequired

Body

Send JSON with every field; use null for a value you do not set.

FieldTypeNotes
namestringRequired
status"draft" | "live" | "paused"Required
timezonestringRequired
send_windowobject | nullRequired
send_window.daysinteger[]Required
send_window.start_hourintegerRequired
send_window.end_hourintegerRequired
is_stopped_on_replybooleanRequired
filter_groupsobject[]Required
filter_groups[].filtersobject[]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[].tagstringRequired, 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[].keystringRequired, when type is "attribute"
filter_groups[].filters[].valuestringRequired, 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[].daysintegerRequired, 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[].daysintegerRequired, 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_idstringRequired, when type is "campaign"
Request
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
        }
      ]
    }
  ]
}'
Response 200
{
  "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

StatusTypeExample message
400invalid_dataSend a JSON request body.
401unauthorizedSend a valid API key as "Authorization: Bearer sc_…", or sign in.
404campaign_not_foundCampaign not found.
409invalid_status_transitionA draft campaign cannot become paused. Set it live first.
422campaign_incompleteAdd at least one email with a subject and content before going live.
422sender_not_readyVerify the domain of your from address before you send email.
422postal_address_missingAdd a postal address to the project before you send marketing email.
422invalid_referenceNo template with that id in this project.
429rate_limitedToo 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

NameInTypeNotes
campaign_idpathstringRequired
Request
curl -X DELETE "https://sendocado.com/api/v1/campaigns/$CAMPAIGN_ID" \
  -H "Authorization: Bearer $SENDOCADO_API_KEY"

Answers 204 with no body.

Errors

StatusTypeExample message
401unauthorizedSend a valid API key as "Authorization: Bearer sc_…", or sign in.
404campaign_not_foundCampaign not found.
409conflictThis campaign has enrollments, so it cannot be deleted. Pause it instead.
429rate_limitedToo 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

NameInTypeNotes
campaign_idpathstringRequired
Request
curl "https://sendocado.com/api/v1/campaigns/$CAMPAIGN_ID/readiness" \
  -H "Authorization: Bearer $SENDOCADO_API_KEY"
Response 200
{
  "problems": ["domain_not_verified"]
}

Errors

StatusTypeExample message
401unauthorizedSend a valid API key as "Authorization: Bearer sc_…", or sign in.
404campaign_not_foundCampaign not found.
429rate_limitedToo 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

NameInTypeNotes
campaign_idpathstringRequired

Body

Send JSON with every field; use null for a value you do not set.

FieldTypeNotes
insert_afterobjectRequired
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_idstringRequired, when type is "step" | "if_true" | "if_false"
delay_daysintegerRequired
subjectstring | nullRequired
preview_textstring | nullRequired
htmlstring | nullRequired
markdownstring | nullRequired
template_idstring | nullRequired
Request
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
}'
Response 201
{
  "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

StatusTypeExample message
400invalid_dataSend a JSON request body.
401unauthorizedSend a valid API key as "Authorization: Bearer sc_…", or sign in.
404campaign_not_foundCampaign not found.
409conflictThis campaign has enrollments, so it cannot be deleted. Pause it instead.
422invalid_referenceNo template with that id in this project.
429rate_limitedToo 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

NameInTypeNotes
step_idpathstringRequired
Request
curl "https://sendocado.com/api/v1/campaign-steps/$STEP_ID" \
  -H "Authorization: Bearer $SENDOCADO_API_KEY"
Response 200
{
  "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

StatusTypeExample message
401unauthorizedSend a valid API key as "Authorization: Bearer sc_…", or sign in.
404campaign_step_not_foundStep not found.
429rate_limitedToo 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

NameInTypeNotes
step_idpathstringRequired

Body

Send JSON with every field; use null for a value you do not set.

FieldTypeNotes
delay_daysintegerRequired
subjectstring | nullRequired
preview_textstring | nullRequired
htmlstring | nullRequired
markdownstring | nullRequired
template_idstring | nullRequired
branchobject | nullRequired
branch.delay_daysintegerRequired
branch.conditionobjectRequired
branch.condition.type"opened_step" | "clicked_step"Required, when type is "opened_step" | "clicked_step"
branch.condition.step_idstringRequired, when type is "opened_step" | "clicked_step"
branch.condition.type"has_tag"Required, when type is "has_tag"
branch.condition.tagstringRequired, when type is "has_tag"
branch.condition.type"attribute_equals"Required, when type is "attribute_equals"
branch.condition.keystringRequired, when type is "attribute_equals"
branch.condition.valuestringRequired, when type is "attribute_equals"
Request
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"
    }
  }
}'
Response 200
{
  "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

StatusTypeExample message
400invalid_dataSend a JSON request body.
401unauthorizedSend a valid API key as "Authorization: Bearer sc_…", or sign in.
404campaign_step_not_foundStep not found.
409conflictThis campaign has enrollments, so it cannot be deleted. Pause it instead.
422invalid_referenceNo template with that id in this project.
429rate_limitedToo 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

NameInTypeNotes
step_idpathstringRequired
Request
curl -X DELETE "https://sendocado.com/api/v1/campaign-steps/$STEP_ID" \
  -H "Authorization: Bearer $SENDOCADO_API_KEY"

Answers 204 with no body.

Errors

StatusTypeExample message
401unauthorizedSend a valid API key as "Authorization: Bearer sc_…", or sign in.
404campaign_step_not_foundStep not found.
409conflictThis campaign has enrollments, so it cannot be deleted. Pause it instead.
429rate_limitedToo 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

NameInTypeNotes
step_idpathstringRequired

Body

Send JSON with every field; use null for a value you do not set.

FieldTypeNotes
insert_afterobjectRequired
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_idstringRequired, when type is "step" | "if_true" | "if_false"
Request
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"
  }
}'
Response 200
{
  "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

StatusTypeExample message
400invalid_dataSend a JSON request body.
401unauthorizedSend a valid API key as "Authorization: Bearer sc_…", or sign in.
404campaign_step_not_foundStep not found.
409conflictThis campaign has enrollments, so it cannot be deleted. Pause it instead.
422invalid_referenceNo template with that id in this project.
429rate_limitedToo 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

NameInTypeNotes
campaign_idpathstringRequired
statusquery"active" | "completed" | "unsubscribed" | "blocked" | "exited"Optional, default "active"
cursorquerystring | nullOptional
limitqueryintegerOptional, default 50
Request
curl "https://sendocado.com/api/v1/campaigns/$CAMPAIGN_ID/enrollments?status=active" \
  -H "Authorization: Bearer $SENDOCADO_API_KEY"
Response 200
{
  "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

StatusTypeExample message
400invalid_datalimit: Too big: expected number to be <=200
401unauthorizedSend a valid API key as "Authorization: Bearer sc_…", or sign in.
404campaign_not_foundCampaign not found.
429rate_limitedToo 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

NameInTypeNotes
enrollment_idpathstringRequired

Body

Send JSON with every field; use null for a value you do not set.

FieldTypeNotes
status"exited"Required
Request
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"
}'
Response 200
{
  "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

StatusTypeExample message
400invalid_dataSend a JSON request body.
401unauthorizedSend a valid API key as "Authorization: Bearer sc_…", or sign in.
404enrollment_not_foundEnrollment not found.
409invalid_status_transitionA draft campaign cannot become paused. Set it live first.
429rate_limitedToo 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

NameInTypeNotes
campaign_idpathstringRequired
from_dayquerystring | nullOptional
to_dayquerystring | nullOptional
Request
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"
Response 200
{
  "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

StatusTypeExample message
400invalid_dataThe query string is not valid: check each field against the reference.
401unauthorizedSend a valid API key as "Authorization: Bearer sc_…", or sign in.
404campaign_not_foundCampaign not found.
429rate_limitedToo 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

NameInTypeNotes
project_idpathstringRequired

Body

Send JSON with every field; use null for a value you do not set.

FieldTypeNotes
filter_groupsobject[]Required
filter_groups[].filtersobject[]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[].tagstringRequired, 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[].keystringRequired, when type is "attribute"
filter_groups[].filters[].valuestringRequired, 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[].daysintegerRequired, 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[].daysintegerRequired, 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_idstringRequired, when type is "campaign"
cursorstring | nullRequired
Request
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
}'
Response 200
{
  "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

StatusTypeExample message
400invalid_dataSend a JSON request body.
401unauthorizedSend a valid API key as "Authorization: Bearer sc_…", or sign in.
404project_not_foundProject not found.
422invalid_referenceNo template with that id in this project.
429rate_limitedToo 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

NameInTypeNotes
project_idpathstringRequired

Body

Send JSON with every field; use null for a value you do not set.

FieldTypeNotes
promptstringRequired
Request
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."
}'
Response 201
{
  "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

StatusTypeExample message
400invalid_dataSend a JSON request body.
401unauthorizedSend a valid API key as "Authorization: Bearer sc_…", or sign in.
404project_not_foundProject not found.
429rate_limitedToo 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

NameInTypeNotes
campaign_draft_idpathstringRequired
Request
curl "https://sendocado.com/api/v1/campaign-drafts/$CAMPAIGN_DRAFT_ID" \
  -H "Authorization: Bearer $SENDOCADO_API_KEY"
Response 200
{
  "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

StatusTypeExample message
401unauthorizedSend a valid API key as "Authorization: Bearer sc_…", or sign in.
404campaign_draft_not_foundCampaign draft not found.
429rate_limitedToo many requests. Try again in 12 seconds.