# Errors
Every Hangar error response is a JSON object with a stable code, a
human message, and an optional structured detail.
## Shape
```json
{
"error": "<machine-readable-code>",
"message": "<human description>",
"details": { "...": "optional" },
"retryAfterSeconds": 0
}
```
The `error` code is machine-readable and stable across releases. The
`message` is for human display. `details` carries Zod-style field
errors on validation failures; `retryAfterSeconds` is set on 429 and
mirrors the `Retry-After` header.
## Code catalog
| Code | HTTP | When to expect it | What to do |
|---|---|---|---|
| `unauthorized` | 401 | Missing, malformed, or expired token / session. | Re-authenticate; mint a new PAT if expired. |
| `forbidden` | 403 | Token is valid but lacks the required scope. | Mint a token that includes the scope. |
| `not-found` | 404 | Resource doesn't exist for this caller. | Check the id; resource may belong to another user. |
| `validation` | 400 | Request body or query failed Zod validation. | Inspect `details` for field-level errors and fix the request. |
| `invalid-json` | 400 | Body could not be parsed as JSON. | Send a valid JSON body with `Content-Type: application/json`. |
| `rate-limited` | 429 | Sliding-window limit exceeded. | Honor `retryAfterSeconds` and back off. |
| `wallet-empty` | 402 | Wallet hit zero before the call ran. | Top up via `POST /api/wallet/topup` and retry. |
| `provider-error` | 502 | Upstream LLM or billing provider failed. | Retry with exponential backoff (4s, 8s, 16s, 32s). |
| `missing-id` | 400 | A required path or query parameter is missing. | Add the parameter and retry. |
| `conflict` | 409 | Resource state conflicts with the requested change. | Re-read state; the operation is not safe to retry blindly. |
| `internal` | 500 | Unhandled server error. | Retry once; report on Discussions if it persists. |
## Examples
### Validation
```json
{
"error": "validation",
"message": "Request validation failed.",
"details": {
"name": ["String must contain at least 1 character(s)"]
}
}
```
### Wallet empty
```json
{
"error": "wallet-empty",
"message": "Wallet balance is zero. Top up at /dashboard/wallet."
}
```
### Rate limited
```json
{
"error": "rate-limited",
"message": "Too many requests. Retry after 12s.",
"retryAfterSeconds": 12
}
```
## What never happens
- We never return HTML on a 4xx/5xx response from `/api/*`. Always JSON.
- We never include a stack trace or PII in production responses.
- We never surface upstream provider error bodies verbatim — they are
re-mapped to `provider-error` with a redacted `details` field.