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

# Errors and status codes

> How BlueReacher API errors are structured, what each HTTP status code and error code means, and when to retry.

Every BlueReacher API failure returns the same JSON shape with a matching HTTP status code. Read the status code to decide whether to retry. Read the `error.code` to decide what to fix.

## The error envelope

Failed requests return a single top-level `error` object. The body never contains partial success data.

```json theme={null}
{
  "error": {
    "code": "contact_opted_out",
    "message": "Contact +15551234567 opted out on 2026-07-19 and cannot be messaged from this line.",
    "doc_url": "https://docs.bluereacher.com/guides/errors#contact_opted_out",
    "trace_id": "trc_01K3QF7M2A9YB4X6ZD8NPV"
  }
}
```

| Field      | Type   | Description                                                                               |
| ---------- | ------ | ----------------------------------------------------------------------------------------- |
| `code`     | string | Stable machine-readable identifier. Branch your code on this, never on `message`.         |
| `message`  | string | Human-readable explanation with the offending value. Wording can change between releases. |
| `doc_url`  | string | Link to the section of this page that explains the code.                                  |
| `trace_id` | string | Unique identifier for this request. Log it. Support needs it.                             |

## HTTP status codes

| Status                     | Meaning              | Typical cause                                                                           |
| -------------------------- | -------------------- | --------------------------------------------------------------------------------------- |
| `400`                      | Bad request          | Malformed JSON, missing required field, unknown `channel` value.                        |
| `401`                      | Unauthorized         | Missing, expired, or malformed API key in the `Authorization` header.                   |
| `403`                      | Forbidden            | The key is valid but the action is not allowed, such as messaging an opted-out contact. |
| `404`                      | Not found            | The `line_id`, `contact_id`, or message id does not exist in your workspace.            |
| `409`                      | Conflict             | The request collides with existing state, such as a repeated `external_id`.             |
| `422`                      | Unprocessable entity | Syntax is correct but the values fail a business rule, like oversized media.            |
| `429`                      | Rate limited         | You exceeded a request rate limit or a sending capacity limit.                          |
| `500`, `502`, `503`, `504` | Server error         | A problem on our side. The request may or may not have been applied.                    |

## Error codes

| `code`                  | Status | What happened                                                                            | What to do                                                                                                                |
| ----------------------- | ------ | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `invalid_recipient`     | 400    | `to` is not a valid E.164 phone number or reachable address for the requested `channel`. | Normalize numbers to `+15551234567` before sending. Check reachability first if you route between iMessage, RCS, and SMS. |
| `line_unavailable`      | 409    | The `from` line is not able to send right now. It may be paused, warming, or reassigned. | Retry in 60 seconds, or send from another `line_id` in the same pool. Check line `status` before large batches.           |
| `contact_opted_out`     | 403    | The contact replied STOP or was suppressed in your workspace.                            | Do not retry. Remove the contact from the campaign. Suppression is permanent until the contact opts back in.              |
| `capacity_exceeded`     | 429    | The line or workspace hit its sending capacity for the current window.                   | Back off and retry after the `Retry-After` interval. Spread volume across more lines or schedule the send.                |
| `media_too_large`       | 422    | The file at `media_url` is above the 25 MB per-message limit or the URL did not resolve. | Compress or host a smaller file. Make sure the URL is publicly reachable and returns a `Content-Length`.                  |
| `duplicate_external_id` | 409    | A message with this `external_id` already exists in your workspace.                      | Treat this as success. The `message` field names the original message id, so fetch it instead of resending.               |

Other codes you will see: `invalid_api_key` (401), `insufficient_permissions` (403), `resource_not_found` (404), `validation_failed` (400), `rate_limited` (429), and `internal_error` (500).

## Retry guidance by class

| Class           | Codes                   | Retry?                                                                                           |
| --------------- | ----------------------- | ------------------------------------------------------------------------------------------------ |
| Client mistakes | 400, 401, 403, 404, 422 | No. The same request fails again. Fix the payload, the key, or the audience.                     |
| Conflicts       | 409                     | Depends. Never retry `duplicate_external_id`. Retry `line_unavailable` once after a short pause. |
| Throttling      | 429                     | Yes, after the delay in the `Retry-After` header.                                                |
| Server errors   | 500, 502, 503, 504      | Yes, with exponential backoff and jitter. Cap at five attempts.                                  |

Send an `external_id` on every message. It makes retries safe: a duplicate returns `409 duplicate_external_id` instead of sending the message twice.

Rate limit responses carry `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and `Retry-After` (seconds).

## Reproducing an error

Use `-i` to see the status line and rate limit headers alongside the body.

```bash theme={null}
curl -i -X POST https://api.bluereacher.com/functions/v1/messages \
  -H "Authorization: Bearer bluereacher_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "line_8f2a41c0",
    "to": "5551234567",
    "content": "Hey Dana, following up on the demo.",
    "channel": "imessage",
    "external_id": "crm-8842"
  }'
```

The unnormalized `to` value returns:

```json theme={null}
{
  "error": {
    "code": "invalid_recipient",
    "message": "to must be an E.164 phone number, for example +15551234567. Received: 5551234567.",
    "doc_url": "https://docs.bluereacher.com/guides/errors#invalid_recipient",
    "trace_id": "trc_01K3QG2B7C4DEF9HJ1MNPQ"
  }
}
```

## Handling errors in code

This helper retries only the classes that are worth retrying and raises with the `trace_id` attached.

```python theme={null}
import random
import time
import requests

BASE_URL = "https://api.bluereacher.com/functions/v1/"
RETRYABLE = {429, 500, 502, 503, 504}

def send_message(payload, api_key, max_attempts=5):
    headers = {
        "Authorization": f"Bearer {api_key}",
        "Content-Type": "application/json",
    }

    for attempt in range(max_attempts):
        r = requests.post(BASE_URL + "messages", json=payload, headers=headers)

        if r.status_code < 400:
            return r.json()

        err = r.json()["error"]

        if r.status_code not in RETRYABLE or attempt == max_attempts - 1:
            raise RuntimeError(
                f"{err['code']}: {err['message']} (trace_id={err['trace_id']})"
            )

        delay = float(r.headers.get("Retry-After", 2 ** attempt))
        time.sleep(delay + random.uniform(0, 0.5))

send_message(
    {
        "from": "line_8f2a41c0",
        "to": "+15551234567",
        "content": "Hey Dana, following up on the demo.",
        "channel": "imessage",
        "external_id": "crm-8842",
    },
    "bluereacher_your_api_key",
)
```

In JavaScript, log the whole envelope so the code and the trace stay together.

```javascript theme={null}
if (!response.ok) {
  const { error } = await response.json();
  console.error(`[bluereacher] ${error.code} ${error.trace_id}: ${error.message}`);
  console.error(`See ${error.doc_url}`);
}
```

## Using trace\_id with support

Every response carries a `trace_id`, on success in the `X-Trace-Id` header and on failure inside the error envelope. It identifies one request in our logs and stays valid for 30 days.

Store it with your own request logs. When you open a ticket at [support@bluereacher.com](mailto:support@bluereacher.com), include the `trace_id`, the `error.code`, and the approximate timestamp. That is enough to pull the full request path without asking you for your payload or your API key. Never send us your API key.
