Reference
Errors
Every failure returns a typed JSON envelope and a standard HTTP status. Branch on the stable type string (or the status code) — never on the human-readable message, which may change.
The error envelope
On /v1/chat/completions a failed request returns these fields:
{
"error": {
"message": "Insufficient balance for this request: about ₹{needed} is needed for the prompt, ₹{available} is available. Top up to continue.",
"type": "insufficient_balance",
"code": 402,
"error_code": "UB-GW-NNN",
"request_id": "7b679c45-424f-4153-b72e-e7217758065f"
}
}| Field | Type | Notes |
|---|---|---|
| message | string | Human-readable description. For logs, not for control flow. |
| type | string | Stable machine code from the taxonomy below. Branch on this. |
| code | number | The numeric HTTP status (e.g. 402), mirrored in the response status line. |
| error_code | string | A stable UB-<AREA>-<NNN> code identifying this exact failure — see below. |
| request_id | string | Identifies the request; also on the x-generation-id header. Quote it in support tickets. |
error_code is always shaped UB-<AREA>-<NNN> — a short uppercase area token (e.g. GW for the gateway itself) and a three-digit number. The same code always means the same thing everywhere it appears, independent of which endpoint returned it. It exists so a specific failure can be identified precisely without exposing how the platform is built internally — message is deliberately generic for a server-class (5xx) failure, so error_code is often the more precise signal in that case.
When you contact support about a failed request, quote both error_code and request_id — together they let us find the exact failure without you needing to know (or us needing to explain) what produced it internally.
Error types
The full taxonomy. HTTP status codes are standard, so status-based SDK exceptions (a 429 raising RateLimitError, a 401 raising AuthenticationError) work unchanged.
| type | HTTP | When it happens | Retry? |
|---|---|---|---|
| authentication | 401 | Missing, malformed, expired or revoked key. | No — fix the key |
| permission_denied | 403 | Key or role lacks access; IP outside the CIDR allowlist; model blocked by workspace policy. | No |
| content_policy_violation | 403 | Blocked by a content guardrail. | No |
| insufficient_balance | 402 | Workspace ₹ balance can't cover the request's prompt, or an earlier request left an outstanding amount (UB-GW-295) that a top-up must clear. | No — top up |
| budget_exceeded | 402 | A workspace or org budget cap was hit. | No |
| payment_required | 402 | This key's own spend cap was hit. | No |
| invalid_request | 400 | Malformed body, bad parameters, or invalid JSON. | No — fix the request |
| context_length_exceeded | 400 | Prompt exceeds the model's context window. | No — shorten / middle-out |
| max_tokens_exceeded | 400 | Reserved in the error taxonomy; unoblox does not emit it today and never limits or changes your max_tokens itself. | No |
| not_found | 404 | Unknown model, or no endpoints serve it. | No |
| rate_limit_exceeded | 429 | Per-key or workspace RPM exceeded. | Yes — after Retry-After |
| provider_unavailable | 503 | Upstream unreachable, or a fail-closed BYOK credential. | Yes — backoff |
| provider_overloaded | 503 | Upstream at capacity. | Yes — backoff |
| no_capacity | 503 | No eligible or healthy endpoints right now. | Yes — backoff |
| timeout | 504 | Upstream did not respond in time. | Yes — backoff |
| server | 500 | Internal gateway error. | Yes — backoff |
| unmapped | 500 | Unclassified internal error. | Yes — backoff |
Handling errors
import os
from openai import OpenAI, APIStatusError
client = OpenAI(
base_url="https://api.unoblox.ai/v1",
api_key=os.environ["UNOBLOX_API_KEY"],
)
try:
resp = client.chat.completions.create(
model="<model-id>", # browse /models for a real one
messages=[{"role": "user", "content": "Hello"}],
)
except APIStatusError as e:
err = e.response.json().get("error", {})
# Standard HTTP status codes — branch on these for portable handling.
if e.status_code == 402:
raise SystemExit(f"payment required: {err.get('type')}") # top up / raise a cap
elif e.status_code == 429:
retry_after = float(e.response.headers.get("retry-after", "1"))
... # back off retry_after seconds, then retry
elif e.status_code >= 500:
... # transient upstream/server — retry with exponential backoff
else:
raise # 400/401/403/404 — a client error; fix the requestThree ways a request can be refused for money
All three are 402 but carry distinct type strings so you can respond correctly:
| type | Cause | Fix |
|---|---|---|
| insufficient_balance | The workspace prepaid ₹ balance is too low. | Top up credits. |
| budget_exceeded | A workspace/org budget ceiling was reached. | Raise or reset the budget. |
| payment_required | The API key's own spend cap was reached. | Raise the key's limit_minor, or wait for its window to reset. |
A key spend-cap refusal also sets x-unoblox-refusal: spend_limit_exceeded and, unlike the other two, its message never states a figure. An insufficient_balance message DOES state your available balance (see the envelope example above) — it is not a generic message.
Errors on the compatibility skins
The native endpoint returns the unoblox type vocabulary above. The compatibility skins reshape the same underlying error into the envelope their SDK expects:
/v1/responses — OpenAI shape (a param field; a string code):
{ "error": { "message": "…", "type": "insufficient_quota", "param": null, "code": "insufficient_quota" } }/v1/messages — Anthropic shape:
{ "type": "error", "error": { "type": "billing_error", "message": "…" } }The HTTP status is the same across all three surfaces; only the JSON shape and the type strings differ.
Errors mid-stream
Once a streamed response has started, its HTTP status is already 200. A failure after that point arrives as a terminal error event in the stream rather than an HTTP status — see Streaming.
Analytics API errors
The management-key-authenticated Analytics API (analytics query/meta, credits balance, usage export) adds four codes of its own:
| error_code | Status | Cause |
|---|---|---|
| UB-API-381 | 401 | The management key does not admit at all — malformed/unknown/wrong-class, dead (disabled/revoked/retired/deleted/expired), missing the required usage operation grant, or outside its own CIDR allowlist. One undifferentiated code, deliberately, so a probe can't learn which reason applied. |
| UB-API-382 | 403 | The key is otherwise fully valid, but the URL names a workspace other than the key's own. |
| UB-API-383 | 403 | An inference (ub-gw-…) key was presented to credits/balance or usage/activity/export — those two accept a session or a management key only. |
| UB-API-414 | 403 | A session or management key's workspace role doesn't permit the dimension it asked for — member needs MembersView, api_key needs ApiKeysView. |
Retry only what is retryable.
Retry 429, 503 and 504 with exponential backoff, using a fresh Idempotency-Key for each attempt — reusing the same one gets the retry itself refused with a 400, not a replayed response. Do not retry a 4xx — fix the request or the account first. See Rate limits & idempotency.