> ## 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.

# Key concepts

> The BlueReacher object model for integrators: Lines, Contacts, Messages, Campaigns, and Webhooks, how they relate, and how the iMessage to RCS to SMS channel cascade picks a route.

## The five objects

BlueReacher's API has five objects. Learn these and the rest of the reference reads quickly.

| Object   | What it is                                     | Owns                            |
| -------- | ---------------------------------------------- | ------------------------------- |
| Line     | A dedicated iMessage line, managed for you     | The `from` number on every send |
| Contact  | A person you message, plus their opt-out state | The `to` number and consent     |
| Message  | One send with a lifecycle                      | Content, channel, status        |
| Campaign | A multi-step sequence that generates messages  | Steps, timing, audience         |
| Webhook  | How platform events reach your server          | Event delivery                  |

They nest in one direction. A Campaign creates Messages. Every Message is sent from a Line to a Contact. Webhooks report what happened to those Messages and to the Contact's consent state.

## Lines

A Line is a dedicated iMessage line assigned to your account and managed for you. You do not provision or maintain anything. Each Line has a phone number in E.164 format, which is the value you pass as `from`.

| Field        | Type   | Notes                                         |
| ------------ | ------ | --------------------------------------------- |
| `id`         | string | Prefixed `line_`, used as `line_id` elsewhere |
| `from`       | string | E.164 number, for example `+14155550123`      |
| `status`     | string | `warming`, `active`, or `paused`              |
| `created_at` | string | ISO 8601 timestamp                            |

New Lines start in `warming` and move to `active` once their sending volume ramps. Only `active` Lines accept full campaign volume. Because these are iMessage lines, there is no A2P registration required to start sending.

Fetch your Lines:

```bash theme={null}
curl https://api.bluereacher.com/functions/v1/lines \
  -H "Authorization: Bearer bluereacher_your_api_key"
```

## Contacts

A Contact is one person, keyed by phone number. The platform creates a Contact automatically the first time you message a new number, so you can send without a separate create call.

| Field         | Type   | Notes                       |
| ------------- | ------ | --------------------------- |
| `id`          | string | Prefixed `cnt_`             |
| `to`          | string | E.164 number                |
| `status`      | string | `active` or `opted_out`     |
| `external_id` | string | Your own CRM or database ID |
| `created_at`  | string | ISO 8601 timestamp          |

Opt-out is enforced at the platform level. When a Contact replies with a stop keyword, their `status` flips to `opted_out`, a `contact.opted_out` webhook fires, and every later send to that number returns `403` without being delivered. You do not have to filter your own lists, though mirroring `status` into your CRM keeps your reporting honest.

Set `external_id` on the Contact so webhook payloads carry your ID back to you and you can match events without a lookup table.

## Messages

A Message is a single send. Create one by posting to `/messages`.

```bash theme={null}
curl -X POST https://api.bluereacher.com/functions/v1/messages \
  -H "Authorization: Bearer bluereacher_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "+14155550123",
    "to": "+14155559876",
    "content": "Hi Dana, saw you are hiring SDRs. Worth a quick look?",
    "external_id": "crm-lead-4821"
  }'
```

```json theme={null}
{
  "id": "msg_01J9Y4T2M8QK",
  "line_id": "line_01J8Z2A5PQ",
  "contact_id": "cnt_01J8Z3B7RS",
  "from": "+14155550123",
  "to": "+14155559876",
  "content": "Hi Dana, saw you are hiring SDRs. Worth a quick look?",
  "channel": "imessage",
  "status": "queued",
  "media_url": null,
  "external_id": "crm-lead-4821",
  "created_at": "2026-07-24T16:02:11Z"
}
```

The response resolves `from` to a `line_id` and `to` to a `contact_id`. Those two IDs are the join keys for every report you build.

Status moves through a fixed lifecycle:

| Status      | Meaning                                                                           |
| ----------- | --------------------------------------------------------------------------------- |
| `queued`    | Accepted and waiting to send                                                      |
| `sent`      | Handed to the channel                                                             |
| `delivered` | Confirmed on the recipient's device                                               |
| `read`      | Read receipt returned, iMessage only, and only when the recipient has receipts on |
| `failed`    | Terminal, with a reason on the message record                                     |

`queued` and `sent` are transient. Do not poll for the rest. Let webhooks tell you.

## Campaigns

A Campaign is a sequence of steps sent to a list of Contacts over time. Each step has content and a delay. When a step fires for a Contact, the platform creates a Message with the campaign attached, so campaign traffic and one-off sends share the same object and the same webhook events.

Two rules matter for integrators. A reply from a Contact stops their remaining steps by default, and an opt-out stops them everywhere, across every Campaign in the account.

## Webhooks

Webhooks are how state changes reach you. Register one HTTPS endpoint per environment and read the `event` field to branch.

| Event               | Fires when                      |
| ------------------- | ------------------------------- |
| `message.sent`      | A message leaves the platform   |
| `message.delivered` | Delivery is confirmed           |
| `message.read`      | A read receipt comes back       |
| `message.failed`    | Delivery fails on every channel |
| `message.received`  | A Contact replies               |
| `contact.opted_out` | A Contact opts out              |

```json theme={null}
{
  "event": "message.delivered",
  "created_at": "2026-07-24T16:02:14Z",
  "data": {
    "id": "msg_01J9Y4T2M8QK",
    "line_id": "line_01J8Z2A5PQ",
    "contact_id": "cnt_01J8Z3B7RS",
    "channel": "imessage",
    "status": "delivered",
    "external_id": "crm-lead-4821"
  }
}
```

Return `200` fast and process asynchronously. Retries use exponential backoff, and events can arrive more than once, so key your handler on `data.id` plus `event` and make it idempotent.

## The channel cascade

Every Message tries iMessage first, then RCS, then SMS. The `channel` field on the response tells you which one carried it.

1. **iMessage.** Blue bubbles, typing indicators, read receipts, full-quality media.
2. **RCS.** Rich formatting and delivery receipts when the recipient's device and carrier support it.
3. **SMS.** Universal fallback. Media downgrades to MMS.

The cascade runs at send time, per message, and does not restart once a channel accepts the message.

## What determines routing

Four inputs decide the channel:

* **Recipient capability.** Whether the destination number is reachable on iMessage, then on RCS. This is checked at send time, not cached from an old send.
* **Your pin.** Pass `channel` in the request to force one channel and turn the cascade off. A pinned send that cannot be delivered on that channel fails instead of falling back.
* **Content.** Media in `media_url` that a lower channel cannot carry at full quality is noted on the message record so you can see the downgrade.
* **Line capability.** A Line configured for iMessage only will fail rather than fall back to SMS.

Pinning is useful when you need predictable cost or a specific look:

```javascript theme={null}
const res = await fetch("https://api.bluereacher.com/functions/v1/messages", {
  method: "POST",
  headers: {
    "Authorization": "Bearer bluereacher_your_api_key",
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    from: "+14155550123",
    to: "+14155559876",
    content: "Your parking pass is attached.",
    media_url: "https://cdn.example.com/pass.png",
    channel: "sms"
  })
});

const message = await res.json();
console.log(message.channel, message.status);
```

Reading the channel back on delivery, in Python:

```python theme={null}
def handle_webhook(payload):
    if payload["event"] != "message.delivered":
        return

    data = payload["data"]
    record_delivery(
        crm_id=data.get("external_id"),
        channel=data["channel"],
        message_id=data["id"],
    )
```

## Next steps

* Send your first message with the [quickstart](/guides/quickstart).
* Read the [Messages API reference](/api-reference/messages) for the full field list and error codes.
* Set up [webhooks](/api-reference/webhooks) before you run volume, so you capture replies and opt-outs from day one.
