Errors
Errors
One envelope, every path, every failure. Branch on code, never on the message: the
message is written for a human reading a screenshot and may be reworded, while the code is
stable.
The envelope
{
"error": {
"message": "Invalid API key (invalid_api_key)",
"type": "authentication_error",
"param": null,
"code": "invalid_api_key",
"support": "[email protected]",
"docs": "https://docs.hisoka.ai/errors/",
"request_id": "01M73N2HP4HDDZH803J1C120HT"
}
}
The code appears inside message too, because a pasted screenshot carries the sentence and
not the JSON field.
On /messages the same content is wrapped in the Anthropic shape:
{"type": "error", "error": {...}, "request_id": "..."} with an Anthropic error.type
beside our code. Sending an OpenAI-shaped body to Claude Code is the failure that shape
prevents.
Headers on a failure
| Header | Value |
|---|---|
X-Request-Id |
matches request_id in the body |
x-should-retry |
true for 408, 409, 429 and 5xx |
x-should-retry is the one to read before writing retry logic. It encodes the gateway’s
own judgement about whether the same request could succeed a moment later.
When to retry
429 rate_limited retry after Retry-After
429 pool_exhausted retry after Retry-After; do NOT switch models
502 upstream_error retry, preferably with backoff
503 model_unavailable retry with backoff
504 request_timeout retry with a longer budget or a shorter prompt
500 internal_error retry once, then report the request_id
400 anything do not retry, the request itself is wrong
401 invalid_api_key do not retry, fix the key
402 insufficient_quota do not retry, top up
403 ip_not_allowed do not retry, fix the allowlist
404 model_not_found do not retry, pick a published id
413 payload_too_large do not retry, send less
A request-shaped failure is deterministic. Retrying a 400 across the pool of upstream
accounts turns one malformed request into several failed attempts, which is why the gateway
stops at the first one instead of failing over. If you see a burst of 400s, the cause is
the payload, not the provider.
Full code list
| Code | HTTP | Fires when |
|---|---|---|
invalid_api_key |
401 | missing, malformed, revoked or expired key |
ip_not_allowed |
403 | the key has an allowlist and you are not on it |
insufficient_quota |
402 | the wallet cannot cover the call, or is under the free-model floor |
model_not_found |
404 | that id is not a published model |
modality_not_supported |
400 | an image, video or audio part sent to a model that does not take it |
context_length_exceeded |
400 | prompt plus completion cap overflow the window |
max_tokens_exceeded |
400 | the cap is above what the model or gateway allows |
invalid_request |
400 | malformed JSON, empty messages, or a bad parameter |
not_found |
404 | no such endpoint |
method_not_allowed |
405 | GET on a POST route, or the reverse |
transaction_not_found |
404 | that ledger id is not yours |
rate_limited |
429 | upstream throttling, or our own route limit |
pool_exhausted |
429 | every account behind the model is throttled; wait |
gateway_rate_limited |
429 | our own per-key route limit |
payload_too_large |
413 | the body exceeds the accepted size |
entitlement_denied |
403 | the plan does not include this feature |
model_unavailable |
503 | a provider credential or quota problem on our side, or nothing healthy in the pool |
no_model_published |
503 | the catalogue is empty |
upstream_error |
502 | the provider failed, or answered something unclassifiable |
request_timeout |
504 | the upstream call ran past its time limit |
usage_unavailable |
502 | the reply arrived but could not be metered, so it was not billed |
internal_error |
500 | an unhandled fault on our side; the request_id has the trace |
What each one actually means for you
model_unavailable is usually not your problem. It means a credential or quota on our
side, or that nothing healthy is left in the pool. Retrying with backoff is right;
switching models is not the first move, because the model itself is fine.
pool_exhausted is a wait, not a model problem. The Retry-After header and the
message both carry the soonest time an account frees up. Switching models costs you a model
choice for a wait you could have sat through.
request_timeout is our clock, not a provider crash. The fix is a longer budget or a
shorter prompt, never a different pool.
usage_unavailable means the answer was thrown away rather than billed half. Nothing
was charged. Look at your request shape, then report the request_id.
internal_error never appears in upstream telemetry, because it fails on our side
before any provider is reached. The request_id has the stack trace, so quoting it is what
makes the report actionable.
Reporting a problem
Include the request_id, the endpoint, and the code. That is enough to find the exact
server-side attempt without exchanging prompts or credentials.
Never paste a full API key. The first characters plus a timestamp is enough to locate one.