Docs
Error catalog
Stable JSON error codes, when to expect each, and how to recover programmatically.
Errors
Every Hangar error response is a JSON object with a stable code, a human message, and an optional structured detail.
Shape
{
"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
{
"error": "validation",
"message": "Request validation failed.",
"details": {
"name": ["String must contain at least 1 character(s)"]
}
}
Wallet empty
{
"error": "wallet-empty",
"message": "Wallet balance is zero. Top up at /dashboard/wallet."
}
Rate limited
{
"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-errorwith a redacteddetailsfield.