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

# Errors

> One envelope on every surface, and a closed set of codes to branch on.

Any failure, on any surface, comes back in one shape:

```json theme={null}
{
  "error": {
    "code": "validation_failed",
    "message": "Give an email address or an external_id",
    "field": "requester.email"
  }
}
```

The REST API, the MCP server, and the widget all use it. A client that had to
branch on which surface it was talking to would sooner or later get the branch
wrong.

## Branch on `code`, never on `message`

`code` is the contract. It comes from a fixed set, and its meaning never
changes.

`message` is written for a human reading a log. We may reword it at any time.
Anything that matches on the text of a message is a bug waiting for a copy
edit.

`field` appears on validation failures. It is a dotted path into the body you
sent.

## The codes

| Status | `code`                | When                                                                                |
| ------ | --------------------- | ----------------------------------------------------------------------------------- |
| 400    | `validation_failed`   | The body failed schema validation. `field` names the cause, and nothing was changed |
| 401    | `invalid_credential`  | Missing, malformed, expired, or revoked token                                       |
| 402    | `quota_exhausted`     | Out of conversations for this period. See [Quotas](/guides/quotas)                  |
| 403    | `forbidden`           | Authenticated, but this role cannot do this                                         |
| 404    | `not_found`           | Unknown id, **or** another workspace's resource                                     |
| 409    | `conflict`            | A unique constraint you can see, such as a reused reference                         |
| 422    | `invalid_transition`  | A status change the state machine does not allow                                    |
| 429    | `rate_limited`        | Over the limit. `Retry-After` says when to come back                                |
| 500    | `internal_error`      | Ours. `X-Request-Id` identifies the occurrence                                      |
| 503    | `service_unavailable` | A dependency we do not control is down, such as the model provider                  |

## Two that surprise people

### 404 for another workspace's data

Asking for a ticket that belongs to someone else returns `not_found`, exactly
as if it did not exist. It never returns `403`.

That is by design. A `403` would tell you the id is real, and being able to
probe for which ids exist in other workspaces is itself the leak.

### 402 rather than 403 when you run out

`quota_exhausted` is a billing state, not a permissions one. Your credentials
are fine, and you are allowed to do this; the account is not paid up.

The distinction matters for clients. Something treating `403` as "log in
again" would send an integration round an auth loop over an unpaid invoice.

## Validation changes nothing

A `400` means the request was rejected before anything acted on it. There is
no partial write to undo. Fix the field named in `field`, then send it again.

## Retrying

<Warning>
  Retrying a `POST` without an `Idempotency-Key` is how you get duplicate
  tickets. See [Idempotency](/guides/idempotency).
</Warning>

| Code                  | Retry?                                                       |
| --------------------- | ------------------------------------------------------------ |
| `rate_limited`        | Yes, after `Retry-After`                                     |
| `service_unavailable` | Yes, with backoff                                            |
| `internal_error`      | Yes, with backoff. Keep the `X-Request-Id`                   |
| `quota_exhausted`     | No. Nothing changes until the period resets or the plan does |
| `validation_failed`   | No. The same body will fail the same way                     |
| `invalid_credential`  | No                                                           |
| `not_found`           | No                                                           |
| `invalid_transition`  | No. Read the current status first                            |
