Skip to content

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

429 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 404 has 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 example unknown_provider, invalid_attribution, unknown_fallback_model, ambiguous_model, state_too_large or batch_too_large on a 400; key_budget, organization_budget or subscription_required on a 402; model_not_allowed, route_not_allowed, project_archived, key_disabled, organization_suspended, ip_not_allowed, guardrail_blocked or guardrail_unavailable on a 403; version_conflict, key_not_rotatable or environment_in_use on a 409; provider_rate_limited on a 429 your own provider returned; no_provider_connected, connection_out_of_scope or model_not_served on a 503. Often null.

Status codes

StatusTypeWhen it occursRetry
400invalid_request_errorMalformed 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 capNo
401authentication_errorMissing, malformed, revoked, or expired API keyNo
402budget_exceededA 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 priceNo
402budget_exceededSoftware entitlement unavailable; code: subscription_requiredNo
402budget_exceededThe charge at the end of a trial failed and its three-day grace ended; code: trial_payment_failedNo
403permission_deniedThe 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 requestNo
404model_not_foundThe model ID does not exist in the catalogNo
404not_foundManagement 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_foundNo
408request_timeoutEvery 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 fallbackYes
409conflictManagement 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 usageAfter re-reading
413request_too_largeThe 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 MBNo
429rate_limit_exceededPer-key request rate exceeded; the response includes a Retry-After headerYes
429rate_limit_exceededEvery 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 connectionAfter Retry-After
429rate_limit_exceededToo many requests with an invalid or unrecognised API key from one address; code: invalid_api_key_rate_limited. Fix the key before retryingAfter Retry-After
500internal_errorA 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 providerNo
502upstream_errorEvery 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 wayYes
503no_available_providerNo 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 belowOnly 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.

StatuscodeWhen it occurs
400if_match_required, invalid_if_matchA PATCH or DELETE without a valid If-Match: "<version>" header
400idempotency_key_reuseAn Idempotency-Key reused with a different request body
400unknown_resource_type, operation_not_supportedA 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
403insufficient_scope, insufficient_role, plan_feature, project_restricted, token_creator_not_adminThe 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
403mcp_disabledMCP server only: an owner or admin turned off AI assistant access for the organization. See AI assistants
400confirmation_mismatchMCP server only: revoke_gateway_key was given a confirm_label that does not equal the key's current label; nothing was revoked
404resource_not_foundNo resource with that ID that this token can see
409version_conflict, project_has_usageSee 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.

StatuscodeWhen it occurs
404mcp_server_not_foundMCP 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)
429mcp_server_rate_limitedMCP gateway only: the MCP server's own calls-per-minute limit, across every key, with Retry-After
400invalid_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_unconfirmedConfiguring 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 503 with code no_provider_connected, connection_out_of_scope or model_not_served, and a 429 with code invalid_api_key_rate_limited, which need a configuration fix first.
  • On 429, honor the Retry-After header over your computed delay — it says exactly when capacity frees up. The X-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

Python
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:

Mid-stream error event
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.

↑ ↓ navigate↵ open/ anywhere in docs