# Error codes

Every error from the eMailPlane API has the same shape. The `code` never changes meaning, so your app or agent can branch on it.

```json
{ "error": {
    "code": "scope.forbidden",
    "message": "token lacks the scope required by this endpoint",
    "retryable": false,
    "human_action": true,
    "remedy": "Ask the app owner for a credential with one of the required scopes.",
    "next": "GET /v1/tokens/self",
    "docs_url": "https://emailplane.com/developers/errors/#scope-forbidden",
    "required_scope": "send",
    "granted_scopes": ["read:inbox"]
} }
```

| Field | Meaning |
|---|---|
| `code` | Stable and machine-readable. Branch on this, not on the message. |
| `message` | One line for a person. It never repeats an email address or a token back to you. |
| `retryable` | `true` when the same request can succeed later without you changing anything (a busy server, a spent hourly budget). |
| `human_action` | `true` when a person has to do something first: approve, change a policy, hand a conversation back, or issue a new credential. |
| `remedy` | What to do, in a sentence or two. When the server knows something more specific, this says it. |
| `next` | A call that tells you more or gets you unstuck, when there is one. |
| `docs_url` | This page, at the code. |

Some errors add details, such as `field`, `reason`, `retry_after_seconds` or `required_scope`. Your server lists every code with `GET /v1/errors`, and one with `GET /v1/errors/{code}`.

## Credentials and permission

### `auth.missing`

**No credential was sent.** Retry can work: no. A person must act: no.

Send `Authorization: Bearer <token>` with a token from your server. Next: `GET /v1/tokens/self`.

### `auth.invalid`

**The credential is not valid.** Retry can work: no. A person must act: yes.

The token is unknown, malformed or revoked. Ask the app owner for a new one; retrying the same token will not help.

### `auth.rate_limited`

**Too many sign-in attempts.** Retry can work: yes. A person must act: no.

Wait for the time in `Retry-After`, then try once. Retrying sooner extends the lockout.

### `scope.forbidden`

**This credential lacks the permission.** Retry can work: no. A person must act: yes.

The body lists the scopes the endpoint accepts and the ones you hold. Ask the app owner for a credential with one of the required scopes. Next: `GET /v1/tokens/self`.

### `role.forbidden`

**Your role cannot do this.** Retry can work: no. A person must act: yes.

No credential you can create fixes this; an operator or the owner has to do it.

### `tier.forbidden`

**Your plan does not include this.** Retry can work: no. A person must act: yes.

The body names the capability and the plan that includes it. Mail you already have keeps working. Next: `GET /v1/usage`.

### `plan.limit_reached`

**Your plan’s allowance is used up.** Retry can work: no. A person must act: yes.

The body shows the limit, what is used and what is included. Upgrade, add on, or free one up. Nothing that already exists is affected. Next: `GET /v1/usage`.

### `credential.inbox_bound`

**This credential is limited to one inbox.** Retry can work: no. A person must act: no.

An inbox credential can read and send only as its own inbox, and only on inbox endpoints. Use an app-wide credential for anything else. Next: `GET /v1/tokens/self`.

## The request

### `request.invalid`

**The request is not valid.** Retry can work: no. A person must act: no.

Fix the field named in `field` (and `reason`, when present), then send again.

### `validation.template_unresolved_tokens`

**The template needs more values.** Retry can work: no. A person must act: no.

Supply a value for every placeholder the body lists, then send again.

### `request.too_large`

**The request body is too large.** Retry can work: no. A person must act: no.

Send a smaller body; `limit_bytes` is the ceiling for this endpoint.

### `idempotency.mismatch`

**That Idempotency-Key was used for a different request.** Retry can work: no. A person must act: no.

Use a new key for a new request. A key always returns the first answer it was given.

### `conflict`

**It conflicts with what already exists.** Retry can work: no. A person must act: no.

Read the current state, then decide whether your change is still needed.

### `not_found`

**Not found.** Retry can work: no. A person must act: no.

Nothing with that id exists for this app. An id that belongs to another app reads the same way, on purpose.

## Sending limits and reputation

### `quota.exceeded`

**Today’s sending quota is used up.** Retry can work: yes. A person must act: no.

The quota resets at midnight UTC (`retry_after_seconds`). An operator can raise it. Next: `GET /v1/tokens/self`.

### `rate.capped`

**Sending is capped for now.** Retry can work: yes. A person must act: no.

The cap follows this credential’s bounce and complaint record and lifts by itself as that improves. Next: `GET /v1/reputation`.

### `budget.exhausted`

**The sending budget is spent.** Retry can work: no. A person must act: yes.

Complaints and bounces used up this credential’s sending budget. Clean the list; an operator can grant more. Next: `GET /v1/reputation`.

### `stream.quarantined`

**This stream is paused.** Retry can work: yes. A person must act: no.

This one stream (transactional or bulk) is paused after complaints; your other stream still sends. Stop the mail that caused it and wait for the gradual reopen. Next: `GET /v1/enforcement`.

### `queue.backpressure`

**The server is catching up.** Retry can work: yes. A person must act: no.

Nothing is wrong with your request. Wait for `Retry-After`, or send it as transactional mail.

### `send.unavailable`

**Sending is unavailable right now.** Retry can work: yes. A person must act: no.

A safety check the send path never skips could not run, so nothing was sent. Try again shortly.

### `plane_mail.daily_cap`

**Today’s account-mail allowance is spent.** Retry can work: yes. A person must act: no.

This credential sends the service’s own account mail (invites, receipts, reminders), which has its own daily limit and shares a daily ceiling with relayed mail. It resets by itself: wait for `Retry-After`. Next: `GET /v1/plane-mail/status`.

### `plane_mail.braked`

**Account mail is paused after complaints.** Retry can work: no. A person must act: yes.

Too many recipients reported this account mail as spam, so it stopped. An operator has to review it and turn it back on; retrying will not help. Next: `GET /v1/plane-mail/status`.

## Recipients

### `recipient.forbidden`

**This server may not deliver to that recipient yet.** Retry can work: no. A person must act: yes.

Delivery is limited to an allowlist the operator controls until this server is cleared to send to anyone. No credential changes that.

### `recipient.suppressed`

**That recipient is suppressed.** Retry can work: no. A person must act: no.

They complained, hard-bounced or unsubscribed. Remove them from your list; only an operator can lift it. Next: `POST /v1/suppressions/check`.

## Inboxes and reading mail

### `inbox.not_active`

**That inbox no longer receives mail.** Retry can work: no. A person must act: no.

The inbox was burned or has expired. Create a new one. Next: `POST /v1/inboxes`.

### `inbox.no_service_domain`

**This app has no domain for inboxes yet.** Retry can work: no. A person must act: yes.

Inboxes are created at the app’s own domain. Set one up first.

### `address.taken`

**That address is already in use.** Retry can work: no. A person must act: no.

Choose another local part, or leave it out to get a random one.

### `address.burned`

**That address was burned.** Retry can work: no. A person must act: no.

A burned address is never issued again, so an old user’s mail cannot reach a new one. Choose another.

### `address.reserved`

**That address is reserved.** Retry can work: no. A person must act: no.

Names such as postmaster and abuse belong to the mail system. Choose another.

### `view.agent_content_only`

**The agent view returns text only.** Retry can work: no. A person must act: no.

Agents read the cleaned, wrapped text. HTML and raw bytes are not available in the agent view.

### `withheld.human_only`

**Only a person can open this message.** Retry can work: no. A person must act: yes.

The message was withheld from agents (a lookalike sender, hidden content or a failed DMARC check). A signed-in person can review it.

### `wait.limit_reached`

**Too many waits are open.** Retry can work: yes. A person must act: no.

Cancel or let some waits finish before arming another. Next: `GET /v1/waits`.

## Agents: policy, drafts and conversations

### `agent.paused`

**Sending is paused for this agent.** Retry can work: no. A person must act: yes.

Someone pulled the kill switch for this credential, its inbox or the app. A person or an admin resumes it. Next: `GET /v1/send-policy`.

### `agent.self_resume`

**A credential cannot lift its own pause.** Retry can work: no. A person must act: yes.

A signed-in person, or another admin credential, resumes it.

### `loop.auto_submitted`

**That message was sent by a machine.** Retry can work: no. A person must act: no.

Mail from auto-responders, mailing lists and bounce systems is never answered automatically, so two machines cannot reply to each other forever.

### `loop.circuit_open`

**Too many sends into one conversation.** Retry can work: yes. A person must act: no.

The per-conversation limit stops a reply loop. It reopens after the window in your send policy. Next: `GET /v1/send-policy`.

### `budget.tool_calls`

**This hour’s call budget is spent.** Retry can work: yes. A person must act: no.

Wait for the next hour (`Retry-After`), or ask a person to raise `tool_calls_per_hour` in the send policy. Next: `GET /v1/send-policy`.

### `draft.not_pending`

**That draft was already decided.** Retry can work: no. A person must act: no.

A draft is approved, rejected or expires once. Read it to see what happened.

### `approval.human_only`

**Only a person can approve or reject.** Retry can work: no. A person must act: yes.

Drafts are decided in the dashboard, or by replying APPROVE or REJECT from the approver’s mailbox. No credential can decide one.

### `thread.human_owned`

**A person has taken this conversation.** Retry can work: no. A person must act: yes.

Do not answer. You can offer a reply with a draft; only a person hands the conversation back. Next: `POST /v1/drafts`.

### `thread.handback_human_only`

**Only a person can hand a conversation back.** Retry can work: no. A person must act: yes.

Handing a conversation back to an agent is a person’s (or an admin’s) decision.

## The server

### `internal`

**Something went wrong on the server.** Retry can work: yes. A person must act: no.

Try again. If it keeps happening, the server log has the details; error bodies never do.

## From the MCP server

These come from the MCP server itself, when the request never reached the API. They carry the same fields.

### `infra.control_plane_unreachable`

**The MCP server could not reach the API.** Retry can work: yes. A person must act: no.

The eMailPlane API runs on your own server; check that it is running and that the MCP server is configured with its address, then try again.

### `infra.unparseable_response`

**The API answered with something that is not JSON.** Retry can work: yes. A person must act: no.

Something between the MCP server and the API answered with a page that is not JSON. Try again; if it persists, check the proxy in front of the API.

### `custody.mailbox_handoff_unavailable`

**A mail-app password cannot be issued over MCP yet.** Retry can work: no. A person must act: yes.

A person’s mail-app password cannot be handed over safely through MCP yet. Nothing was changed; ask the owner to issue it from the dashboard.

### `handoff.activation_unconfirmed`

**The new credential is saved but not yet confirmed active.** Retry can work: yes. A person must act: no.

The new credential is saved in its local file. Call the same tool again with the same idempotency key to confirm it is active.

---

Source: https://emailplane.com/developers/errors/
