DocsAPI reference
Errors
Every INFRO error uses one JSON shape. Status codes and type strings, which failures to retry with backoff, and how errors surface mid-stream.
INFRO is in private early access. Running these examples requires an early-access account, an active trial or subscription entitlement, and your own provider connection. Self-serve purchasing is not generally open. See Early access.
Every API error is JSON with a single top-level error object and a meaningful HTTP status code. The status tells you whether a retry can help; the type string is stable and safe to branch on. The shape is identical across every endpoint, including chat completions.
Because INFRO fails over between your connections and route targets automatically, a failure on one of them does not reach you while another can serve. What does surface falls into the small set of statuses below.
Error response body
{
"error": {
"message": "Rate limit exceeded: 200 requests per minute for this key.",
"type": "rate_limit_exceeded",
"code": null
}
}error.messagestringrequired- Human-readable description of the failure. Useful in logs; do not branch on it — the wording can change.
error.typestringrequired- Stable, machine-readable category. One type per status code — see the table below — except on the management API, where a
404has its own type. Branch on this or on the HTTP status. error.codestring | nullrequired- A narrower machine-readable code inside the
type, when one exists — for exampleunknown_provider,invalid_attribution,unknown_fallback_model,ambiguous_model,state_too_largeorbatch_too_largeon a 400;key_budget,organization_budgetorsubscription_requiredon a 402;model_not_allowed,route_not_allowed,project_archived,key_disabled,organization_suspended,ip_not_allowed,guardrail_blockedorguardrail_unavailableon a 403;version_conflict,key_not_rotatableorenvironment_in_useon a 409;provider_rate_limitedon a 429 your own provider returned;no_provider_connected,connection_out_of_scopeormodel_not_servedon a 503. Often null.
Status codes
| Status | Type | When it occurs | Retry |
|---|---|---|---|
| 400 | invalid_request_error | Malformed JSON, invalid message structure, an oversized uploaded file inside a multipart body, a routing.providers entry that is not a provider id (unknown_provider, naming it), the same provider in both allow and deny (invalid_provider_selection), invalid attribution metadata (invalid_attribution), a fallbacks entry that is not a model you can call (unknown_fallback_model, naming the entry), a bare model name that matches more than one of your connected vendors (ambiguous_model), or a request with no end-customer id when an end-customer rule requires one (end_user_id_required). Also returned when every provider tried rejected the request as invalid (their 400 or 422); the message then starts Provider rejected the request: and carries the provider's own message and param, with control characters removed, anything shaped like a credential redacted, and a 500-character cap | No |
| 401 | authentication_error | Missing, malformed, revoked, or expired API key | No |
| 402 | budget_exceeded | A budget you set has no room for this request. code names it: key_budget, key_lifetime_limit, project_budget, member_budget or organization_budget; end_user_budget when the end customer in metadata.user_id has used its own cap (the error then also carries scope, end_user_id, period and resets_at); unpriced_model_under_budget when a blocking budget applies and the model has no price | No |
| 402 | budget_exceeded | Software entitlement unavailable; code: subscription_required | No |
| 402 | budget_exceeded | The charge at the end of a trial failed and its three-day grace ended; code: trial_payment_failed | No |
| 403 | permission_denied | The model is outside an organization, project or key allowlist (code is model_not_allowed; the message names which), the key is pinned to routes and called something else (route_not_allowed), the key belongs to an archived project (code is project_archived; restoring the project makes the key work again), the key was disabled (code is key_disabled; enabling it makes the same key work again), the key has an IP allowlist and the request came from outside it (code is ip_not_allowed; not retryable from the same address), INFRO has suspended the organization's access (code is organization_suspended; the body carries a support_url and the remedy is to contact support@infro.io, not to change a setting or a plan), the organization's guardrails refused the request or the response (code is guardrail_blocked), the guardrails could not finish checking it and the policy fails closed (code is guardrail_unavailable), or content moderation blocked the request | No |
| 404 | model_not_found | The model ID does not exist in the catalog | No |
| 404 | not_found | Management API only (/manage/v1): no resource with that ID, or one this token may not see. code is resource_not_found. Also POST /v1/decisions/{slug} when no enabled decision policy of that slug may be called by this key: code is decision_policy_not_found | No |
| 408 | request_timeout | Every connection tried timed out before responding. Mid-stream, the same type with code stream_stalled means the provider went silent. Code decision_deadline: a decision's deadline passed and its policy has no fallback | Yes |
| 409 | conflict | Management API only (/manage/v1). code is version_conflict when the resource changed since the version named in If-Match (the current version is in the ETag header), or project_has_usage when ?purge=true names a project that has gateway keys or usage | After re-reading |
| 413 | request_too_large | The request body is larger than the endpoint accepts (code is request_too_large). Checked before the body is parsed. Limits: chat completions 28 MB, image edits 51 MB, audio transcriptions 26 MB, videos 11 MB, every other endpoint 1 MB | No |
| 429 | rate_limit_exceeded | Per-key request rate exceeded; the response includes a Retry-After header | Yes |
| 429 | rate_limit_exceeded | Every connection tried failed and at least one of them was rate-limited by your own provider account; code: provider_rate_limited. Retry-After carries the provider's own value (capped at 300 seconds) when it sent one, and is absent when it did not. A provider 429 never counts toward ejecting the connection | After Retry-After |
| 429 | rate_limit_exceeded | Too many requests with an invalid or unrecognised API key from one address; code: invalid_api_key_rate_limited. Fix the key before retrying | After Retry-After |
| 500 | internal_error | A defect on INFRO's side. Not an upstream failure — an identical retry will usually fail identically, so it is reported as ours rather than blamed on a provider | No |
| 502 | upstream_error | Every connection or route target tried for the requested model returned an error; the message names each connection tried with the reason it failed: the provider's HTTP status and its own error message, cleaned and with anything shaped like a credential redacted. A render that fails after it was accepted carries the provider's reason the same way | Yes |
| 503 | no_available_provider | No connection is currently able to serve the requested model. code is no_provider_connected when the organization has connected no provider that serves it at all, connection_out_of_scope when a connection exists but is out of reach (for example scoped to another environment than the key), and model_not_served when every connection tried answered that its key cannot serve the model (for example a LaoZhang token outside the group that carries it); the message says which — see below | Only without a code |
503 with code: "no_provider_connected" is not an outage. It means this organization has not connected a provider account that can serve the model you asked for, so there is nowhere for the request to go. The message names the provider to connect. It shares the 503 status with the transient case deliberately — from the caller's side both mean nothing can serve this right now — but this one will not resolve on a retry, so branch on code before applying backoff. The same applies to connection_out_of_scope: fix the connection's environment, the key's environment, or re-enable the connection. And to model_not_served: the message names each connection and its provider's own words; give that key access to the model on the provider's side (for LaoZhang, a token group that serves it), or connect another provider.
For 404, confirm the model ID against GET /v1/models or the models page — IDs use the vendor/model-name form, like anthropic/claude-sonnet-5.
An unrecognised top-level parameter is not a 400. INFRO forwards request fields it does not know about, and drops the ones the serving model cannot use. Model APIs gain parameters continuously, and a gateway that rejected everything it had not heard of would break your build every time you upgraded an SDK. The cost of this choice is that a typo in an optional field is ignored rather than reported, so treat GET /v1/requests/{id} as the check on what actually reached the model.
Response cache codes
Three 400 codes (type invalid_request_error) belong to the response cache: cache_ttl_too_long when cache.ttl_seconds, X-INFRO-Cache-TTL or a default TTL is above the plan's maximum; invalid_cache_control when an X-INFRO-Cache* header is malformed; and response_cache_unavailable when turning the cache on in settings while the cache store is not available to your organization. None is retryable unchanged.
Management API errors
The management API (/manage/v1) uses the same envelope and adds two types that appear nowhere else. A 404 there has type not_found and code: "resource_not_found", because it names a project, key or route by ID rather than a model; the same answer is given for a resource that belongs to a project a project-restricted token cannot see. A 409 has type conflict: version_conflict means the resource changed since you read it, so read it again (the ETag header carries the current version) and decide before retrying; project_has_usage means a project cannot be purged and should be archived instead.
| Status | code | When it occurs |
|---|---|---|
| 400 | if_match_required, invalid_if_match | A PATCH or DELETE without a valid If-Match: "<version>" header |
| 400 | idempotency_key_reuse | An Idempotency-Key reused with a different request body |
| 400 | unknown_resource_type, operation_not_supported | A resource type the API does not serve, or an operation that resource does not allow |
| 401 | — | Missing, unknown, expired or revoked management token, or a gateway key sent to /manage/v1 |
| 403 | insufficient_scope, insufficient_role, plan_feature, project_restricted, token_creator_not_admin | The token's scopes, its creator's current role, the organization's plan, or the token's project restriction does not allow the request. See Management tokens |
| 403 | mcp_disabled | MCP server only: an owner or admin turned off AI assistant access for the organization. See AI assistants |
| 400 | confirmation_mismatch | MCP server only: revoke_gateway_key was given a confirm_label that does not equal the key's current label; nothing was revoked |
| 404 | resource_not_found | No resource with that ID that this token can see |
| 409 | version_conflict, project_has_usage | See above |
| 429 | — | More than 120 requests a minute on one token, or too many failed authentications from one address |
Any other 400 or 403 comes from the same validation the console applies to that change, with the same message and code.
MCP gateway errors
The MCP gateway answers a refused tool call with a tool result (isError: true) whose structuredContent.error has the same type, code and message as this envelope, so the model can read it. The new tool-result codes: tool_not_allowed, tool_not_approved and tool_definition_changed (type permission_denied); invalid_tool_arguments (invalid_request_error); tool_result_too_large, tool_result_invalid, upstream_unavailable and upstream_rejected (upstream_error); upstream_timeout (request_timeout). guardrail_blocked and guardrail_unavailable keep their meaning.
| Status | code | When it occurs |
|---|---|---|
| 404 | mcp_server_not_found | MCP gateway only, type not_found: the per-server path names no server this key can reach (unknown, disabled, out of scope or not granted; one answer for all) |
| 429 | mcp_server_rate_limited | MCP gateway only: the MCP server's own calls-per-minute limit, across every key, with Retry-After |
| 400 | invalid_mcp_server_url, invalid_mcp_credential, mcp_discovery_failed, mcp_server_slug_taken, mcp_tool_name_clash, mcp_tool_definition_stale, mcp_tool_removed, mcp_tool_not_approved, mcp_trifecta_unconfirmed | Configuring MCP servers, tools and grants |
Every code, the HTTP refusals the gateway shares with /v1 and its JSON-RPC errors are on MCP gateway errors.
Retry rules
- Retry 408, 429, 502, and 503 with exponential backoff and jitter. These are transient: a later attempt can land on a healthy connection or a fresh rate-limit window. The exceptions are a
503with codeno_provider_connected,connection_out_of_scopeormodel_not_served, and a429with codeinvalid_api_key_rate_limited, which need a configuration fix first. - On 429, honor the
Retry-Afterheader over your computed delay — it says exactly when capacity frees up. TheX-RateLimit-*headers on every response are covered in Rate limits. - Never retry 400, 401, 402, 404, or 413. They are deterministic — the same request fails the same way. Fix the request, the key, or the model ID; for 402, inspect the code and resolve the subscription or the named budget in the console.
403 sits in between: retrying the identical request will not help, but if provider content moderation was the cause, rewording the content might. Treat it as non-retryable in automated code.
Automatic provider failover
When a connection returns an error or times out before the first token, INFRO moves the request to the next target on the route, or the next of your connections that serves the model. The response names the provider and connection that served it, and route tells you how far down the chain it went — primary when the first served, standby_a when the second did. You only see a 502, 408 or provider 429 when every connection tried failed, and a 503 when none is available at all. Refusals INFRO makes itself (budgets, allowlists, plan limits) are not retried on another connection.
Failover between connections is automatic. For model-level resilience, pass an ordered fallbacks list so the request can complete on a different model when the primary is fully down — see Fallbacks; fallbacks is ignored when model names a route. To choose which connections serve a request and in what order, call a route by name. To choose only which of your providers may serve one request, send routing.providers ({"allow": ["aws-bedrock"]} or {"deny": ["openrouter"]}); it narrows the route's targets, the model's connections and the fallbacks alike, and never widens what your key allows.
A 502 means INFRO already tried every connection that could serve that model, so an immediate retry rarely helps. Back off first, or configure fallbacks so the request lands on another model instead of failing.
Retrying with backoff
import os
import random
import time
import requests
RETRYABLE = {408, 429, 502, 503}
def chat(payload: dict, max_retries: int = 5) -> dict:
headers = {"Authorization": f"Bearer {os.environ['INFRO_API_KEY']}"}
for attempt in range(max_retries + 1):
resp = requests.post(
"https://api.infro.io/v1/chat/completions",
headers=headers,
json=payload,
timeout=120,
)
if resp.ok:
return resp.json()
if resp.status_code not in RETRYABLE or attempt == max_retries:
resp.raise_for_status()
# Honor Retry-After on 429; otherwise exponential backoff with jitter
retry_after = resp.headers.get("Retry-After")
if retry_after:
delay = float(retry_after)
else:
delay = min(2 ** attempt, 30) + random.uniform(0, 1)
time.sleep(delay)
completion = chat({
"model": "openai/gpt-5.6-luna",
"messages": [{"role": "user", "content": "Ping"}],
})
print(completion["choices"][0]["message"]["content"])Errors mid-stream
Failover and fallbacks only apply before the first token — once a provider begins streaming, it cannot be swapped out. If it drops mid-stream, the SSE stream delivers an event whose payload is the standard error object instead of a chat.completion.chunk:
data: {"error": {"message": "The response stream ended before it completed.", "type": "upstream_error", "code": null}}The stream then closes without a terminating data: [DONE], and if you requested usage via stream_options, the final usage chunk never arrives. Treat mid-stream errors as retryable: keep or discard the partial output as your application requires, then reissue the request with your normal backoff. The exception is a mid-stream error with code: "guardrail_blocked" or "guardrail_unavailable": an output guardrail stopped the response, and the same request will be stopped the same way. The full event format is covered in Streaming.
All 402 responses currently use error.type: "budget_exceeded". Inspect error.code to distinguish a configured budget (key_budget, key_lifetime_limit, project_budget, member_budget, organization_budget, end_user_budget, unpriced_model_under_budget) from subscription_required and trial_payment_failed; the remedy is different even though the HTTP status and type are shared.