Errors & rate limits
Every error, on every endpoint, uses one envelope:
{
"error": {
"code": 401,
"message": "Missing or invalid API key",
"metadata": { "error_type": "authentication" }
}
}
code repeats the HTTP status. metadata.error_type is a stable machine-readable classifier —
branch on it, not on message, which is written for humans and may change. Some errors add
further metadata members, listed below where they exist. Monetary members among them
(required, spendable, limit, used) are integer strings in Heimdallm's internal fixed-point
unit (10⁻²⁴ USD) — compare them to each other, not to dollar figures.
Statuses
| Status | error_type | When |
|---|---|---|
400 | invalid_request | The body does not validate: a missing required field, a wrong type, an out-of-range value, malformed JSON, an unknown key inside provider. |
400 | provider_error | The serving endpoint rejected the request as invalid. metadata.provider_status and a short provider_message say why. |
400 | context_length_exceeded | The provider rejected the request as longer than the model's context. |
401 | authentication | The key is missing, malformed, unknown, revoked or expired. Deliberately identical in every case. |
402 | payment_required | Not enough spendable balance for the request's credit hold (metadata.required, metadata.spendable), or a spend limit is reached (metadata.scope = key / workspace / account, with limit, used and limit_reset). |
404 | not_found | No such route, model or generation. When the id names a model family, metadata.family_variants lists the served variants (details). |
408 | timeout | The request was aborted client-side before it finished. |
413 | payload_too_large | The request body exceeds the 8 MB limit, or the provider rejected it as too large. |
429 | rate_limit_exceeded | Your key or address hit its rate limit. Honour Retry-After. |
429 | provider_overloaded | The serving provider is rate limiting; a provider-sent Retry-After is passed through. |
500 | server | A fault in Heimdallm. The message is always Internal server error; details stay server-side. |
502 | provider_unavailable / provider_error | Every eligible endpoint failed: unreachable, an unreadable response, a provider-side error, or the provider does not serve this API. |
503 | no_route | The model exists but no live endpoint satisfies the request — metadata.rejections itemises why, per endpoint. |
503 | provider_overloaded | The catalogue is still warming up after a restart, or the instance that received the request is restarting and refused it. A restart carries Retry-After and X-Should-Retry: true. |
503 | server | The marketplace could not finish an admitted request for a reason of its own: the instance serving it restarted mid-answer, or its database was briefly unreachable. Repeat the request. |
504 | timeout | The provider accepted the request and then stopped: no answer in time, or a stream silent past its silence bound. |
Retrying. 408, 429, 502, 503 and 504 are worth retrying with exponential backoff —
routing re-runs on every attempt, so the retry may land on a different endpoint entirely. A restart
of the marketplace is the case to wait out rather than give up on: it answers 503 with Retry-After: 5
and X-Should-Retry: true, and a client that keeps retrying is served by the replacement within seconds. 4xx
responses other than those will fail the same way until the request changes. During a routed
request Heimdallm already fails over between eligible endpoints on its own; errors that no other
endpoint could fix (an invalid body, an oversized payload, a client abort) are returned
immediately instead.
Provider failures carry evidence. When the terminal error came from a provider, metadata
includes provider_status (their HTTP status) and usually provider_message (a short excerpt of
their error text), plus provider_code where one was given. A provider rejecting our upstream
credential is reported as a plain 502 — it is not your authentication problem.
For a failure attributed to the provider, metadata.provider_name names the provider whose terminal
attempt failed. metadata.raw, when a response body was received, contains its original text: the HTTP
body or the failing SSE event's data payload, including its whitespace. It is capped at 4,096 UTF-8
bytes without splitting a character, so a truncated JSON body may not parse. No response body is
invented for a disconnect or timeout. These members are carried identically in HTTP errors and
mid-stream error chunks and are omitted for caller-caused errors, blocked addresses and marketplace
failures. They add no stored response-body history.
Rate limits
Two request-rate buckets protect the public API, both over a 60-second sliding window:
| Bucket | Applies to | Default |
|---|---|---|
| Per API key | Every authenticated request, per key | 600 / minute |
| Per client address | Unauthenticated (catalogue) requests | 120 / minute |
A client address is an IPv4 address, or the /64 prefix of an IPv6 address, which is the block one
subscriber is assigned. A HEAD request counts against the same bucket as the GET it mirrors.
Every counted response carries the state of your bucket:
X-RateLimit-Limit: 600
X-RateLimit-Remaining: 597
X-RateLimit-Reset: 1755300060
X-RateLimit-Reset is Unix seconds. When the bucket is exhausted the request is refused with
429 and a Retry-After header (seconds); back off until then. Rate limits count requests, not
tokens — batch where the API allows it (embeddings input arrays, longer conversations per call)
rather than multiplying round-trips.
{
"error": {
"code": 429,
"message": "Rate limit exceeded",
"metadata": { "error_type": "rate_limit_exceeded" }
}
}
Spending ceilings are a separate mechanism — a breached spend limit is a 402, described in
Pricing & billing.
Errors while streaming
Once a streaming response has started, the HTTP status can no longer
change. A failure then arrives in-band as a final chunk with an error member — the same
envelope shape — and finish_reason: "error", followed by data: [DONE]:
data: {"id":"gen-9f2c7e1a4b6d","object":"chat.completion.chunk","created":1755300000,"model":"deepseek/deepseek-v4-flash-0731-abliterated","provider":"ImpCC","error":{"code":502,"message":"Provider disconnected","metadata":{"error_type":"provider_error"}},"choices":[{"index":0,"delta":{"content":""},"finish_reason":"error"}]}
data: [DONE]
Treat such a chunk as terminal for that request. A provider that closes its stream after part of the
answer, with no finish reason, no usage report and no [DONE], fails the same way, with a 502 whose
message is The provider closed the stream before the answer finished. A 503 chunk is the marketplace restarting under a
stream: its message says to try again in 5 seconds, and repeating the request is correct. On
/v1/messages the same failure is overloaded_error, and on
/v1/responses it is server_error, the codes those clients retry.
Output that streamed before the failure is billed for the tokens
actually generated; a stream that failed before producing anything has its token charges waived
(see Pricing & billing).
metadata may carry additional keys beyond error_type — operator notes about where a failure
came from. The set grows over time, so ignore keys you do not
recognise; the status, the code, the message and error_type are the contract.
Correlating failures
Every inference response — errors included — carries an X-Generation-Id header, assigned before
anything can fail. Quote it when reporting a problem, and use
GET /generation to inspect what was recorded and charged
for a request that went wrong.