# Errors

Every error has a stable code, a message and a hint written for the agent: what to do next.

REST answers with the HTTP status of the code and this body (`details` only when there are details):

```json
{ "error": { "code": "SCOPE_MISSING", "message": "This key lacks scope 'send'.", "hint": "…", "details": { "scope": "send" } } }
```

An MCP tool answers with `isError: true` and one text block, `CODE: message hint`. A missing or revoked key and a rate limit are refused before any tool runs: HTTP 401 or 429 with a JSON-RPC error whose message is the same text.

| Code | HTTP | Message | Hint |
|---|---|---|---|
| `UNAUTHENTICATED` | 401 | Missing or invalid API key. | Pass the project key as `Authorization: Bearer brk_live_…`. Run `npx brynth init` to get one. |
| `KEY_REVOKED` | 401 | This API key has been revoked. | This key was revoked. Ask the project owner for a new key. |
| `SCOPE_MISSING` | 403 | This key lacks a required scope. | Create a key with that scope (`POST /v1/keys`, needs `admin`). |
| `VALIDATION_FAILED` | 400 | The request is invalid. | Fix the listed fields and retry. |
| `NOT_FOUND` | 404 | Not found. |  |
| `SLUG_TAKEN` | 409 | This slug is already taken. | Choose another slug or omit it to get one derived from the name. |
| `LOGIN_NOT_VERIFIED` | 409 | The sign-in link has not been confirmed yet. | The user has not clicked the email link yet. Keep polling `/v1/auth/poll`. |
| `LOGIN_EXPIRED` | 410 | This sign-in has expired. | Start again with `/v1/auth/start`. |
| `LOGIN_CONSUMED` | 409 | This sign-in was already used. | This login was already used to create a project. Start a new login. |
| `PROJECT_LIMIT_REACHED` | 409 | This account already has the maximum number of projects. | Reuse an existing project: run `npx brynth status` or connect it with `BRYNTH_KEY`. Contact support to raise the limit. |
| `LAST_ADMIN_KEY` | 409 | This is the last active admin key of the project. | Create another admin key before revoking this one. |
| `RATE_LIMITED` | 429 | Too many requests. | Wait `retry_after` seconds. |
| `EMAIL_DELIVERY_FAILED` | 502 | The sign-in email could not be sent. | Retry in a minute. If it keeps failing, contact support. |
| `INGRESS_SIGNATURE_INVALID` | 401 | The webhook signature is missing or invalid. | Signature missing or wrong. Check the source secret set with `source_add`. |
| `PAYLOAD_TOO_LARGE` | 413 | The request body is too large. | Webhook bodies are limited to 5 MB. |
| `SOURCE_NAME_TAKEN` | 409 | A source with this name already exists in the project. | Choose another `name`. |
| `EMAIL_SOURCE_EXISTS` | 409 | The project already has an email source. | The project already has its email address; see `source_list`. |
| `EVENT_NOT_CLAIMABLE` | 409 | The event cannot be claimed or changed in its current state. | The event is already claimed or closed. Call `inbox_list` for pending events. |
| `EVENT_ROUTED` | 409 | The event is delivered by a routing rule. | This event is delivered by callback, not by pull. |
| `LEASE_NOT_HELD` | 409 | Another key holds the lease on this event. | Another key holds this event. Claim a pending event instead. |
| `LEASE_EXPIRED` | 409 | The lease on this event has expired. | Your lease expired and the event went back to the queue. Claim it again with `inbox_claim`. |
| `TOO_MANY_WAITS` | 429 | Too many concurrent waits. | Too many concurrent `inbox_wait` (10 per key, 25 per project). Wait for one to return. |
| `CALLBACK_URL_REJECTED` | 400 | The callback URL is not allowed. | Use an https URL on a public address. |
| `KEY_LIMIT_REACHED` | 409 | The project has reached its limit of active keys. | The project has 20 active keys. Revoke one first. |
| `CHANNEL_NOT_VERIFIED` | 409 | An approver is not an active channel of this project. | An approver is not an active channel. Call `channel_list`, or `channel_add("telegram")` and ask the user to complete verification. |
| `CHANNEL_ADDRESS_REJECTED` | 400 | This address cannot be used as a channel. | Use a valid email outside the Brynth inbound domain. |
| `CHANNEL_EXISTS` | 409 | This address is already a channel of the project. | This address is already a channel; see `channel_list`. |
| `CHANNEL_OWNER_PROTECTED` | 409 | The owner's channel cannot be removed. | The owner's channel cannot be removed. |
| `CHANNEL_KIND_UNAVAILABLE` | 400 | This channel kind is not available on this server. | This channel kind is not configured on this server. Use `email`. |
| `POLICY_INVALID` | 400 | The policy document is invalid. | Fix the field at `path` and call `policy_set` again. |
| `TOO_MANY_PENDING_REQUESTS` | 429 | The project has too many pending requests. | 50 requests are pending. Wait for answers or let old ones expire. |
| `CALLBACK_SECRET_MISSING` | 409 | The project has no request callback secret. | Create the project's callback secret with `POST /v1/request-callback-secret`, or omit `callback_url`. |
| `DETAILS_REQUIRED` | 400 | A high-risk request needs `details`. | Put in `details` what you will do, why, what it touches (resources, people, amounts) and whether it can be undone. If the action is not high risk, lower `risk`. |
| `QUOTA_EXCEEDED` | 409 | The project disk is full. | The project disk is full (`limit`, `used`). Delete keys or files you no longer need, then retry. |
| `POLICY_DENIED` | 403 | A project policy denies this action. | A project policy denies this action (`rule`). Ask the user to change the policy with `policy_set` if it should be allowed. |
| `APPROVAL_NOT_APPLICABLE` | 409 | The approval does not apply to this call. | The approval does not match this call: it must be an approved `file_url` request for the same file and `expires_in`, not yet used, redeemed with the API key that asked for it within 24 hours of the decision. Call `file_url` without `approval` to ask again. |
| `UPLOAD_INCOMPLETE` | 409 | The upload is incomplete. | The upload is missing or its size differs from the declared one. PUT the file to `upload_url` with `upload_headers`, then call `file_put` with `complete` again. |
| `UPLOAD_LIMIT` | 429 | Too many open uploads at this path. | 3 presigned uploads are already open at this path. Finish one with `complete`, or wait for its `upload_url` to expire (15 minutes after `upload`), then call `file_put` with `upload` again. |
| `PATH_RESERVED` | 400 | This key or path is reserved. | Keys under `state/` are managed by `state_save`; paths under `evt_…/` are inbound attachments and read-only. |
| `SEND_LIMIT_REACHED` | 429 | The project reached its sending limit. | The project reached its sending limit (`limit`, `window`). Retry after `retry_after`, or send to fewer recipients. |
| `SEND_BLOCKED` | 403 | Sending is blocked for this project. | Sending is blocked for this project after too many bounces or complaints, or by the operator. Tell the user; only Brynth support can unblock it. |
| `EMAIL_SOURCE_REQUIRED` | 409 | The project has no active email source. | The project has no active email address, so replies would be lost. Add one with `source_add` (type `email`), then retry. |
| `INTERNAL` | 500 | Internal error. | Retry later. Quote the X-Request-Id header if it keeps failing. |
