Errors, limits, retries
Error format
Errors use the OpenAI format:
{
"error": {
"message": "No API key provided",
"type": "invalid_api_key",
"param": "api_key",
"code": "401"
}
}
Branch on the type field, not on the message text; the texts may change.
Status codes
| Status | type | Meaning | What to do |
|---|---|---|---|
400 | invalid_request_error | Invalid request, e.g. unknown model name or no model given. | Fix the request; check the model name in the model list. |
401 | invalid_api_key | Key missing, invalid or revoked. | Check the Authorization: Bearer <key> header. |
402 | payment_required | Balance empty. Private account: with a link to top up in the console. Team member: "ask your admin". | Top up or ask your admin. |
403 | account_suspended | Account suspended. | Contact support. |
403 | company_plan_paused | The company's team plan is paused (applies to members). | Ask the team owner. |
403 | permission_denied | Model not enabled for your company, or a partner model without any top-up so far. | Ask your admin to enable it, or top up. |
403 | model_not_enabled | Model not enabled for your account. | Contact Open Hippo. |
404 | not_found_error | The path does not exist. | Compare the path with the endpoint list. |
429 | quota_exceeded | A budget is reached: key, account, member or company. Header Retry-After: 60. | See Budgets & limits. |
429 | rate_limit_exceeded | Only on GET /v1/usage: more than 30 queries per minute and key. | Query again after Retry-After. |
503 | service_unavailable | Authentication service temporarily unavailable. Header Retry-After: 60. | Retry after Retry-After. |
A path that does not exist at all may also get a plain 404 response without JSON.
Examples
Format examples with the gateway's message texts. Branch on the type field; the texts in message may change.
429, key budget with hard stop reached:
HTTP/2 429
Retry-After: 60
Content-Type: application/json
{
"error": {
"message": "API key spend limit reached",
"type": "quota_exceeded",
"param": "api_key",
"code": "429"
}
}
Other budgets return the same type with a different text, e.g. Monthly account budget reached (account), You have reached your monthly limit — ask your admin to raise it. (member) or Your company has reached its monthly limit. (company).
402, balance empty, private account:
{
"error": {
"message": "Insufficient credit balance. Top up at https://console.openhippo.io/billing",
"type": "payment_required",
"param": "api_key",
"code": "402"
}
}
402, balance empty, team member:
{
"error": {
"message": "Your company's balance is empty — ask your admin to top up.",
"type": "payment_required",
"param": "api_key",
"code": "402"
}
}
Check order
Before every request the API checks in this order; the first hit determines the error:
- Account suspended →
403 account_suspended - Team plan paused →
403 company_plan_paused - Balance at €0 →
402 payment_required - Key budget reached →
429 quota_exceeded - Company budget reached →
429 quota_exceeded - Member or account budget reached →
429 quota_exceeded
The result of the check is cached for up to 60 seconds. A budget can therefore be overshot slightly before the stop takes effect.
Rate limits
Fixed RPM/TPM limits (requests or tokens per minute) are currently not documented. For load planning: support@openhippo.io. Exception: GET /v1/usage allows 30 queries per minute and key. The 429 quota_exceeded above are budgets, not rate limits.
Retries
- Retrying makes sense on
503: wait as long asRetry-Aftersays. 429 quota_exceededis a budget, not a short-term rate limit. A retry only helps once the budget is raised or the new month begins.- Do not retry on
400,401,402,403and404: the request will fail the same way.
import os, time
import openai
from openai import OpenAI
client = OpenAI(
base_url="https://api.openhippo.io/v1",
api_key=os.environ["OPENHIPPO_API_KEY"],
max_retries=0, # control retries yourself
)
def chat(messages, attempts=3):
for attempt in range(attempts):
try:
return client.chat.completions.create(model="openhippo/gpt-oss-120b", messages=messages)
except openai.APIStatusError as e:
if e.status_code != 503 or attempt == attempts - 1:
raise
time.sleep(int(e.response.headers.get("retry-after", "60")))
print(chat([{"role": "user", "content": "Hello!"}]).choices[0].message.content)