polyrouter speaks the OpenAI and Anthropic wire protocols, so any SDK that takes a base URL and a key works unchanged. This is the exact HTTP contract — endpoints, auth, the routing-control header, streaming and fallback behavior, error kinds, and the dashboard's management REST API.
Base URLs & auth
Mint a key in the dashboard (Agents → New agent — it looks like poly_… and is shown once). An OpenAI client points at /v1 and sends a Bearer token; an Anthropic client points at the root and sends x-api-key (it appends /v1/messages itself). The same poly_… key is accepted either way.
NoteThe raw endpoints are /v1/chat/completions, /v1/messages, and /v1/models. An OpenAI client → Anthropic provider (and the reverse) is translated through polyrouter's intermediate representation — tool calls, system prompts, stop reasons, and usage carry across, locked by a golden-file contract suite. Protocol-specific controls (cache_control, thinking, output_config on Anthropic; response_format, reasoning_effort on OpenAI; image_url.detail) are carried opaquely on their own protocol and deliberately dropped — never mis-mapped — when crossing to the other.
POST /v1/chat/completions
The OpenAI-compatible endpoint. The body is a standard Chat Completions request — a model, a messages array, and the OpenAI parameters polyrouter recognises (temperature, tools, response_format, …). Set stream: true for token streaming. The output cap may be sent as either max_tokens or max_completion_tokens — polyrouter accepts both inbound and emits the spelling each provider needs.
The recognised fields are model, messages, tools, tool_choice, parallel_tool_calls, temperature, top_p, max_tokens/max_completion_tokens, stop, stream, stream_options, response_format, reasoning_effort and n. Anything else (frequency_penalty, presence_penalty, seed, logprobs, logit_bias, user, store, service_tier, prediction, …) is silently dropped rather than forwarded. n > 1 is rejected with a 400 — the router always returns exactly one choice.
The model field is where routing begins. It takes four forms:
Form
Example
Resolves to
Direct model id
gpt-5
That exact model on the single provider that exposes it — matched against your whole model catalog, independently of tiers, with no fallback chain (a single target). If two providers expose the same id the request is a 404 ambiguous_model — qualify it as <providerId>:<model>. If the string matches no model but is a tier key, it resolves as that tier.
Provider-prefixed
<providerId>:gpt-5
That exact provider's copy of the model. <providerId> is the provider row's UUID — call GET /v1/models for the exact qualified ids.
Tier name
fast
The named tier's ordered chain — primary entry, then up to four fallbacks.
Auto alias
auto
The enabled smart layers (L1 structural, then opt-in L2 semantic and L3 cascade), degrading to the default tier.
WORKED EXAMPLE · streaming
curl https://<your-instance>/v1/chat/completions \
-H "Authorization: Bearer poly_your_key" \
-H "Content-Type: application/json" \
-d '{"model":"auto","stream":true,"messages":[{"role":"user","content":"Summarize this diff in two sentences."}]}'
TipA tier key or auto resolves to an ordered chain and gets fallback on provider failure; a directly-named model has none — its failure is returned to you. Budgets are enforced and the decision (decision_layer + human-readable routing_reason) is recorded for the inspector either way. See Routing for the full layered pipeline.
POST /v1/messages
The Anthropic-compatible endpoint. The body is a standard Messages request — a model, a required max_tokens, a messages array, and an optional system. Set stream: true for SSE. An Anthropic SDK targets the root base URL and appends /v1/messages itself; the same routing forms in the model field apply here too.
WORKED EXAMPLE · max_tokens
curl https://<your-instance>/v1/messages \
-H "x-api-key: poly_your_key" \
-H "Content-Type: application/json" \
-d '{"model":"claude-sonnet-5","max_tokens":1024,"messages":[{"role":"user","content":"Write a haiku about load balancers."}]}'
NoteRoute this request to an OpenAI-compatible provider and polyrouter translates it end to end — Anthropic's cache_control markers are carried opaquely and dropped when crossing to OpenAI; multi-turn tool loops preserve round-trip semantics.
GET /v1/models
Returns everything routable by your key in the OpenAI list shape, so clients that fetch a model list for validation or pickers work unchanged. Authenticate exactly as for chat completions. The list is auto, then every tier key (always including default), then <providerId>:<model> for every model, plus the bare model id when it is unique across your providers. Every entry is {id, object:"model", owned_by:"polyrouter"} — no created, no permission.
Two mechanisms steer routing without hard-coding a concrete model. Send the x-polyrouter-tier header to pin a tier, and/or set "model":"auto" in the body to engage the smart layers.
Mechanism
Effect & precedence
x-polyrouter-tier: <tier>
request header
Pins that tier's chain. The highest-precedence header — it beats every other header rule regardless of their priority, but still yields to a concrete model in the body.
"model": "auto"
body field
Engages the enabled smart layers — but only for a request that Layer 0 already landed on the default tier. A concrete model, the tier header, or a rule on any other header preempts it entirely; the layers then refine that default decision (L1 structural, then opt-in L2 semantic and L3 cascade).
NoteLayer 0, first match wins: (1) a concrete model in the body, (2) x-polyrouter-tier, (3) a rule on any other header (priority desc, then oldest), (4) a default-match rule, (5) the seeded default tier. "model":"auto" is not a precedence step — the smart layers refine a request that already landed on default. Above, the tier header matches at phase 2, so the request routes to fast and the smart layers never run.
Streaming & fallback semantics
Set "stream": true and the response is a text/event-stream of SSE frames in the wire format of the client's protocol.
Send stream_options: {"include_usage": true} to receive the terminal usage chunk (a final frame with choices: []). polyrouter always requests usage upstream so its own cost records are exact, but only relays that chunk when you asked for it — without the flag an OpenAI client gets no usage at all. Anthropic clients always get usage.
message_delta carries stop_reason, stop_sequence and the terminal output_tokens.
CommitFallbacks run freely before the first token — polyrouter can walk the tier's chain (and, under cascade, buffer and grade the cheap tier) with no client-visible effect. Once the first token has been sent the model is committed: a mid-stream upstream failure ends the stream with a clear error, and the model is never silently swapped mid-response.
The HTTP status stays 200 — headers were flushed before the failure, so the error arrives in-band. An OpenAI client receives data: {"error":{"message":"the upstream model failed mid-stream","type":"upstream_error","code":null}} followed by data: [DONE]; an Anthropic client receives an event: error frame with error.type: "api_error" and no message_stop. Check for an error frame rather than trusting the status code.
How long polyrouter waits is per-provider: first_byte_timeout_ms bounds time-to-first-token and — plus PROXY_EVENT_TIMEOUT_MARGIN_MS (500 ms) — the gap between streamed events, so this is the knob to raise for slow streaming models. idle_timeout_ms bounds the inter-chunk gap on non-streaming (buffered) reads only; raising it can never keep a slow stream alive. Each is 1 s–1 h; unset inherits the instance defaults PROXY_FIRST_EVENT_TIMEOUT_MS / PROXY_IDLE_TIMEOUT_MS, both 30 s. Raise the first-byte bound for research-class models — Deep Research, or Opus with thinking — whose long prefill would otherwise 503 and trip the provider's circuit breaker.
Errors
When a provider in the resolved tier's chain fails, polyrouter classifies the error. Every provider kind except bad_request is fallback-eligible — a fault at one provider should not fail the request, so the chain walks to the next entry. A terminal error returns immediately. Fallback only applies before the response commits — see Streaming.
Error kind
Class
On failure
auth
fallback-eligible
Try the next entry in the chain.
rate_limit
fallback-eligible
Try the next entry in the chain.
unavailable
fallback-eligible
Try the next entry in the chain.
unknown_model
provider
fallback-eligible
Try the next entry in the chain — a model retired at one provider must not fail the request. Breaker-neutral: the provider is healthy, only the model is gone.
credential
fallback-eligible
A revoked OAuth grant or an identity-provider outage. Try the next entry in the chain; breaker-neutral. If the whole chain is exhausted: 503, type: "api_error", code: "upstream_credential".
circuit open
fallback-eligible
The entry is skipped before an adapter is even built. If every entry in the chain is open the request ends 503.
bad_request
terminal
The only fallback-ineligible provider kind — the request itself is at fault, so another provider would reject it too. Returned immediately as a 400.
unknown_model
routing
terminal
A different error with the same name: the model field named nothing in your catalog and no tier key, so there is no chain to walk. 404, code: "model_not_found".
NoteTwo more cases never retry: your own client aborting the call, and any throw the router cannot classify — an unrecognised failure is the one thing that is never walked past, and it returns 500 internal proxy error. When the chain is exhausted or the error is terminal, the request fails and the terminal provider error detail — which provider, what kind — is recorded on the row in the dashboard's Requests inspector. A failed request is never dressed up as a silent success.
Error response shape
Every /v1 failure is rendered in the caller's own envelope with a fixed, sanitized message — never the raw upstream body, request id, or credential.
The upstream rejected the request — terminal, never retried.
400
empty_tier
The resolved tier has no models configured.
400
unresolved_target
The routing target could not be resolved.
401
invalid_api_key
The poly_… key is missing, malformed, or revoked.
402
budget_exceeded
A block budget is at or over its threshold. The message names the budget and its reset time, so you know when to retry.
404
model_not_found
The model field matched nothing in your catalog and no tier key.
404
ambiguous_model
Two providers expose that id — qualify it as <providerId>:<model>.
413
request_too_large
The body exceeded PROXY_MAX_BODY_BYTES (10 MiB default).
429
rate_limited
Every provider in the chain was rate limited.
500
no_default
No default tier is configured — the catch-all is missing.
502
upstream_auth
Every provider in the chain rejected polyrouter's credential.
503
upstream_unavailable
Every provider was down, timed out, or had an open breaker.
503
upstream_credential
A subscription credential could not be resolved — usually a revoked OAuth grant needing reauthorization in the dashboard.
503
budget_enforcement_unavailable
The budget check could not be trusted (Redis fault or stale reconciliation) and the operator chose to fail closed. See BUDGET_FAIL_OPEN.
A 500 with a null code is the unclassified-throw case — internal proxy error. Anthropic envelopes carry type and message only; the code column above appears in the OpenAI envelope.
Management REST API
The dashboard is driven by a REST API under /api/*. These use dashboard session auth (the Better Auth session cookie from signing in) — notpoly_… agent keys, which authenticate only the inference endpoints under /v1. Disabling a user cuts both planes at once.
Endpoint
Purpose
/api/agents
Agent API keys — mint poly_…, rotate (POST :id/rotate-key), and delete (DELETE :id, which revokes the key immediately). Disabling the owning user cuts every one of their keys at once.
/api/providers
Provider CRUD, plus POST :id/test-connection and POST :id/sync-models; the subscription OAuth wizard lives under /oauth/*.
/api/models
The dashboard's model catalog — GET and PATCH only; records are created by provider sync, never by hand. Distinct from the agent-facing GET /v1/models.
/api/routing/tiers
Named routing targets and their ordered entry chains.
/api/routing/tiers/:tierId/entries
GET returns the ordered chain; PUT replaces it atomically (position 0 = primary, up to four fallbacks). There is no per-entry create or delete.
/api/routing/rules
Header-based routing rules, matched in priority order.
/api/routing/auto-layers
Read/set the enabled smart layers; reports semanticAvailable honestly.
/api/routing/semantic-learning
Layer-2 learning /status and one-click /revert.
/api/analytics
Usage, cost breakdowns, band mix, cascade outcomes, net-savings.
/api/budgets
Budgets — day/week/month, global or per-agent, alert-or-block.
/api/notification-channels
SMTP and Apprise channels for alerts and summaries, plus POST :id/test.