# Errors

> The one shape every refusal takes, how to handle an error by its code, which errors to retry, and every code the API answers with.

When the API refuses a request, it answers with an HTTP status of 400 or above and a body in one shape, whatever went wrong. The `code` in it is stable: write your program against the code, and show `message` and `hint` to people. Every code has a page of its own under `/docs/errors`, and every refusal links to that page in `doc_url`.

## The error body

The answer, `400`:

```json
{
  "error": {
    "code": "invalid_request",
    "message": "Some fields are missing or wrong.",
    "hint": "Check the fields named in the message and send it again.",
    "doc_url": "https://usebluebot.com/docs/errors/invalid_request",
    "details": [
      { "field": "to", "message": "Send `to` or `chat_id`." }
    ],
    "request_id": "req_1Hq2LxW9cVbN3mKpRtYs0a"
  }
}
```

| Field         | Always there           | What it is                                                                                                                                                   |
| ------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `code`        | Yes                    | What went wrong, in snake_case. It never changes for a given kind of refusal, so it's what your code should read.                                            |
| `message`     | Yes                    | A sentence for a person, such as "There’s no message with that id." The words may change; don't match on them. A message never contains the words of a text. |
| `hint`        | Yes                    | One sentence saying what to do next.                                                                                                                         |
| `doc_url`     | Yes                    | The page for this code: `https://usebluebot.com/docs/errors/<code>`.                                                                                         |
| `request_id`  | Yes                    | This request's id, the same as the `Request-Id` header. Quote it when you write to support.                                                                  |
| `retry_after` | When waiting will help | Seconds to wait before sending the request again. The `Retry-After` header says the same.                                                                    |
| `details`     | For `invalid_request`  | One entry per field that's wrong: `field` is a dotted path (`files.2`, `quiet_hours.start`) and `message` says what's wrong with it.                         |

The status always matches the code: `invalid_request` is always 400, `rate_limited` always 429. Every answer is `application/json`, refusals included.

## Handle errors by code

Branch on `error.code`, not on the status alone, since several codes share a status (`forbidden`, `insufficient_scope` and `account_held` are all 403, and `conflict`, `idempotency_conflict` and `limit_reached` are all 409).

```ts title="TypeScript"
const response = await fetch('https://usebluebot.com/api/v1/messages', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.BLUEBOT_API_KEY}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': `order-${order.id}`,
  },
  body: JSON.stringify({ to: order.phone, text: `Hi ${order.firstName}, your order is ready.` }),
});

if (!response.ok) {
  const { error } = await response.json();
  switch (error.code) {
    case 'invalid_request':
      // A field is wrong. Fix the data; sending it again won't help.
      console.warn('Not sent', error.details, error.request_id);
      break;
    case 'rate_limited':
    case 'internal':
    case 'upstream_failed':
    case 'unavailable':
      // Wait and send the same request with the same Idempotency-Key.
      await retryLater(error.retry_after ?? 2);
      break;
    default:
      throw new Error(`${error.code}: ${error.message} (${error.request_id})`);
  }
}
```

```python title="Python"
import os
import requests

response = requests.post(
    "https://usebluebot.com/api/v1/messages",
    headers={
        "Authorization": f"Bearer {os.environ['BLUEBOT_API_KEY']}",
        "Idempotency-Key": f"order-{order.id}",
    },
    json={"to": order.phone, "text": f"Hi {order.first_name}, your order is ready."},
)

if not response.ok:
    error = response.json()["error"]
    if error["code"] == "invalid_request":
        # A field is wrong. Fix the data; sending it again won't help.
        print("Not sent", error.get("details"), error["request_id"])
    elif error["code"] in ("rate_limited", "internal", "upstream_failed", "unavailable"):
        # Wait and send the same request with the same Idempotency-Key.
        retry_later(error.get("retry_after", 2))
    else:
        raise RuntimeError(f"{error['code']}: {error['message']} ({error['request_id']})")
```

The [SDKs](https://usebluebot.com/docs/sdks) turn a refusal into an error you can catch, with the same `code`, `message`, `hint` and `request_id`, and they retry the codes worth retrying for you.

## Which errors to retry

Every code falls in one of three groups, and each code's page says which.

| Retry?             | Codes                                                                                                                                                                                      | What to do                                                                                                                                                                                                                               |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Yes, after waiting | `rate_limited`, `internal`, `upstream_failed`, `unavailable`                                                                                                                               | Wait `retry_after` seconds when it's there; otherwise wait a second or two and double the wait on each try, up to a minute or so. Send the same request with the same `Idempotency-Key`, so a send that did go through isn't sent twice. |
| After fixing it    | `invalid_request`, `unauthorized`, `insufficient_scope`, `plan_lapsed`, `account_held`, `conflict`, `idempotency_conflict`, `limit_reached`, `payload_too_large`, `unsupported_media_type` | The same request gets the same answer until something changes: the data, the key, the plan, or the state of the thing you're acting on. Fix that first.                                                                                  |
| No                 | `forbidden`, `not_found`                                                                                                                                                                   | Repeating it gets the same answer. Change the request.                                                                                                                                                                                   |

A `5xx` answer to a POST with an `Idempotency-Key` lets the key go, so the retry runs as a fresh request. Any other answer is kept for 24 hours and returned to a retry with the same key, with `Idempotent-Replayed: true`. One `conflict` is worth a quick retry: "A request with this Idempotency-Key is still running" comes with `Retry-After: 1`, because the first request is still being answered. [Idempotency](https://usebluebot.com/docs/concepts/idempotency)

## What isn't an error

Some things that sound like failures come back as ordinary answers, because nothing about the request was wrong:

- **A held text.** A send that can't go yet (quiet hours, the Mac away, a daily limit, its turn in the pace) is answered `201` with the message `queued` and a `hold`. It goes on its own. [Holds](https://usebluebot.com/docs/concepts/holds)
- **A text to someone on your stop list.** It's answered `201` with the message already `canceled` and `cancel_reason: "opted_out"`, so your code learns the person stopped without a special case. [Stop words](https://usebluebot.com/docs/concepts/stop-words)
- **A text that didn't go.** Once BlueBot has accepted a text, what happens to it is its state, not an HTTP error: `failed` with a `failure.reason`, `expired`, or `unconfirmed`. You hear about each with a webhook. [Message states](https://usebluebot.com/docs/concepts/message-states)
- **Canceling a text that's already canceled.** It answers `200` with the message as it is.

## When something keeps failing

- Check [BlueBot's status](https://usebluebot.com/docs/api/device/get-status) with `GET /v1/status`, which needs no key. `"sending": "stopped"` means BlueBot paused sending for everyone during an incident; texts are held, not lost. `"database": "down"` with `"sending": "unknown"` means BlueBot can’t reach its database, so it can’t say whether sending is on, and Macs can’t pick up texts until it’s back.
- Check your Mac with `GET /v1/device`. Most "my texts aren't going" questions are a Mac that went to sleep, and that's a hold, not an error.
- Write to [support@usebluebot.com](mailto:support@usebluebot.com) with the `request_id`. BlueBot keeps the route, status and time of every request for 30 days, never the body, so the id is how a person finds yours.

## Every code

| Code                                                            | Status | What it means          | Retry?             |
| --------------------------------------------------------------- | ------ | ---------------------- | ------------------ |
| [`invalid_request`](https://usebluebot.com/docs/errors/invalid_request)               | 400    | Invalid request        | After fixing it    |
| [`unauthorized`](https://usebluebot.com/docs/errors/unauthorized)                     | 401    | Unauthorized           | After fixing it    |
| [`plan_lapsed`](https://usebluebot.com/docs/errors/plan_lapsed)                       | 402    | Plan lapsed            | After fixing it    |
| [`forbidden`](https://usebluebot.com/docs/errors/forbidden)                           | 403    | Forbidden              | No                 |
| [`insufficient_scope`](https://usebluebot.com/docs/errors/insufficient_scope)         | 403    | Insufficient scope     | After fixing it    |
| [`account_held`](https://usebluebot.com/docs/errors/account_held)                     | 403    | Account held           | After fixing it    |
| [`not_found`](https://usebluebot.com/docs/errors/not_found)                           | 404    | Not found              | No                 |
| [`conflict`](https://usebluebot.com/docs/errors/conflict)                             | 409    | Conflict               | After fixing it    |
| [`idempotency_conflict`](https://usebluebot.com/docs/errors/idempotency_conflict)     | 409    | Idempotency key reused | After fixing it    |
| [`limit_reached`](https://usebluebot.com/docs/errors/limit_reached)                   | 409    | Limit reached          | After fixing it    |
| [`payload_too_large`](https://usebluebot.com/docs/errors/payload_too_large)           | 413    | Payload too large      | After fixing it    |
| [`unsupported_media_type`](https://usebluebot.com/docs/errors/unsupported_media_type) | 415    | Unsupported media type | After fixing it    |
| [`rate_limited`](https://usebluebot.com/docs/errors/rate_limited)                     | 429    | Rate limited           | Yes, after waiting |
| [`internal`](https://usebluebot.com/docs/errors/internal)                             | 500    | Internal error         | Yes, after waiting |
| [`upstream_failed`](https://usebluebot.com/docs/errors/upstream_failed)               | 502    | Upstream failed        | Yes, after waiting |
| [`unavailable`](https://usebluebot.com/docs/errors/unavailable)                       | 503    | Unavailable            | Yes, after waiting |
