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

# Retrieve and List Messages

> Fetch a single message, poll its delivery status, and list messages with filters and cursor pagination, plus the full message lifecycle from queued to delivered, read, or failed.

Every message BlueReacher sends or receives is stored as a message record you can read back by ID or list with filters. This page covers three read endpoints and the lifecycle those records move through.

| Endpoint                  | Use it for                                                                                                         |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `GET /get-message`        | The full record for one message, including content, media, and channel history.                                    |
| `GET /get-message-status` | A small, fast payload with just the delivery state and timestamps. Built for polling.                              |
| `GET /list-messages`      | Paginated search across your workspace with filters for status, channel, direction, contact, line, and date range. |

All three are read-only. They never change a message and never trigger a send.

## Base URL and authentication

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

Send your API key as a bearer token on every request.

```
Authorization: Bearer bluereacher_your_api_key
Content-Type: application/json
```

A key only ever reads messages belonging to its own workspace. Requesting a message ID from another workspace returns `404`, not `403`, so IDs cannot be probed across accounts.

## The message object

Every endpoint on this page returns the same object shape, either whole or as a documented subset.

| Field               | Type             | Description                                                                                                                                |
| ------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `id`                | string           | Unique message ID, prefixed `msg_`. Stable forever.                                                                                        |
| `external_id`       | string or null   | Your own reference, set when you sent the message. Unique per workspace. `null` for inbound messages.                                      |
| `direction`         | string           | `outbound` for messages you sent, `inbound` for replies you received.                                                                      |
| `status`            | string           | Current lifecycle state. One of `queued`, `sending`, `sent`, `delivered`, `read`, `failed`.                                                |
| `from`              | string           | E.164 number the message came from. Your dedicated line on outbound, the contact on inbound.                                               |
| `to`                | string           | E.164 number the message went to.                                                                                                          |
| `content`           | string           | Text body. Empty string when the message is media only.                                                                                    |
| `media_url`         | array of strings | Attachment URLs in the order they were sent. Empty array when there are none. Links are signed and expire 7 days after they are generated. |
| `channel`           | string           | The channel the message actually went out on: `imessage`, `rcs`, or `sms`.                                                                 |
| `requested_channel` | string           | The channel you asked for at send time. Differs from `channel` when a fallback happened.                                                   |
| `fallback`          | object or null   | Details of a channel downgrade. `null` when the message went out on the channel you requested. See [Channel fallback](#channel-fallback).  |
| `line_id`           | string           | The dedicated iMessage line that carried the message, prefixed `line_`. Lines are managed for you.                                         |
| `contact_id`        | string           | The contact record for the other party, prefixed `cont_`.                                                                                  |
| `error`             | object or null   | Failure detail. Present only when `status` is `failed`, otherwise `null`.                                                                  |
| `created_at`        | string           | ISO 8601 UTC timestamp for when the message was accepted and queued.                                                                       |
| `sent_at`           | string or null   | When the message left the line.                                                                                                            |
| `delivered_at`      | string or null   | When the recipient device confirmed receipt.                                                                                               |
| `read_at`           | string or null   | When the recipient opened the message.                                                                                                     |
| `failed_at`         | string or null   | When the message entered `failed`.                                                                                                         |
| `updated_at`        | string           | Last time any field on the record changed.                                                                                                 |

Message records are retained for 24 months from `created_at`, then deleted. Requests for a deleted message return `404`.

## Message lifecycle

A message moves forward through the states below. It never moves backward. `read` and `failed` are terminal.

| Status      | Meaning                                                                                                                                     | Terminal |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------- | -------- |
| `queued`    | BlueReacher accepted the message and is holding it for send. Messages sit here while rate pacing, scheduling windows, or line warmup apply. | No       |
| `sending`   | The message has been handed to the line and is going out right now. Typically lasts under 5 seconds.                                        | No       |
| `sent`      | The message left your line successfully. Nothing has confirmed the recipient received it yet.                                               | No       |
| `delivered` | The recipient's device confirmed receipt.                                                                                                   | No       |
| `read`      | The recipient opened the message.                                                                                                           | Yes      |
| `failed`    | The message could not be delivered on any channel. Check the `error` object for the reason.                                                 | Yes      |

### Which states each channel reaches

Not every channel reports every signal. Build your logic around what the channel can actually tell you.

| Channel    | Reaches `delivered`                                                        | Reaches `read`                                                                                                                                         |
| ---------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `imessage` | Yes, on every message                                                      | Yes, when the recipient has read receipts turned on. Most people do not, so treat a missing `read` as no information rather than as a negative signal. |
| `rcs`      | Yes, on every message                                                      | Yes, when the recipient's device reports read receipts.                                                                                                |
| `sms`      | Only when the carrier returns a delivery receipt. Many US carriers do not. | Never.                                                                                                                                                 |

Because of this, `sent` is a normal resting state for SMS and is not a problem. Do not treat `sent` as a failure or retry against it.

### Timing you can plan around

| Transition            | Typical                            | Timeout behavior                                                                                                                 |
| --------------------- | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `queued` to `sending` | Under 30 seconds on default pacing | A queued message that cannot send within 24 hours moves to `failed` with error code `queue_expired`.                             |
| `sending` to `sent`   | 1 to 5 seconds                     | No confirmation within 90 seconds moves the message to `failed` with error code `send_timeout`.                                  |
| `sent` to `delivered` | 1 to 20 seconds on iMessage        | No delivery confirmation within 60 seconds triggers channel fallback if fallback is enabled, otherwise the message stays `sent`. |
| `delivered` to `read` | Unbounded                          | Never times out. A message can flip to `read` days later.                                                                        |

### Inbound messages

Inbound messages are created with `status` set to `delivered` and stay there. They have no `queued` or `sending` phase, `read_at` is always `null`, and `fallback` is always `null`. Filter on `direction=inbound` to read only replies.

## Channel fallback

BlueReacher sends on the best channel available for each number. When the requested channel cannot carry the message, the message is downgraded and sent again on the next channel down, in this order:

```
imessage  ->  rcs  ->  sms
```

A downgrade is not a separate status. The message keeps one `id` and one lifecycle, then continues through `sending`, `sent`, and `delivered` on the new channel. What changes is that `channel` no longer matches `requested_channel`, and the `fallback` object gets filled in.

### The fallback object

| Field          | Type   | Description                                                           |
| -------------- | ------ | --------------------------------------------------------------------- |
| `from_channel` | string | The channel that could not carry the message.                         |
| `to_channel`   | string | The channel that carried it instead. Matches the top-level `channel`. |
| `reason`       | string | Why the downgrade happened. See the table below.                      |
| `occurred_at`  | string | ISO 8601 UTC timestamp of the downgrade.                              |

### Fallback reasons

| Reason                  | What happened                                                                            |
| ----------------------- | ---------------------------------------------------------------------------------------- |
| `not_imessage_capable`  | The number is not registered for iMessage. Detected before send, so no time is lost.     |
| `imessage_timeout`      | The message went out on iMessage but no delivery confirmation arrived within 60 seconds. |
| `rcs_unavailable`       | The number does not accept RCS, or the recipient's carrier does not support it.          |
| `media_unsupported`     | The attachment type or size is not accepted on the requested channel.                    |
| `line_channel_disabled` | The line that sent the message does not have the requested channel turned on.            |

A fallback message is billed once, at the rate of the channel it finally went out on.

### Detecting a fallback

Two equivalent checks:

```javascript theme={null}
const downgraded = message.fallback !== null;
// or
const downgraded = message.channel !== message.requested_channel;
```

To pull every downgraded message for a period, list on the channel you did not request. For example, `GET /list-messages?requested_channel=imessage&channel=sms` returns iMessage sends that ended up as SMS.

## Failure reasons

When `status` is `failed`, the `error` object explains why.

| Field       | Type    | Description                                                                                |
| ----------- | ------- | ------------------------------------------------------------------------------------------ |
| `code`      | string  | Machine-readable reason. See the table below.                                              |
| `message`   | string  | Human-readable explanation. Wording can change, so branch on `code`, never on this string. |
| `retryable` | boolean | `true` when re-sending the same content to the same number has a real chance of working.   |

| Error code             | Meaning                                                               | `retryable` |
| ---------------------- | --------------------------------------------------------------------- | ----------- |
| `invalid_number`       | The destination is not a valid, reachable mobile number.              | false       |
| `no_channel_available` | Every channel in the fallback chain was ruled out.                    | false       |
| `recipient_opted_out`  | The contact replied STOP or was marked opted out. Sending is blocked. | false       |
| `carrier_rejected`     | The carrier refused the message.                                      | false       |
| `content_blocked`      | The content was blocked by a filter before send.                      | false       |
| `media_rejected`       | The attachment could not be sent on any available channel.            | false       |
| `send_timeout`         | The line did not confirm the send within 90 seconds.                  | true        |
| `queue_expired`        | The message sat in `queued` for 24 hours without going out.           | true        |
| `line_unavailable`     | The assigned line could not send at that moment.                      | true        |

Retry `retryable: true` failures with a fresh send and a new `external_id`. Do not retry anything else. Re-sending to `recipient_opted_out` or `invalid_number` will fail the same way every time.

## Retrieve a message

Returns the complete record for a single message, including content, media, channel history, and every lifecycle timestamp. Use this when you need the message body or fallback detail. Use `/get-message-status` instead when you only need to know where it stands.

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

Authentication: `Authorization: Bearer bluereacher_your_api_key`. The message must belong to your workspace.

### Request fields

Passed as query string parameters.

| Parameter     | Type   | Required                           | Description                                                                       |
| ------------- | ------ | ---------------------------------- | --------------------------------------------------------------------------------- |
| `id`          | string | Yes, unless `external_id` is given | The message ID, for example `msg_01J9K3W7XQ8ZP4M2VYT6DN5RAC`.                     |
| `external_id` | string | Yes, unless `id` is given          | Your own reference from send time. Resolves to exactly one message per workspace. |

Send one or the other. Sending both returns `400` with code `conflicting_identifiers`.

### Response fields

Returns the full [message object](#the-message-object) at the top level of the response body.

| Field               | Type             | Description                               |
| ------------------- | ---------------- | ----------------------------------------- |
| `id`                | string           | Message ID.                               |
| `external_id`       | string or null   | Your reference.                           |
| `direction`         | string           | `outbound` or `inbound`.                  |
| `status`            | string           | Current lifecycle state.                  |
| `from`              | string           | Sending number in E.164.                  |
| `to`                | string           | Destination number in E.164.              |
| `content`           | string           | Text body.                                |
| `media_url`         | array of strings | Signed attachment URLs.                   |
| `channel`           | string           | Channel actually used.                    |
| `requested_channel` | string           | Channel originally requested.             |
| `fallback`          | object or null   | Downgrade detail.                         |
| `line_id`           | string           | Dedicated line used.                      |
| `contact_id`        | string           | Contact on the other side.                |
| `error`             | object or null   | Failure detail when `status` is `failed`. |
| `created_at`        | string           | Queued at.                                |
| `sent_at`           | string or null   | Sent at.                                  |
| `delivered_at`      | string or null   | Delivered at.                             |
| `read_at`           | string or null   | Read at.                                  |
| `failed_at`         | string or null   | Failed at.                                |
| `updated_at`        | string           | Last change to the record.                |

### Example request

```bash theme={null}
curl -X GET "https://api.bluereacher.com/functions/v1/get-message?id=msg_01J9K3W7XQ8ZP4M2VYT6DN5RAC" \
  -H "Authorization: Bearer bluereacher_your_api_key" \
  -H "Content-Type: application/json"
```

### Example response

`200 OK`

```json theme={null}
{
  "id": "msg_01J9K3W7XQ8ZP4M2VYT6DN5RAC",
  "external_id": "campaign-4471-lead-8890",
  "direction": "outbound",
  "status": "read",
  "from": "+13105550142",
  "to": "+14155550188",
  "content": "Hey Dana, saw you just opened the Austin office. Worth a quick chat about coverage there?",
  "media_url": [],
  "channel": "imessage",
  "requested_channel": "imessage",
  "fallback": null,
  "line_id": "line_01J8ARQ4XN7C2VDKM6PB3T9WEZ",
  "contact_id": "cont_01J8B2M9YK5FD1XQZ7VTHN4RGP",
  "error": null,
  "created_at": "2026-07-24T16:41:09.482Z",
  "sent_at": "2026-07-24T16:41:11.907Z",
  "delivered_at": "2026-07-24T16:41:14.220Z",
  "read_at": "2026-07-24T17:02:56.311Z",
  "failed_at": null,
  "updated_at": "2026-07-24T17:02:56.311Z"
}
```

A message that fell back to SMS and then failed looks like this.

```json theme={null}
{
  "id": "msg_01J9K4B2ZR6HT8NW3QDXV5MFCY",
  "external_id": "campaign-4471-lead-8891",
  "direction": "outbound",
  "status": "failed",
  "from": "+13105550142",
  "to": "+12065550119",
  "content": "Quick question about your Q3 rollout.",
  "media_url": [],
  "channel": "sms",
  "requested_channel": "imessage",
  "fallback": {
    "from_channel": "imessage",
    "to_channel": "sms",
    "reason": "not_imessage_capable",
    "occurred_at": "2026-07-24T16:44:02.115Z"
  },
  "line_id": "line_01J8ARQ4XN7C2VDKM6PB3T9WEZ",
  "contact_id": "cont_01J8B5T7WQ2NJ4RXH9VKPD3MZA",
  "error": {
    "code": "carrier_rejected",
    "message": "The destination carrier refused this message.",
    "retryable": false
  },
  "created_at": "2026-07-24T16:44:00.664Z",
  "sent_at": "2026-07-24T16:44:03.881Z",
  "delivered_at": null,
  "read_at": null,
  "failed_at": "2026-07-24T16:44:09.402Z",
  "updated_at": "2026-07-24T16:44:09.402Z"
}
```

### Errors

| Status | Code                      | Cause                                                                                     |
| ------ | ------------------------- | ----------------------------------------------------------------------------------------- |
| 400    | `missing_identifier`      | Neither `id` nor `external_id` was supplied.                                              |
| 400    | `conflicting_identifiers` | Both `id` and `external_id` were supplied.                                                |
| 400    | `invalid_identifier`      | The ID is malformed.                                                                      |
| 401    | `invalid_api_key`         | The key is missing, revoked, or wrong.                                                    |
| 404    | `message_not_found`       | No message with that ID in your workspace, or it aged past the 24 month retention window. |
| 429    | `rate_limited`            | Read rate limit exceeded.                                                                 |
| 500    | `internal_error`          | Something broke on our side. Retry with backoff.                                          |

## Get message status

Returns delivery state and timestamps only, without content, media, or contact detail. The payload is small and the endpoint is cheap, so use it for polling loops and dashboards.

```
GET https://api.bluereacher.com/functions/v1/get-message-status
```

Authentication: `Authorization: Bearer bluereacher_your_api_key`.

### Request fields

| Parameter     | Type   | Required                           | Description                        |
| ------------- | ------ | ---------------------------------- | ---------------------------------- |
| `id`          | string | Yes, unless `external_id` is given | The message ID.                    |
| `external_id` | string | Yes, unless `id` is given          | Your own reference from send time. |

### Response fields

| Field               | Type           | Description                                                        |
| ------------------- | -------------- | ------------------------------------------------------------------ |
| `id`                | string         | Message ID.                                                        |
| `external_id`       | string or null | Your reference.                                                    |
| `status`            | string         | One of `queued`, `sending`, `sent`, `delivered`, `read`, `failed`. |
| `channel`           | string         | Channel the message went out on.                                   |
| `requested_channel` | string         | Channel originally requested.                                      |
| `fallback`          | object or null | Downgrade detail, `null` when no downgrade happened.               |
| `error`             | object or null | Failure detail when `status` is `failed`.                          |
| `created_at`        | string         | Queued at.                                                         |
| `sent_at`           | string or null | Sent at.                                                           |
| `delivered_at`      | string or null | Delivered at.                                                      |
| `read_at`           | string or null | Read at.                                                           |
| `failed_at`         | string or null | Failed at.                                                         |
| `updated_at`        | string         | Last change to the record.                                         |

### Example request

```bash theme={null}
curl -X GET "https://api.bluereacher.com/functions/v1/get-message-status?id=msg_01J9K3W7XQ8ZP4M2VYT6DN5RAC" \
  -H "Authorization: Bearer bluereacher_your_api_key" \
  -H "Content-Type: application/json"
```

### Example response

`200 OK`

```json theme={null}
{
  "id": "msg_01J9K3W7XQ8ZP4M2VYT6DN5RAC",
  "external_id": "campaign-4471-lead-8890",
  "status": "delivered",
  "channel": "imessage",
  "requested_channel": "imessage",
  "fallback": null,
  "error": null,
  "created_at": "2026-07-24T16:41:09.482Z",
  "sent_at": "2026-07-24T16:41:11.907Z",
  "delivered_at": "2026-07-24T16:41:14.220Z",
  "read_at": null,
  "failed_at": null,
  "updated_at": "2026-07-24T16:41:14.220Z"
}
```

### Polling guidance

Webhooks are the better pattern. They push each state change to you the moment it happens and cost you no requests. Poll only when you cannot receive webhooks.

If you do poll, poll no more than once every 5 seconds per message, and stop as soon as you hit a terminal state or a state you care about.

```javascript theme={null}
const TERMINAL = new Set(["read", "failed"]);

async function waitForDelivery(messageId, timeoutMs = 120000) {
  const deadline = Date.now() + timeoutMs;

  while (Date.now() < deadline) {
    const res = await fetch(
      `https://api.bluereacher.com/functions/v1/get-message-status?id=${messageId}`,
      {
        headers: {
          Authorization: "Bearer bluereacher_your_api_key",
          "Content-Type": "application/json"
        }
      }
    );

    if (!res.ok) throw new Error(`Status check failed: ${res.status}`);
    const msg = await res.json();

    if (TERMINAL.has(msg.status) || msg.status === "delivered") {
      if (msg.fallback) {
        console.log(`Downgraded ${msg.fallback.from_channel} to ${msg.fallback.to_channel}: ${msg.fallback.reason}`);
      }
      return msg;
    }

    await new Promise((r) => setTimeout(r, 5000));
  }

  throw new Error("Timed out waiting for a terminal status");
}
```

### Errors

Same set as `/get-message`.

| Status | Code                      | Cause                                        |
| ------ | ------------------------- | -------------------------------------------- |
| 400    | `missing_identifier`      | Neither `id` nor `external_id` was supplied. |
| 400    | `conflicting_identifiers` | Both were supplied.                          |
| 400    | `invalid_identifier`      | The ID is malformed.                         |
| 401    | `invalid_api_key`         | The key is missing, revoked, or wrong.       |
| 404    | `message_not_found`       | No such message in your workspace.           |
| 429    | `rate_limited`            | Read rate limit exceeded.                    |
| 500    | `internal_error`          | Retry with backoff.                          |

## List messages

Returns a paginated list of messages in your workspace, newest first by default. Filter by status, channel, direction, contact, line, and date range. Use it to reconcile a campaign, pull a conversation thread, or sweep for failures.

```
GET https://api.bluereacher.com/functions/v1/list-messages
```

Authentication: `Authorization: Bearer bluereacher_your_api_key`. Only messages in the key's workspace are returned.

### Request fields

All parameters are optional and go in the query string. Filters combine with AND.

| Parameter           | Type    | Default | Description                                                                                                                                                                      |
| ------------------- | ------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `status`            | string  | none    | Filter by lifecycle state. Accepts one value or a comma separated list, for example `status=sent,delivered`. Values: `queued`, `sending`, `sent`, `delivered`, `read`, `failed`. |
| `channel`           | string  | none    | Channel the message actually went out on. Comma separated list allowed. Values: `imessage`, `rcs`, `sms`.                                                                        |
| `requested_channel` | string  | none    | Channel originally requested. Pair with `channel` to isolate fallbacks.                                                                                                          |
| `direction`         | string  | none    | `outbound` or `inbound`. Omit to get both.                                                                                                                                       |
| `contact`           | string  | none    | Phone number in E.164. URL-encode the leading plus as `%2B`, so `+14155550188` becomes `%2B14155550188`. Matches messages to or from that number.                                |
| `contact_id`        | string  | none    | Contact record ID, prefixed `cont_`. More exact than `contact` when a contact has several numbers.                                                                               |
| `line_id`           | string  | none    | Only messages carried by this dedicated line.                                                                                                                                    |
| `external_id`       | string  | none    | Exact match on your own reference.                                                                                                                                               |
| `created_after`     | string  | none    | ISO 8601 UTC. Inclusive lower bound on `created_at`.                                                                                                                             |
| `created_before`    | string  | none    | ISO 8601 UTC. Exclusive upper bound on `created_at`.                                                                                                                             |
| `order`             | string  | `desc`  | Sort on `created_at`. `desc` for newest first, `asc` for oldest first.                                                                                                           |
| `limit`             | integer | `50`    | Records per page, 1 to 200.                                                                                                                                                      |
| `cursor`            | string  | none    | Opaque cursor from the previous page's `next_cursor`.                                                                                                                            |

Passing both `contact` and `contact_id` returns `400` with code `conflicting_filters`. The maximum span between `created_after` and `created_before` is 90 days; a wider range returns `400` with code `date_range_too_wide`.

### Response fields

| Field         | Type             | Description                                                                               |
| ------------- | ---------------- | ----------------------------------------------------------------------------------------- |
| `data`        | array of objects | Matching messages, each a full [message object](#the-message-object), sorted per `order`. |
| `has_more`    | boolean          | `true` when more records match beyond this page.                                          |
| `next_cursor` | string or null   | Pass back as `cursor` to fetch the next page. `null` when `has_more` is `false`.          |
| `limit`       | integer          | The page size that was applied.                                                           |

### Cursor pagination

Cursors are opaque, forward-only, and stable. Keep every filter identical between pages and change only `cursor`. Changing a filter mid-walk returns `400` with code `cursor_filter_mismatch`.

Stop when `has_more` is `false`. Do not stop on an empty `data` array alone, and do not build your own cursor strings. A cursor expires 24 hours after it is issued; an expired cursor returns `400` with code `cursor_expired`, and you restart the walk.

Records added during a walk with `order=desc` can be missed, since new messages land on page one behind you. For a complete sweep of a fixed window, set `created_after` and `created_before` and use `order=asc`.

### Example request

Every failed or fallback message for one line over a two day window.

```bash theme={null}
curl -X GET "https://api.bluereacher.com/functions/v1/list-messages?status=failed&line_id=line_01J8ARQ4XN7C2VDKM6PB3T9WEZ&created_after=2026-07-22T00:00:00Z&created_before=2026-07-24T00:00:00Z&order=asc&limit=2" \
  -H "Authorization: Bearer bluereacher_your_api_key" \
  -H "Content-Type: application/json"
```

### Example response

`200 OK`

```json theme={null}
{
  "data": [
    {
      "id": "msg_01J9G7PA3T6DKW2NXQV8YR4MHB",
      "external_id": "campaign-4470-lead-2210",
      "direction": "outbound",
      "status": "failed",
      "from": "+13105550142",
      "to": "+19735550164",
      "content": "Following up on the pricing sheet I sent over.",
      "media_url": [],
      "channel": "sms",
      "requested_channel": "imessage",
      "fallback": {
        "from_channel": "imessage",
        "to_channel": "sms",
        "reason": "imessage_timeout",
        "occurred_at": "2026-07-22T14:03:11.508Z"
      },
      "line_id": "line_01J8ARQ4XN7C2VDKM6PB3T9WEZ",
      "contact_id": "cont_01J8C1R6VN9PZ4KXW7TQDM2HFJ",
      "error": {
        "code": "invalid_number",
        "message": "The destination is not a reachable mobile number.",
        "retryable": false
      },
      "created_at": "2026-07-22T14:02:09.771Z",
      "sent_at": null,
      "delivered_at": null,
      "read_at": null,
      "failed_at": "2026-07-22T14:03:14.902Z",
      "updated_at": "2026-07-22T14:03:14.902Z"
    },
    {
      "id": "msg_01J9GQ2XM8W5RT7NKD4VPB6HZC",
      "external_id": "campaign-4470-lead-2287",
      "direction": "outbound",
      "status": "failed",
      "from": "+13105550142",
      "to": "+16175550133",
      "content": "Sharing the deck we talked about.",
      "media_url": [
        "https://media.bluereacher.com/f/01J9GQ32K7YD5NWXVR8QMT4PBC.pdf"
      ],
      "channel": "imessage",
      "requested_channel": "imessage",
      "fallback": null,
      "line_id": "line_01J8ARQ4XN7C2VDKM6PB3T9WEZ",
      "contact_id": "cont_01J8C4W9ZP2MRK6XVN7TQH5DFB",
      "error": {
        "code": "queue_expired",
        "message": "The message stayed queued for 24 hours without sending.",
        "retryable": true
      },
      "created_at": "2026-07-22T09:15:44.203Z",
      "sent_at": null,
      "delivered_at": null,
      "read_at": null,
      "failed_at": "2026-07-23T09:15:44.681Z",
      "updated_at": "2026-07-23T09:15:44.681Z"
    }
  ],
  "has_more": true,
  "next_cursor": "eyJjIjoiMjAyNi0wNy0yMlQwOToxNTo0NC4yMDNaIiwiaSI6Im1zZ18wMUo5R1EyWE04VzVSVDdOS0Q0VlBCNkhaQyJ9",
  "limit": 2
}
```

### Walking every page

```python theme={null}
import requests
from urllib.parse import urlencode

BASE = "https://api.bluereacher.com/functions/v1/list-messages"
HEADERS = {
    "Authorization": "Bearer bluereacher_your_api_key",
    "Content-Type": "application/json",
}

def list_all_messages(**filters):
    params = {"limit": 200, "order": "asc", **filters}
    cursor = None

    while True:
        query = dict(params)
        if cursor:
            query["cursor"] = cursor

        response = requests.get(f"{BASE}?{urlencode(query)}", headers=HEADERS, timeout=30)
        response.raise_for_status()
        page = response.json()

        for message in page["data"]:
            yield message

        if not page["has_more"]:
            return
        cursor = page["next_cursor"]

# Count how many iMessage sends were downgraded to SMS last week
downgraded = list_all_messages(
    requested_channel="imessage",
    channel="sms",
    created_after="2026-07-17T00:00:00Z",
    created_before="2026-07-24T00:00:00Z",
)
print(sum(1 for _ in downgraded))
```

### Errors

| Status | Code                     | Cause                                                                       |
| ------ | ------------------------ | --------------------------------------------------------------------------- |
| 400    | `invalid_filter_value`   | A filter got a value outside its allowed set, for example `status=pending`. |
| 400    | `conflicting_filters`    | Both `contact` and `contact_id` were supplied.                              |
| 400    | `invalid_date_format`    | `created_after` or `created_before` is not ISO 8601.                        |
| 400    | `date_range_too_wide`    | The window between the two dates is over 90 days.                           |
| 400    | `invalid_limit`          | `limit` is below 1 or above 200.                                            |
| 400    | `cursor_expired`         | The cursor is older than 24 hours. Restart the walk.                        |
| 400    | `cursor_filter_mismatch` | Filters changed between pages.                                              |
| 401    | `invalid_api_key`        | The key is missing, revoked, or wrong.                                      |
| 429    | `rate_limited`           | Read rate limit exceeded.                                                   |
| 500    | `internal_error`         | Retry with backoff.                                                         |

An empty result set is a success, not an error. You get `200` with `data: []`, `has_more: false`, and `next_cursor: null`.

## Error format

Every error on these endpoints returns the same JSON shape.

```json theme={null}
{
  "error": {
    "code": "message_not_found",
    "message": "No message with that ID exists in this workspace.",
    "status": 404
  }
}
```

Branch on `error.code`. The `message` string is written for humans and its wording can change without notice.

## Rate limits

Read endpoints share a limit of 600 requests per minute per API key. Every response carries the current state of your budget.

| Header                  | Meaning                                           |
| ----------------------- | ------------------------------------------------- |
| `X-RateLimit-Limit`     | Requests allowed in the window.                   |
| `X-RateLimit-Remaining` | Requests left in the current window.              |
| `X-RateLimit-Reset`     | Unix timestamp in seconds when the window resets. |

A `429` response includes a `Retry-After` header in seconds. Wait that long, then retry with exponential backoff. Prefer one `/list-messages` call with `limit=200` over 200 separate `/get-message` calls; it costs one request instead of two hundred.

## Related

* Send a message: `POST /send-message`
* Webhook events for every lifecycle change, so you can stop polling
* Manage lines: your dedicated iMessage lines are provisioned and managed for you, with no A2P registration required
