Errors you can act on
Every error from Hyperouter uses OpenAI's error object and a matching HTTP status. Code that handles OpenAI's errors already handles most of these. This guide covers what is worth acting on and what is not, and ends with a retry loop.
The error object
{
"error": {
"message": "n must be an integer from 1 to 128",
"type": "invalid_request_error",
"code": "invalid_body",
"param": "n"
}
}
| Field | Meaning |
|---|---|
message | A sentence that says what is wrong. Do not parse it. |
type | The class of error. |
code | A stable identifier for the cause. Match on this field. |
param | The request field at fault, or null. |
Some errors add generation_id: the id of a job that is still running or already ran. Poll it with GET /v1/generations/{id}.
Statuses worth handling
| Status | What it means | What to do |
|---|---|---|
400 | The request is invalid. code names the cause and param the field. | Fix the request. Do not retry it unchanged. |
401 | The API key is missing or not valid. | Check the key. |
402 | insufficient_credits: your balance, or the key's limit, cannot cover the most the request could cost. | Top up, or raise the key's limit. |
403 | The account is suspended, or a feature or model is not enabled for it. | Open a support ticket. |
404 | No such id, another account's id, or a video past its expires_at. | Do not retry. |
409 | Not in a state that allows the call, or an Idempotency-Key conflict. | See retry safely. |
429 | Every provider is at capacity, or your account is over its rate limits. | Wait for Retry-After, then retry. |
500, 501 | Hyperouter failed, or the endpoint is not implemented. Not billed. | Retry a 500 with care, as below. |
503 | A provider failed or did not answer in time. | Retry, as below. |
Three codes you will meet
insufficient_credits(402). On chat, the message says how many tokens you can afford. Loweringmax_tokenslowers the most the request can cost. A top-up is a manual request in the dashboard; GET /v1/credits lets you see it coming.model_not_enabled(403). The model needs access granted to your account. Open a support ticket to request it. Nothing is held or sent to the provider.rate_limited(429). Your account is over its requests per minute or its concurrent requests. Nothing is held or forwarded. The response has aRetry-Afterheader in seconds, and the rate limits section lists the tiers.
The same family includes venue_busy (429): every provider for a chat model is over its rate budget.
Why 503, not 502 or 504
Hyperouter never answers 502 or 504. Those would reach you as a bare page from our edge network, without the error body, so you could not read code.
Where a gateway would answer them, you get 503 with the usual error object, and the gateway status in the response header X-Upstream-Status: 502 when a provider failed, 504 when it timed out. The code says which kind of failure it was: all_venues_failed, upstream_error or timeout.
HTTP/2 503
x-upstream-status: 504
Read X-Upstream-Status if you want to tell a failure from a timeout. For most code, 503 and code are enough.
When not to retry
Retrying is not always safe. A retry can start a second job or a second charge. Hyperouter marks those answers with x-should-retry: false:
- the
503timeoutof a sync image. The job goes on, and is billed when it succeeds. Pollerror.generation_idinstead; - a
500after a job was created; 409idempotency_key_reusedandidempotency_key_replayed.
On chat, a provider that has accepted a request may be running it. If it then stops answering, the request is not sent to another provider: the answer is 503 with code timeout or upstream_error, and it is billed on its hold. Look it up with GET /v1/generations/{id} before you decide to send it again.
A retry loop with backoff
Retry 429 and 503, honour Retry-After, give up on x-should-retry: false, and send an Idempotency-Key on anything that costs money.
import random import time import uuid import requests def post_with_retry(url, headers, body, attempts=5): headers = {**headers, "Idempotency-Key": str(uuid.uuid4())} # same key on every attempt for attempt in range(attempts): r = requests.post(url, headers=headers, json=body, timeout=120) if r.status_code < 400: return r.json() if r.headers.get("x-should-retry") == "false": r.raise_for_status() if r.status_code not in (429, 503) or attempt == attempts - 1: r.raise_for_status() wait = float(r.headers.get("Retry-After", 2 ** attempt)) time.sleep(wait + random.random()) # jitter
async function postWithRetry(url: string, body: unknown, attempts = 5) {
const headers = {
Authorization: `Bearer ${process.env.HYPEROUTER_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(), // same key on every attempt
};
for (let attempt = 0; ; attempt++) {
const res = await fetch(url, { method: "POST", headers, body: JSON.stringify(body) });
if (res.ok) return res.json();
const retryable = [429, 503].includes(res.status) && res.headers.get("x-should-retry") !== "false";
if (!retryable || attempt === attempts - 1) throw new Error(`HTTP ${res.status}`);
const wait = Number(res.headers.get("Retry-After") ?? 2 ** attempt);
await new Promise((r) => setTimeout(r, (wait + Math.random()) * 1000));
}
}
On a chat request, the retry gets 409 idempotency_key_replayed if the first attempt did run. That is the cue to read the record with GET /v1/generations/{id} and stop.
The OpenAI SDKs already retry a 408, 409, 429 or 5xx twice and respect x-should-retry: false, so you may need no loop at all.
What next
- Errors lists every code with its status.
- Quote the request
idwhen you open a ticket: Every request has an ID you can quote.