# Campaigns

A series of emails that people join on their own: steps, waits, branches, send windows and going live.

A campaign is an [audience](https://sendocado.com/docs/audiences) plus a series of emails, called steps. People who match the audience of a live campaign join it on their own and get its steps, in order, with waits between them.

## Steps and waits

- Each step has a `delay_days`: the wait before it is sent, counted from the step before (or from joining, for the first step). Up to 365 days.
- A campaign has up to 50 steps.
- Each step’s content is HTML, markdown or a copy of a template; see [Email content](https://sendocado.com/docs/email-content).
- A step’s id never changes. Edit, move or remove steps one at a time with the step routes; people already waiting keep their place, and stats stay with the step.

Add a step after another one, at the start, or first on a branch path:

```sh Add a step
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
}'
```

## Branches

After a step, a campaign can wait and then take one of two paths:

- **opened_step** or **clicked_step**: did the person open, or click a link in, a step on their path?
- **has_tag**: does the contact have a tag now?
- **attribute_equals**: does the contact have this attribute value now?

The step’s `next` shows what follows it: another step, the end, or a branch with an `if_true_step_id` and an `if_false_step_id` (`null` ends that path). Set a branch with the step update:

```sh Branch on whether the welcome email was opened
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"
    }
  }
}'
```

## Send windows and time zones

`send_window` limits sending to some days and hours, for example weekdays from 9 to 17. Days run from 0 (Sunday) to 6. Hours use the contact’s time zone when it has one, else the campaign’s. An email due outside the window waits for the next window start. Sendocado looks for due emails every 60 seconds.

## Status

| From | To | What happens |
| --- | --- | --- |
| `draft` | `live` | Readiness checks pass, then every matching contact joins |
| `live` | `paused` | Nothing more is sent; people keep their place |
| `paused` | `live` | Sending picks up where it stopped |

Change the status with an update to the campaign. Any other move is `409 invalid_status_transition`. Edits to a live campaign apply at once.

## Readiness

A campaign can go live when it has at least one step with a subject and content, the project has a working sender (with a verified domain, for providers that use one), and the project has a postal address. Ask what is missing before you try:

```sh Check readiness
curl "https://sendocado.com/api/v1/campaigns/$CAMPAIGN_ID/readiness" \
  -H "Authorization: Bearer $SENDOCADO_API_KEY"
```

```json Response
{
  "problems": ["domain_not_verified"]
}
```

## Enrollments

A person in a campaign is an enrollment. A contact joins each campaign at most once. An enrollment ends with one of these exit reasons:

| Exit reason | Why |
| --- | --- |
| `completed` | The last step was sent |
| `replied` | The person replied and the campaign stops on reply (`is_stopped_on_reply`) |
| `unsubscribed` | The person unsubscribed |
| `blocked` | The address bounced, reported spam or was blocked |
| `filter_mismatch` | The contact no longer matches the audience |
| `send_failed` | An email to them could not be sent |
| `step_removed` | The step they waited for was removed and nothing followed it |
| `contact_deleted` | The contact was deleted |
| `removed` | You took them out |

Automatic replies, like out-of-office messages, never end an enrollment.

## Drafting with AI

Describe who should get what, and Claude writes the audience and the emails. Drafting runs in the background: create a draft, then read it until it is `ready`. A ready draft’s `result` is the body of a campaign create, without the schedule. Drafts have their own limit of 10 an hour.

```sh Draft a campaign
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."
}'
```

## Stats

Campaign stats count each email on the UTC day it was sent, so the rates for a range are that range’s emails and what they led to. Stats come per step, per day and per link. See the [Campaigns reference](https://sendocado.com/docs/api/campaigns) for every route.
