# API errors

Every response under `/api/` with a status of 400 or above returns the same JSON object. Branch on `error`. Never parse `message`.

## The envelope

```
{`{
  "error": "method_not_allowed",
  "message": "GET is not supported on this endpoint. Allowed: POST.",
  "request_id": "d22e3678-8a18-4aa3-ab86-bfd374080269",
  "documentation_url": "https://glassmkr.com/docs/api/errors#method_not_allowed",
  "retryable": false,
  "retry_after_seconds": null,
  "details": [{ "allowed_methods": ["POST"] }]
}`}
```

- **error** is a stable machine code. It is the only field you should make decisions from. Codes are added over time; an unrecognised one should be treated by its HTTP status.
- **message** is for humans and logs. The wording can change in any release.
- **request_id** matches the `X-Request-Id` response header. Quote it when reporting a problem.
- **retryable** says whether an identical retry could plausibly succeed. A client mistake is never retryable, because retrying it unchanged only burns quota.
- **retry_after_seconds** is set when the server can say how long to wait. The `Retry-After` header carries the same value.
- **details** is a list of structured extras, empty when there are none.

This holds for the whole namespace, including a 404 for an unknown path and a 405 for the wrong verb. Those two used to return the HTML app shell and untyped plain text respectively, which is exactly the shape an autonomous client cannot recover from.

## Deciding what to do

The envelope is designed so a client can decide without reading prose:

- `retryable: true` and a `retry_after_seconds`: wait that long, then retry the same request.
- `retryable: true` without a delay: retry with your own backoff.
- `retryable: false` on a 401: get a different credential. The same one will fail identically.
- `retryable: false` on a 403: stop. This credential is not permitted the operation, and repeating it will not change that.
- `retryable: false` on a 400 or 422: fix the request from `details` before sending anything again.
- `retryable: false` on a 409: re-read the current state before deciding, because yours is stale.

## Code reference

{codes.length} codes, {retryable.length} of them retryable. Each anchor here is what `documentation_url` points at.

| Code | Status | Retryable | Meaning |
| --- | --- | --- | --- |
| `{c.code}` | {c.status ?? "4xx"} | {c.retryable ? "yes" : "no"} | {c.meaning} |

## OAuth is deliberately different

The MCP authorization endpoints under `/oauth/` return the shape RFC 6749 mandates, `{"{ error, error_description }"}`, not the envelope above. An OAuth client is entitled to expect the standard shape, so those routes are excluded on purpose rather than by oversight.
