Errors and exit codes
Every Inbot error code with its HTTP status and meaning, the error body shape shared by REST and MCP, and the stable inbot CLI exit codes.
Every REST and MCP error has the same shape:
{
"name": "InbotError",
"code": "missing_permission",
"message": "The API key does not permit this operation.",
"fix": "Ask the owner for the message_send permission."
}
Errors may also carry an optional docs link; the contract permits an omitted fix, while service errors provide an actionable fix.
code is stable; message and fix are for people and agents to read. Follow fix. OAuth endpoints are the exception: they use the OAuth format, { error, error_description }.
A 401 response also carries WWW-Authenticate: Bearer resource_metadata="<origin>/.well-known/oauth-protected-resource/...", scope="inbot", which OAuth-capable clients use to start authorization.
Error codes
| Code | HTTP | Meaning |
|---|---|---|
validation_error |
400 | Invalid fields, query parameters, JSON or content type; invalid composition; an expired resume ID |
unauthorized |
401 | Missing, invalid or expired credential; wrong password, setup code or login link |
missing_permission |
403 | The grant lacks a permission; an owner-only route; a read_only send; a foreign origin or host; missing CSRF; a route blocked over the tunnel |
approval_required |
403 | The key’s send policy is approval. A pending draft was created; its ID is in message and fix. |
message_rejected |
403 | A recipient is refused by the send or reply lists, or the draft was rejected by the owner. Nothing was queued. |
inbox_paused |
403 | The inbox is paused |
not_found |
404 | The resource does not exist, is outside the grant’s scope, or carries a hidden label the grant cannot read |
already_exists |
409 | Duplicate inbox address, list entry, or owner account |
resource_taken |
409 | The draft is being sent, or a Cloudflare resource belongs to someone else |
idempotency_conflict |
409 | The idempotency key was used with a different payload |
page_token_stale |
409 | The page token does not match the query; restart without it |
action_required |
409 | The owner must act first, for example choose an account, tear down a domain, fix DNS, or restore the buffer configuration before a quarantine retry or delete |
payload_too_large |
413 | The composed outbound message exceeds 5 MiB |
locked |
423 | The database is locked. The current service only serves after unlocking, so this is not returned in practice. |
rate_limit_exceeded |
429 | Too many password attempts or pending login links |
not_supported |
501 | URL attachments, OAuth consent without the web app, retrying a quarantined Queue item, a local service install from a source checkout, or another unsupported option |
service_unavailable |
503 | Storage failure, unexpected error, or no mail transport configured |
The inbound 25 MiB limit is an SMTP rejection or a quarantine reason, not an API 413 response.
The contract also declares permission_escalation, limit_exceeded and domain_not_verified (403). The first two are not currently returned. domain_not_verified can appear as a failed message’s send_reason when Cloudflare refuses the sender, rather than as a response to the send request.
CLI exit codes
| Exit | Meaning |
|---|---|
| 0 | Success, or help |
| 1 | Unexpected local failure, including failing to open a browser |
| 2 | Invalid arguments or payload; HTTP 400 or 413; binary download output to a terminal |
| 3 | HTTP 401 |
| 4 | HTTP 403, including approval_required and message_rejected; inbot service with INBOT_URL or INBOT_API_KEY set |
| 5 | HTTP 404 |
| 6 | HTTP 409 |
| 7 | HTTP 423 |
| 8 | HTTP 429 |
| 9 | HTTP 501 |
| 10 | Connection or response failure, HTTP 503, any other status, or a lost event stream |
Errors print to stderr as one JSON line. Server errors keep their original fields; local errors print { code, message, fix } without credentials or payload contents. inbot serve exits with 0 after a clean stop and 1 when startup fails.