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

# Errors

Most Reflecto errors return JSON with a stable `error.code` you can switch on
in client code; authentication failures (`401`) use a flat shape instead — see
the [auth shape](#auth-failure-shape) below. The human-readable `message` may
evolve; the codes will not.

## Error shape

Most endpoints return:

```json theme={null}
{
  "error": {
    "code": "payload_too_large",
    "message": "Payload exceeds 2048 byte limit",
    "details": { "size": 3104, "max": 2048 }
  }
}
```

`details` is populated on `payload_too_large` (413) as `{ size, max }` (the
serialized envelope size), and on the byte-length 400 errors
(`message_too_long`, `invalid_title`, `invalid_url`, `invalid_url_title`,
`invalid_action`) as `{ bytes, max }` — the actual UTF-8 byte count of the
offending field and the cap it exceeded. URL errors that fail for a non-length
reason (malformed URL, non-`http(s)` scheme) carry no `details`.

### Message-text changes from earlier API revisions

The error `code` values are stable wire contracts and have not changed.
The human-readable `error.message` strings, however, were rewritten when
validation moved to Zod schemas. Clients that match on `.code` are
unaffected; clients that display `.message` verbatim will see new wording:

| `.code`           | When                  | Previous `.message`                                             | Current `.message`                                   |
| ----------------- | --------------------- | --------------------------------------------------------------- | ---------------------------------------------------- |
| `invalid_action`  | invalid action URL    | `must be a valid URL`                                           | `is not a valid URL`                                 |
| `invalid_title`   | empty title           | `title must be 1–100 bytes` (shared with the over-limit branch) | `title must not be empty` (now distinct)             |
| `invalid_title`   | over-limit title      | `title must be 1–100 bytes` (shared with the empty branch)      | `title must be ≤ 100 bytes` (now distinct)           |
| `invalid_message` | `message` key omitted | `message is required`                                           | `Invalid input: expected string, received undefined` |
| `invalid_action`  | action `url` missing  | `each action requires label + url`                              | Schema default for the missing required field        |

### Auth failure shape

Auth failures use a flat shape so token problems don't get masked behind
nested structure. The full shape — both fields — is shown below:

```json theme={null}
{
  "error": "missing_token",
  "message": "Authorization: Bearer rfk_live_… required"
}
```

`message` is optional. The `invalid_token` variant omits it (the code alone
is actionable):

```json theme={null}
{
  "error": "invalid_token"
}
```

## Status codes

| Status | When                                                                     |
| ------ | ------------------------------------------------------------------------ |
| 200    | Accepted and enqueued. Check `warnings` in the body for non-fatal notes. |
| 400    | Body malformed or a field failed validation.                             |
| 401    | Missing, invalid, or revoked bearer token.                               |
| 403    | Token exists but lacks scope (e.g. priority above `priority_cap`).       |
| 413    | Serialized envelope > 2048 bytes.                                        |
| 429    | Rate limit hit on one of the layers; `Retry-After` is set.               |
| 5xx    | Server error. Retry with exponential backoff.                            |

## Codes

### Validation (`400`)

| Code                | Meaning                                                  |
| ------------------- | -------------------------------------------------------- |
| `invalid_body`      | Body wasn't JSON, form-encoded, or text.                 |
| `invalid_message`   | `message` missing or not a string.                       |
| `message_too_long`  | `message` exceeds 1500 bytes (UTF-8).                    |
| `invalid_title`     | `title` is 0 bytes or > 100 bytes (UTF-8).               |
| `invalid_priority`  | Not one of `min`, `low`, `default`, `high`, `urgent`.    |
| `invalid_url`       | Not http(s), invalid URL, or > 512 bytes.                |
| `invalid_url_title` | `url_title` > 32 bytes.                                  |
| `invalid_action`    | Missing `label`/`url`, oversized, or non-http(s) scheme. |
| `invalid_device`    | `device` parameter > 256 chars.                          |

### Auth (`401`)

| Code            | Meaning                                    |
| --------------- | ------------------------------------------ |
| `missing_token` | No `Authorization: Bearer …` header sent.  |
| `invalid_token` | Token format invalid, unknown, or revoked. |

### Scope (`403`)

| Code              | Meaning                                                   |
| ----------------- | --------------------------------------------------------- |
| `priority_capped` | Token's `priority_cap` is below the requested `priority`. |

### Size (`413`)

| Code                | Meaning                           |
| ------------------- | --------------------------------- |
| `payload_too_large` | Serialized envelope > 2048 bytes. |

### Rate limit (`429`)

| Code                  | Meaning                                                                         |
| --------------------- | ------------------------------------------------------------------------------- |
| `rate_limit_exceeded` | One of the rate-limit layers tripped. See `X-RateLimit-Resource` for which one. |

### Server errors (`5xx`)

| Code             | Meaning                                                                                                                           |
| ---------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `internal_error` | Catch-all for unhandled exceptions on the server (Redis unavailable, FCM dispatch failure, etc.). Retry with exponential backoff. |

## Warnings vs errors

Some conditions are surfaced as **warnings** on a `200` response rather than
errors, to keep long-running automations alive:

* `unknown device label: '…'` — one of the names in `device` didn't match any
  paired device.
* `device fallback: all entries unknown, delivering to every paired device` —
  every entry was unknown; the send fell back to broadcast.
* `tags truncated to first 5 (got 7)` — excess tags dropped.
* `tag #N truncated to 32 bytes` — a tag exceeded the per-tag byte cap.
* `actions truncated to first 3 (got 5)` — excess action buttons dropped.
* `no paired devices for this token's owner` — the request succeeded but
  produced no enqueues. Useful for cron jobs run during transient unpairing.
* `token scope '…' dropped out-of-scope device(s): …` — caller's `device`
  parameter referenced devices outside the token's `scopeDevices`.
