> ## Documentation Index
> Fetch the complete documentation index at: https://docs.bluereacher.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Campaigns and sequences

> Create multi-step iMessage sequences, enroll contacts, cancel pending sends, and pull campaign performance stats from the BlueReacher API.

A campaign is a named, ordered set of message steps. You create the campaign once, then enroll contacts into it. BlueReacher schedules each step, sends it from one of your dedicated iMessage lines, and stops the sequence the moment a contact replies.

Campaigns send over iMessage by default and fall back to SMS when a recipient cannot receive iMessage. No A2P registration required.

Base URL for every endpoint on this page:

```
https://api.bluereacher.com/functions/v1/
```

All requests use `Content-Type: application/json` and a bearer token:

```
Authorization: Bearer bluereacher_your_api_key
```

## How sequences run

### Steps and delays

A campaign holds 1 to 12 steps. Each step has `content`, a `delay`, and optional `conditions`.

Step 1 sends as soon as the contact is enrolled, subject to quiet hours. Its `delay` is ignored. Every step after that waits its `delay` measured from the moment the previous step was **sent**, not from enrollment. A campaign with delays of 0, 2 days, and 3 days sends on day 0, day 2, and day 5.

Delays are objects so you do not have to convert everything to minutes:

```json theme={null}
{ "value": 2, "unit": "days" }
```

`unit` is `minutes`, `hours`, or `days`. Minimum delay between two steps is 15 minutes. Maximum is 90 days.

`content` supports merge fields in double braces, resolved from the `merge_fields` object you pass at enrollment:

```
Hey {{first_name}}, saw {{company}} is hiring SDRs. Worth a look?
```

Add a fallback after a pipe when a value might be missing: `{{first_name|there}}`. A merge field with no value and no fallback blocks the send and marks the message `failed` with error code `merge_field_missing`, so the contact never gets a message with a blank in it.

### Stop-on-reply and step conditions

`stop_on_reply` is `true` by default. Any inbound message from the contact on the campaign's thread ends the sequence: pending steps are cancelled, the enrollment moves to status `replied`, and the reply counts once in your stats. Reactions like a thumbs up do not count as a reply and do not stop the sequence.

Set `stop_on_reply` to `false` when you want every step to run no matter what, then control skipping per step with `conditions`.

Per-step `conditions` fields:

| Field          | Type             | Description                                                                                                         |
| -------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------- |
| `send_if`      | string           | `no_reply` (default), `always`, or `no_booking`. `no_booking` skips the step if the contact has a booking recorded. |
| `skip_if_tags` | array of strings | Skip this step if the contact carries any of these tags. Useful for suppressing a step for existing customers.      |
| `channel`      | string           | Override the campaign channel for this step. One of `imessage`, `rcs`, `sms`.                                       |

A skipped step does not delay the rest of the sequence. The next step's delay still counts from the previous step that actually sent.

Opt-out keywords are handled before any condition. If a contact replies with STOP, UNSUBSCRIBE, or any keyword on your account opt-out list, the enrollment ends with status `opted_out` and the contact is suppressed from all future campaigns on your account.

### Quiet hours and pacing

Every campaign has a send window. The default window is 09:00 to 20:00 in the contact's local time, Monday through Friday.

Timezone resolution happens per contact, in this order:

1. The `timezone` you pass on the contact record or at enrollment (IANA name, such as `America/Chicago`).
2. The timezone inferred from the recipient number.
3. Your account timezone.

A step scheduled outside the window rolls forward to the next time the window opens, plus a random offset of 0 to 40 minutes so your whole queue does not fire at 09:00 exactly. A step scheduled inside the window sends inside the window. Quiet hours are always enforced. There is no per-contact override.

BlueReacher also paces sends automatically. Messages on the same line are spaced 45 to 90 seconds apart, and each line carries a default cap of 200 campaign messages per day. Enrollments beyond that cap queue for the next day rather than failing. Raise or lower the cap with `daily_send_limit`.

## Create a campaign

Creates a campaign and its steps. Campaigns are created in `draft` status unless you set `status` to `active`. A draft campaign accepts enrollments but sends nothing until you activate it.

```
POST https://api.bluereacher.com/functions/v1/create-campaign
```

Auth: send `Authorization: Bearer bluereacher_your_api_key`. The key determines which account and lines the campaign belongs to.

### Request fields

| Field              | Type             | Required | Description                                                                                                                                                          |
| ------------------ | ---------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`             | string           | Yes      | Internal name, 1 to 120 characters. Not visible to recipients.                                                                                                       |
| `steps`            | array            | Yes      | 1 to 12 step objects, in send order. See the step table below.                                                                                                       |
| `channel`          | string           | No       | `imessage` (default), `rcs`, or `sms`. Sets the channel for every step that does not override it.                                                                    |
| `fallback_channel` | string           | No       | `sms` (default), `rcs`, or `none`. Used when the recipient cannot receive on `channel`. `none` marks the message `failed` instead of falling back.                   |
| `line_ids`         | array of strings | No       | Pool of dedicated lines to send from. Contacts are assigned one line at enrollment and keep it for the whole sequence. Defaults to every active line on the account. |
| `stop_on_reply`    | boolean          | No       | Defaults to `true`. See stop-on-reply above.                                                                                                                         |
| `quiet_hours`      | object           | No       | `{ "start": "09:00", "end": "20:00", "days": ["mon","tue","wed","thu","fri"] }`. Times are 24 hour, in the contact's local time. Omit to use the default window.     |
| `daily_send_limit` | integer          | No       | Campaign messages per line per day. Defaults to 200, maximum 500.                                                                                                    |
| `status`           | string           | No       | `draft` (default) or `active`.                                                                                                                                       |
| `external_id`      | string           | No       | Your own identifier, up to 128 characters, unique per account. Reusing one returns the existing campaign with `200` instead of creating a duplicate.                 |

Step object:

| Field        | Type   | Required                       | Description                                                                          |
| ------------ | ------ | ------------------------------ | ------------------------------------------------------------------------------------ |
| `content`    | string | Yes, unless `media_url` is set | Message body, up to 1200 characters. Supports merge fields.                          |
| `media_url`  | string | No                             | Public HTTPS URL of an image, video, or PDF to attach. Maximum 100 MB.               |
| `delay`      | object | Yes, except step 1             | `{ "value": 2, "unit": "days" }`. Minimum 15 minutes between steps, maximum 90 days. |
| `conditions` | object | No                             | See the conditions table above. Defaults to `{ "send_if": "no_reply" }`.             |
| `name`       | string | No                             | Label for the step in stats, up to 60 characters.                                    |

### Response fields

| Field              | Type             | Description                                                                 |
| ------------------ | ---------------- | --------------------------------------------------------------------------- |
| `campaign_id`      | string           | Identifier for the campaign. Pass this to the other endpoints on this page. |
| `name`             | string           | Campaign name you sent.                                                     |
| `status`           | string           | `draft`, `active`, `paused`, or `archived`.                                 |
| `channel`          | string           | Default channel for the campaign.                                           |
| `fallback_channel` | string           | Channel used when the recipient cannot receive on `channel`.                |
| `line_ids`         | array of strings | Lines assigned to this campaign.                                            |
| `stop_on_reply`    | boolean          | Whether an inbound reply ends the sequence.                                 |
| `quiet_hours`      | object           | Resolved send window, including defaults you did not set.                   |
| `daily_send_limit` | integer          | Campaign messages per line per day.                                         |
| `steps`            | array            | Steps with a generated `step_id` and 1 based `position`, in send order.     |
| `external_id`      | string or null   | Your identifier, if you sent one.                                           |
| `created_at`       | string           | ISO 8601 UTC timestamp.                                                     |

### Example request

```bash theme={null}
curl -X POST https://api.bluereacher.com/functions/v1/create-campaign \
  -H "Authorization: Bearer bluereacher_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Q3 agency owners",
    "channel": "imessage",
    "fallback_channel": "sms",
    "line_ids": ["line_8f21c4", "line_3ba907"],
    "stop_on_reply": true,
    "daily_send_limit": 200,
    "status": "active",
    "quiet_hours": {
      "start": "09:00",
      "end": "18:00",
      "days": ["mon", "tue", "wed", "thu", "fri"]
    },
    "external_id": "q3-agency-owners",
    "steps": [
      {
        "name": "Opener",
        "content": "Hey {{first_name|there}}, saw {{company}} runs paid ads for local brands. Mind if I send over a quick idea?"
      },
      {
        "name": "Bump",
        "delay": { "value": 2, "unit": "days" },
        "content": "Following up on this one, {{first_name|there}}. Worth 10 minutes?"
      },
      {
        "name": "Case study",
        "delay": { "value": 4, "unit": "days" },
        "content": "Last note from me. Here is the teardown I mentioned.",
        "media_url": "https://cdn.example.com/teardown.pdf",
        "conditions": {
          "send_if": "no_booking",
          "skip_if_tags": ["customer"]
        }
      }
    ]
  }'
```

### Example response

`201 Created`

```json theme={null}
{
  "campaign_id": "camp_7d19b2ae",
  "name": "Q3 agency owners",
  "status": "active",
  "channel": "imessage",
  "fallback_channel": "sms",
  "line_ids": ["line_8f21c4", "line_3ba907"],
  "stop_on_reply": true,
  "daily_send_limit": 200,
  "quiet_hours": {
    "start": "09:00",
    "end": "18:00",
    "days": ["mon", "tue", "wed", "thu", "fri"],
    "timezone_source": "contact"
  },
  "steps": [
    {
      "step_id": "step_a1",
      "position": 1,
      "name": "Opener",
      "channel": "imessage",
      "delay": { "value": 0, "unit": "minutes" },
      "conditions": { "send_if": "no_reply" }
    },
    {
      "step_id": "step_a2",
      "position": 2,
      "name": "Bump",
      "channel": "imessage",
      "delay": { "value": 2, "unit": "days" },
      "conditions": { "send_if": "no_reply" }
    },
    {
      "step_id": "step_a3",
      "position": 3,
      "name": "Case study",
      "channel": "imessage",
      "delay": { "value": 4, "unit": "days" },
      "conditions": { "send_if": "no_booking", "skip_if_tags": ["customer"] }
    }
  ],
  "external_id": "q3-agency-owners",
  "created_at": "2026-07-24T15:02:41Z"
}
```

### Errors

| Status | Code                  | Cause                                                                        |
| ------ | --------------------- | ---------------------------------------------------------------------------- |
| 400    | `invalid_request`     | Malformed JSON or an unknown field.                                          |
| 401    | `invalid_api_key`     | Missing or bad bearer token.                                                 |
| 403    | `line_not_owned`      | A `line_ids` entry does not belong to your account.                          |
| 422    | `steps_empty`         | `steps` is missing or an empty array.                                        |
| 422    | `too_many_steps`      | More than 12 steps.                                                          |
| 422    | `delay_too_short`     | A delay after step 1 is under 15 minutes.                                    |
| 422    | `content_too_long`    | A step body is over 1200 characters.                                         |
| 422    | `invalid_quiet_hours` | `end` is not after `start`, or `days` contains an unknown value.             |
| 429    | `rate_limited`        | Over the request limit. Retry after the seconds in the `Retry-After` header. |

## Add a contact to a campaign

Enrolls one contact into a campaign. Enrollment assigns a line, resolves the contact's timezone, and schedules step 1.

```
POST https://api.bluereacher.com/functions/v1/add-contact-to-campaign
```

Auth: send `Authorization: Bearer bluereacher_your_api_key`.

Send one contact per request. The rate limit is 600 requests per minute, so a straightforward loop handles bulk enrollment.

If the contact is already enrolled in this campaign with an active status, the request returns `409 already_enrolled` and changes nothing. If the contact is on your opt-out list, the request returns `409 contact_opted_out`.

### Request fields

| Field          | Type             | Required                    | Description                                                                                                                      |
| -------------- | ---------------- | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `campaign_id`  | string           | Yes                         | Campaign to enroll into.                                                                                                         |
| `contact_id`   | string           | One of `contact_id` or `to` | Existing contact on your account.                                                                                                |
| `to`           | string           | One of `contact_id` or `to` | Recipient number in E.164 format, such as `+14155550142`. Creates the contact if it does not exist.                              |
| `from`         | string           | No                          | Pin a specific line in E.164 format. Must be in the campaign's `line_ids`. Omit to let BlueReacher assign one from the pool.     |
| `merge_fields` | object           | No                          | Flat key and string value pairs used to fill `{{tokens}}` in step content. Keys are lowercase letters, numbers, and underscores. |
| `timezone`     | string           | No                          | IANA timezone for this contact, such as `America/New_York`. Overrides the inferred timezone for quiet hours.                     |
| `tags`         | array of strings | No                          | Tags applied to the contact, read by `skip_if_tags`.                                                                             |
| `start_at`     | string           | No                          | ISO 8601 timestamp. Step 1 waits until this time, then still respects quiet hours. Defaults to now.                              |
| `external_id`  | string           | No                          | Your identifier for this enrollment, up to 128 characters. Reusing one returns the existing enrollment with `200`.               |

### Response fields

| Field           | Type           | Description                                                                          |
| --------------- | -------------- | ------------------------------------------------------------------------------------ |
| `enrollment_id` | string         | Identifier for this contact's run through the campaign.                              |
| `campaign_id`   | string         | Campaign the contact joined.                                                         |
| `contact_id`    | string         | Contact identifier, created if you passed `to` for a new number.                     |
| `to`            | string         | Recipient number in E.164.                                                           |
| `from`          | string         | Line assigned for the whole sequence, in E.164.                                      |
| `line_id`       | string         | Identifier of the assigned line.                                                     |
| `channel`       | string         | Channel step 1 will attempt.                                                         |
| `status`        | string         | `scheduled` on success. See the status reference below.                              |
| `current_step`  | integer        | Position of the next step to send. `1` at enrollment.                                |
| `next_send_at`  | string         | ISO 8601 UTC timestamp of the next scheduled send, already adjusted for quiet hours. |
| `timezone`      | string         | Timezone used for this contact's quiet hours.                                        |
| `merge_fields`  | object         | Values stored for this enrollment.                                                   |
| `external_id`   | string or null | Your identifier, if you sent one.                                                    |
| `created_at`    | string         | ISO 8601 UTC timestamp.                                                              |

### Example request

```bash theme={null}
curl -X POST https://api.bluereacher.com/functions/v1/add-contact-to-campaign \
  -H "Authorization: Bearer bluereacher_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "campaign_id": "camp_7d19b2ae",
    "to": "+14155550142",
    "timezone": "America/Los_Angeles",
    "tags": ["inbound-lead"],
    "merge_fields": {
      "first_name": "Dana",
      "company": "Northwind Media"
    },
    "external_id": "crm-lead-90412"
  }'
```

Enrolling a list from Node:

```javascript theme={null}
const leads = [
  { to: "+14155550142", first_name: "Dana", company: "Northwind Media" },
  { to: "+13125550117", first_name: "Marcus", company: "Halo Fitness" }
];

for (const lead of leads) {
  const res = await fetch(
    "https://api.bluereacher.com/functions/v1/add-contact-to-campaign",
    {
      method: "POST",
      headers: {
        Authorization: "Bearer bluereacher_your_api_key",
        "Content-Type": "application/json"
      },
      body: JSON.stringify({
        campaign_id: "camp_7d19b2ae",
        to: lead.to,
        merge_fields: {
          first_name: lead.first_name,
          company: lead.company
        }
      })
    }
  );

  if (res.status === 409) continue; // already enrolled or opted out
  if (!res.ok) throw new Error(`Enroll failed: ${res.status}`);
}
```

### Example response

`201 Created`

```json theme={null}
{
  "enrollment_id": "enr_5c40f8b1",
  "campaign_id": "camp_7d19b2ae",
  "contact_id": "cont_2ea7d9",
  "to": "+14155550142",
  "from": "+16195550188",
  "line_id": "line_8f21c4",
  "channel": "imessage",
  "status": "scheduled",
  "current_step": 1,
  "next_send_at": "2026-07-24T16:24:00Z",
  "timezone": "America/Los_Angeles",
  "merge_fields": {
    "first_name": "Dana",
    "company": "Northwind Media"
  },
  "external_id": "crm-lead-90412",
  "created_at": "2026-07-24T15:11:07Z"
}
```

### Errors

| Status | Code                   | Cause                                                  |
| ------ | ---------------------- | ------------------------------------------------------ |
| 400    | `missing_recipient`    | Neither `contact_id` nor `to` was sent.                |
| 400    | `invalid_phone_number` | `to` is not valid E.164.                               |
| 401    | `invalid_api_key`      | Missing or bad bearer token.                           |
| 403    | `line_not_in_campaign` | `from` is not one of the campaign's lines.             |
| 404    | `campaign_not_found`   | No campaign with that `campaign_id` on your account.   |
| 404    | `contact_not_found`    | No contact with that `contact_id`.                     |
| 409    | `already_enrolled`     | The contact has an active enrollment in this campaign. |
| 409    | `contact_opted_out`    | The contact is on your opt-out list.                   |
| 422    | `campaign_archived`    | The campaign is archived and cannot take enrollments.  |
| 422    | `merge_field_missing`  | A step uses a token with no value and no fallback.     |
| 429    | `rate_limited`         | Over the request limit.                                |

## Cancel pending messages

Stops every message still waiting to go out for a contact. Use this when a lead books a call, replies on another channel, or asks you to stop.

```
POST https://api.bluereacher.com/functions/v1/cancel-messages
```

Auth: send `Authorization: Bearer bluereacher_your_api_key`.

Cancellation covers messages in `scheduled` and `queued` status. A message already handed to the network cannot be recalled, so anything in `sent` or `delivered` stays as it is. Cancelling stops sends and does not add the contact to your opt-out list. To suppress a contact from all future campaigns, add them to your opt-out list instead.

Omit `campaign_id` to cancel across every campaign the contact is in. The endpoint is idempotent. Calling it twice returns `cancelled: 0` the second time.

### Request fields

| Field         | Type   | Required                    | Description                                                                                            |
| ------------- | ------ | --------------------------- | ------------------------------------------------------------------------------------------------------ |
| `contact_id`  | string | One of `contact_id` or `to` | Contact whose pending sends you want stopped.                                                          |
| `to`          | string | One of `contact_id` or `to` | Recipient number in E.164 format.                                                                      |
| `campaign_id` | string | No                          | Limit cancellation to one campaign. Omit to cancel across all campaigns.                               |
| `reason`      | string | No                          | Free text stored on the enrollment, up to 200 characters. Shows in the dashboard and in stats exports. |

### Response fields

| Field                              | Type            | Description                                                          |
| ---------------------------------- | --------------- | -------------------------------------------------------------------- |
| `contact_id`                       | string          | Contact the cancellation applied to.                                 |
| `to`                               | string          | Recipient number in E.164.                                           |
| `cancelled`                        | integer         | Count of pending messages stopped by this request.                   |
| `enrollments`                      | array           | One entry per affected enrollment.                                   |
| `enrollments[].enrollment_id`      | string          | Enrollment identifier.                                               |
| `enrollments[].campaign_id`        | string          | Campaign the enrollment belongs to.                                  |
| `enrollments[].status`             | string          | `cancelled` after this call.                                         |
| `enrollments[].messages_cancelled` | integer         | Pending messages stopped in that enrollment.                         |
| `enrollments[].last_step_sent`     | integer or null | Position of the last step that went out, `null` if nothing sent yet. |
| `reason`                           | string or null  | Reason you sent, if any.                                             |
| `created_at`                       | string          | ISO 8601 UTC timestamp of the cancellation.                          |

### Example request

```bash theme={null}
curl -X POST https://api.bluereacher.com/functions/v1/cancel-messages \
  -H "Authorization: Bearer bluereacher_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+14155550142",
    "campaign_id": "camp_7d19b2ae",
    "reason": "Booked a call through the website form"
  }'
```

### Example response

`200 OK`

```json theme={null}
{
  "contact_id": "cont_2ea7d9",
  "to": "+14155550142",
  "cancelled": 2,
  "enrollments": [
    {
      "enrollment_id": "enr_5c40f8b1",
      "campaign_id": "camp_7d19b2ae",
      "status": "cancelled",
      "messages_cancelled": 2,
      "last_step_sent": 1
    }
  ],
  "reason": "Booked a call through the website form",
  "created_at": "2026-07-24T18:40:12Z"
}
```

### Errors

| Status | Code                   | Cause                                                |
| ------ | ---------------------- | ---------------------------------------------------- |
| 400    | `missing_recipient`    | Neither `contact_id` nor `to` was sent.              |
| 400    | `invalid_phone_number` | `to` is not valid E.164.                             |
| 401    | `invalid_api_key`      | Missing or bad bearer token.                         |
| 404    | `contact_not_found`    | No contact matches on your account.                  |
| 404    | `campaign_not_found`   | No campaign with that `campaign_id` on your account. |
| 429    | `rate_limited`         | Over the request limit.                              |

## Get campaign stats

Returns performance counters for one campaign, with an optional per-step or per-line breakdown.

```
GET https://api.bluereacher.com/functions/v1/get-campaign-stats
```

Auth: send `Authorization: Bearer bluereacher_your_api_key`.

Counters update within about 60 seconds of the underlying event. Rates are decimals rounded to four places, so `0.1834` means 18.34 percent. A rate is `null` when its denominator is zero.

`booked` counts contacts with a booking recorded against the campaign. Bookings come from a connected calendar or from your own system posting to the bookings webhook. With no booking source connected, `booked` and `booking_rate` return `null` rather than `0`, so you can tell "nothing tracked" apart from "nobody booked".

### Query parameters

| Parameter     | Type   | Required | Description                                                                         |
| ------------- | ------ | -------- | ----------------------------------------------------------------------------------- |
| `campaign_id` | string | Yes      | Campaign to report on.                                                              |
| `start_date`  | string | No       | `YYYY-MM-DD`, inclusive. Defaults to the campaign's first send.                     |
| `end_date`    | string | No       | `YYYY-MM-DD`, inclusive. Defaults to today.                                         |
| `timezone`    | string | No       | IANA timezone used to bucket the date range. Defaults to your account timezone.     |
| `group_by`    | string | No       | `total` (default), `step`, or `line`. `step` adds `by_step`, `line` adds `by_line`. |
| `channel`     | string | No       | Filter to `imessage`, `rcs`, or `sms`. Omit for all channels.                       |

### Response fields

| Field               | Type            | Description                                                                   |
| ------------------- | --------------- | ----------------------------------------------------------------------------- |
| `campaign_id`       | string          | Campaign reported on.                                                         |
| `name`              | string          | Campaign name.                                                                |
| `status`            | string          | Current campaign status.                                                      |
| `range`             | object          | `start_date`, `end_date`, and `timezone` applied to the query.                |
| `contacts_enrolled` | integer         | Contacts enrolled in the range.                                               |
| `sent`              | integer         | Messages handed off for delivery.                                             |
| `delivered`         | integer         | Messages confirmed delivered to the recipient.                                |
| `delivery_rate`     | number or null  | `delivered / sent`.                                                           |
| `replied`           | integer         | Contacts who replied at least once. Counted per contact, not per message.     |
| `reply_rate`        | number or null  | `replied / contacts_enrolled` after excluding contacts with nothing sent yet. |
| `booked`            | integer or null | Contacts with a booking recorded. `null` when no booking source is connected. |
| `booking_rate`      | number or null  | `booked / replied`. `null` when `booked` is `null`.                           |
| `opted_out`         | integer         | Contacts who opted out during the range.                                      |
| `failed`            | integer         | Messages that could not be sent, including merge field failures.              |
| `cancelled`         | integer         | Pending messages stopped by `/cancel-messages` or by a reply.                 |
| `pending`           | integer         | Messages scheduled but not yet sent.                                          |
| `by_step`           | array           | Present when `group_by` is `step`. Per step counters.                         |
| `by_line`           | array           | Present when `group_by` is `line`. Per line counters.                         |
| `updated_at`        | string          | ISO 8601 UTC timestamp of the last counter refresh.                           |

### Example request

```bash theme={null}
curl -G https://api.bluereacher.com/functions/v1/get-campaign-stats \
  -H "Authorization: Bearer bluereacher_your_api_key" \
  -H "Content-Type: application/json" \
  --data-urlencode "campaign_id=camp_7d19b2ae" \
  --data-urlencode "start_date=2026-07-01" \
  --data-urlencode "end_date=2026-07-24" \
  --data-urlencode "group_by=step"
```

### Example response

`200 OK`

```json theme={null}
{
  "campaign_id": "camp_7d19b2ae",
  "name": "Q3 agency owners",
  "status": "active",
  "range": {
    "start_date": "2026-07-01",
    "end_date": "2026-07-24",
    "timezone": "America/New_York"
  },
  "contacts_enrolled": 1840,
  "sent": 3127,
  "delivered": 3061,
  "delivery_rate": 0.9789,
  "replied": 341,
  "reply_rate": 0.1853,
  "booked": 47,
  "booking_rate": 0.1378,
  "opted_out": 19,
  "failed": 66,
  "cancelled": 512,
  "pending": 288,
  "by_step": [
    {
      "step_id": "step_a1",
      "position": 1,
      "name": "Opener",
      "sent": 1840,
      "delivered": 1801,
      "replied": 214,
      "reply_rate": 0.1163,
      "booked": 31
    },
    {
      "step_id": "step_a2",
      "position": 2,
      "name": "Bump",
      "sent": 883,
      "delivered": 869,
      "replied": 97,
      "reply_rate": 0.1098,
      "booked": 12
    },
    {
      "step_id": "step_a3",
      "position": 3,
      "name": "Case study",
      "sent": 404,
      "delivered": 391,
      "replied": 30,
      "reply_rate": 0.0743,
      "booked": 4
    }
  ],
  "updated_at": "2026-07-24T19:05:33Z"
}
```

### Errors

| Status | Code                 | Cause                                                             |
| ------ | -------------------- | ----------------------------------------------------------------- |
| 400    | `invalid_date_range` | `end_date` is before `start_date`, or a date is not `YYYY-MM-DD`. |
| 400    | `invalid_group_by`   | `group_by` is not `total`, `step`, or `line`.                     |
| 401    | `invalid_api_key`    | Missing or bad bearer token.                                      |
| 404    | `campaign_not_found` | No campaign with that `campaign_id` on your account.              |
| 429    | `rate_limited`       | Over the request limit.                                           |

## Status reference

Enrollment `status` values:

| Value       | Meaning                                                   |
| ----------- | --------------------------------------------------------- |
| `scheduled` | Waiting on the next step.                                 |
| `running`   | A step is being sent right now.                           |
| `replied`   | The contact replied and the sequence stopped.             |
| `completed` | Every step ran with no reply.                             |
| `cancelled` | Stopped by `/cancel-messages` or by pausing the campaign. |
| `opted_out` | The contact sent an opt-out keyword.                      |
| `failed`    | Every step failed to send.                                |

Message `status` values inside a campaign:

| Value       | Meaning                                                     |
| ----------- | ----------------------------------------------------------- |
| `scheduled` | Waiting for its send time.                                  |
| `queued`    | Send time reached, waiting on line pacing.                  |
| `sent`      | Handed off for delivery.                                    |
| `delivered` | Confirmed delivered to the recipient.                       |
| `failed`    | Could not be sent. Check the `error` object on the message. |
| `cancelled` | Stopped before sending.                                     |

## Error format

Every error returns the same shape:

```json theme={null}
{
  "error": {
    "code": "campaign_not_found",
    "message": "No campaign matches camp_0000000 on this account.",
    "field": "campaign_id"
  }
}
```

`field` is present when the problem maps to a specific request field, and absent otherwise. Treat `code` as the stable value to branch on. Message text can change.

Any `5xx` response is safe to retry with exponential backoff. Use `external_id` on `/create-campaign` and `/add-contact-to-campaign` so retries do not create duplicates.

## Rate limits

600 requests per minute per API key, counted across all endpoints. A `429` includes a `Retry-After` header with the seconds to wait. Campaign send throughput is governed separately by `daily_send_limit` and line pacing, not by the API rate limit.

## Related

* [Send a single message](/api-reference/send-message) for one off sends outside a campaign
* [Contacts](/api-reference/contacts) for tags, opt-outs, and timezones
* [Webhooks](/api-reference/webhooks) for `message.delivered`, `message.received`, and `campaign.completed` events
