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 4xx will fail the same way on every account

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.