API Overview
Choose the right AnyRouter inference surface — Chat Completions, Messages, Responses, Embeddings — plus catalog and status endpoints.
AnyRouter exposes four core inference surfaces — Chat Completions, Messages, Responses, and Embeddings — plus catalog and account endpoints. Most OpenAI-compatible tooling works by overriding the base URL and API key; Anthropic-compatible tooling uses the Messages endpoint.
The full OpenAPI spec is browsable at /docs/api — every endpoint with live request/response samples and a "try it" runner.
Base URL
https://anyrouter.dev/api/v1OpenAI-compatible clients that set the base URL to https://anyrouter.dev (and append /v1/... themselves) also work: /v1/models and /v1/chat/completions are the same API as /api/v1/....
All endpoints are versioned under /v1. Breaking changes bump the version (/v2) without removing older versions for a minimum 12-month deprecation window.
Choose an endpoint
Chat Completions — OpenAI-compatible chat, streaming, tool calls, and broad cross-provider routing.
Messages — Anthropic-compatible clients such as Claude Code or the Anthropic SDK.
Responses — modern agent and tool workflows that need structured output items.
Embeddings — semantic search, clustering, and classification.
Rerank — sort documents by relevance to a query for search and RAG.
Document Parse — turn a PDF, Word file or image into Markdown with your own Nace key.
Moderations — classify text for harmful content.
Decisions — TypeSafe typed decisions (noul / choice / score), not chat.
OCR — extract text from images and documents as Markdown.
Models — discovery, context windows, capabilities, pricing, and provider routes.
Health — live status, checked_at, and per-component availability.
Authentication: which key for which endpoint
AnyRouter uses two distinct API key families. Pick the right one for the endpoint you're calling.
| Key family | Prefix | Use for | Get one at |
|---|---|---|---|
| LLM API key | sk-ar-v1- | Inference: /chat/completions, /messages, /responses, /embeddings, /rerank, /moderations, /decisions (/systemone is the same handler). Also reads /models, /generation/:id, /auth/key. | /dashboard/keys |
| Management API key | ak_ | Account administration: /keys (LLM-key CRUD), /presets, /auth/byok, /credits, /management-keys. | /dashboard/settings#keys |
The two families do not cross over. Sending an ak_ key to /chat/completions returns 401; sending a sk-ar-v1- key to /api/v1/keys returns 401 with a hint pointing you here.
flowchart LR
App["Your app"]
App -->|"sk-ar-…"| LLM["LLM API key"]
App -->|"ak_…"| Mgmt["Management API key"]
LLM --> Inference["Inference<br/>POST /chat/completions<br/>POST /messages<br/>POST /responses<br/>POST /embeddings<br/>POST /rerank<br/>POST /moderations<br/>POST /decisions"]
LLM --> Read["Read-only<br/>GET /models<br/>GET /generation/:id<br/>GET /auth/key"]
Mgmt --> Admin["Account admin<br/>/keys (CRUD)<br/>/management-keys<br/>/presets<br/>/auth/byok<br/>/credits"]
classDef key fill:#fef3c7,stroke:#f59e0b,stroke-width:1px,color:#92400e
classDef endpoint fill:#f1f5f9,stroke:#cbd5e1,color:#0f172a
class LLM,Mgmt key
class Inference,Read,Admin endpointQuick decision rule:
- I want a model to answer a prompt →
sk-ar-LLM key. - I want to create, rotate, or revoke keys / manage BYOK / read balances →
ak_management key.
See Management API Keys for the full scope catalog and management lifecycle.
Common headers
| Header | Required | Description |
|---|---|---|
Authorization: Bearer <key> | Yes | LLM key (sk-ar-…) on inference routes; management key (ak_…) on account routes. |
Content-Type: application/json | POST | Required on POST endpoints. |
x-api-key | Messages | Anthropic-compatible auth header for /messages (accepts sk-ar-…). |
anthropic-version | Messages | Required by Anthropic-compatible Messages clients. |
X-AnyRouter-Provider | No | Force routing to a specific upstream provider. |
X-AnyRouter-Balance-After-Debit: true | No | Ask for this request's charge and the workspace balance after it. The response waits up to 500 ms for the charge to be applied, so leave it off unless you read the values. |
X-AnyRouter-Trace-Id | Response | Canonical trace id on every inference response. Paste it into the dashboard logs "Request ID" filter to open the exact call. |
X-Request-ID | Response | Legacy alias of X-AnyRouter-Trace-Id — same value, kept for back-compat. |
X-AnyRouter-Model | Response | Public catalog model id that served the request (including the resolved model for a virtual preset). |
X-AnyRouter-Upstream-Model | Response | Wire model name sent to the selected upstream. |
X-AnyRouter-Provider-Chain | Response | Ordered backend ids tried, joined with ->; contains route ids only, never credentials. |
X-AnyRouter-Credential-Source | Response | platform, own_byok, or pool for the winning attempt. This is a class label, not key material. |
X-AnyRouter-Requested-Model / X-AnyRouter-Fallback-Reason | Response, on model fallback | Sent on /chat/completions only when a different model served the request than the one you asked for (for example through models[]). The reason is one of rate_limited, timeout, model_unavailable, credential_error, upstream_error, or not_attempted. The body carries the same facts as requested_model plus fallback: { used, reason, attempts[] }, on the non-streaming response and, for streams, on the first chunk that carries usage. A stream has no such block if the upstream sends no usage chunk, so treat the two headers as authoritative. requested_model is the resolved canonical id, not the raw @preset/... string (for a preset, the first branch's model). Requests to an anyrouter/* router never get these, because picking a model is the router's job. |
X-AnyRouter-Credits-Balance-At-Admission | Response, when read | Workspace balance observed during paid-inference admission, before the asynchronous debit. It is omitted when the balance gate is skipped (free/BYOK-only). |
X-AnyRouter-Credits-Balance-After-Debit | Response, non-streaming, on request | Workspace balance right after this request's own charge was applied, on /chat/completions, /messages, and /responses. Sent only when the request included X-AnyRouter-Balance-After-Debit: true. Streaming responses carry the same value as anyrouter_metadata.balanceAfterDebit in the final metadata event on /chat/completions, /messages, and /responses. It is omitted for free and BYOK requests, for a retried request that was already charged, and whenever the charge has not settled by the time the response is written — use GET /credits then. Other requests running at the same time can lower the balance further. |
X-AnyRouter-Credits-Charged | Response, non-streaming, on request | Credits this request was charged, read from the billing ledger, so it already reflects the backend that served it, retries, fallbacks, and cache pricing. Sent on /chat/completions, /messages, and /responses only when the request included X-AnyRouter-Balance-After-Debit: true. 0 for free and BYOK requests. Streaming responses carry it as anyrouter_metadata.creditsCharged in the final metadata event. It is omitted for a retried request that was already charged and whenever the charge has not settled by the time the response is written. |
X-AnyRouter-Credits-Balance | GET /credits response | Current authoritative workspace balance returned by the credits read endpoint. |
X-RateLimit-* / RateLimit-* | Response | The per-minute limit and policy for the tier the request hit (free-model is 2400 per minute per key). Go's 5-hour window also reports remaining count and reset. |
Server-Timing | Response | Gateway-vs-upstream latency split in ms on /chat/completions, /messages and /responses: gw is AnyRouter's own work before the first upstream call (auth, routing, admission), upstream is the total time spent inside upstream attempts — repeated once per attempt with a desc naming the backend — and post is AnyRouter's work after the upstream call (response assembly, usage and cost finalization). total is the sum. For a streamed response the header leaves with the first byte, so post covers only what happened before it; the full split also arrives as anyrouter_metadata.timings in the final metadata frame. |
Endpoint summary
| Method | Path | Description |
|---|---|---|
| POST | /chat/completions | Chat completions (OpenAI-compatible) |
| POST | /ocr/extract | OCR — extract text as Markdown |
| POST | /messages | Anthropic Messages passthrough |
| POST | /responses | Responses for structured output and tool workflows |
| POST | /embeddings | Embeddings for search and clustering |
| POST | /rerank | Rerank documents by relevance to a query |
| POST | /documents/parse | Document parse — file to Markdown, your own Nace key |
| POST | /moderations | Moderations — classify text for harmful content |
| POST | /decisions | Decisions TypeSafe decisions (not chat). Preferred path. |
| POST | /systemone | Same handler as /decisions |
| GET | /models | List models |
| GET | /models/:id | Model metadata |
| GET | /models/count | Total model count |
| GET | /generation/:id | Per-request stats (tokens, cost, latency) |
| GET | /providers | List upstream providers |
| GET | /credits | Remaining credit balance |
| GET | /auth/key | Current API key info |
| GET | /health | Gateway + provider status |
Error shape
All errors follow a consistent JSON envelope:
{
"error": {
"code": "payment_required",
"message": "Upstream provider \"cloudflare\" returned 402 — insufficient quota",
"metadata": {
"type": "upstream_error",
"upstream_backend": "cloudflare",
"upstream_status": 402,
"upstream_message": "You exceeded your current quota, please check your plan and billing details."
}
}
}error.code is a stable machine-readable slug. The canonical set:
| HTTP | error.code | Notes |
|---|---|---|
| 400 | bad_request / max_tokens_too_small | Malformed body or invalid parameters. max_tokens_too_small: a reasoning model spent the whole output budget on reasoning and produced no content — raise max_tokens (max_output_tokens on /responses) to 256 or more, or set stream: true. |
| 401 | unauthorized | Missing or invalid API key. |
| 402 | insufficient_balance | Out of credits on a paid model, or a held signup bonus still pending (signup_bonus_pending). Top up at Credits. Upstream BYOK billing failures may also surface as 402 with metadata.type: "upstream_error" — read metadata.upstream_message. |
| 403 | forbidden | Key lacks permission for the resource. |
| 404 | not_found | Unknown model or missing resource. |
| 408 | request_timeout | Upstream did not respond in time. |
| 413 | content_too_large | Request body exceeds the configured limit. |
| 422 | unprocessable_entity | Validation failed semantically. |
| 429 | rate_limit_exceeded / ip_rate_limit_exceeded | Rate limit hit. Honor Retry-After. X-RateLimit-Remaining / X-RateLimit-Reset appear only on the Go 5-hour window limiter — not on the per-minute tier. |
| 500 | internal_server_error | Unexpected AnyRouter failure. |
| 502 | bad_gateway / upstream_exhausted | Upstream 5xx or every fallback failed; see metadata.upstream_attempts. |
| 503 | service_unavailable | Upstream temporarily unavailable. |
When AI Gateway BYOK rejects a call, AnyRouter parses the provider's verbatim message into error.metadata.upstream_message — that is the authoritative reason, not Cloudflare's wrapping text. See Errors for retry recipes and /docs/api for the full interactive reference.
Rate limits
Rate limits are enforced per-key. Every response carries at least:
X-RateLimit-Limit: <requests per minute>
X-RateLimit-Window: 60
X-RateLimit-Tier: <tier name>
RateLimit-Limit: <same as X-RateLimit-Limit>
RateLimit-Policy: <limit>;w=60The per-minute tier does not emit X-RateLimit-Remaining or X-RateLimit-Reset — the edge limiter only reports the cap. Go's 5-hour window does emit Remaining / Reset (and matching RateLimit-* headers). See Rate Limits.
Tracing
POST /chat/completions and POST /responses also accept an optional request-body trace object:
{
"trace": {
"trace_id": "4f8c2b9670b44b49a2e71e7fd5c0b1d3",
"trace_name": "checkout-flow",
"span_name": "summarize-order"
}
}AnyRouter uses this metadata to correlate internal request steps such as identity resolution, rate-limit checks, upstream selection, and upstream dispatch. If you do not send a trace object, AnyRouter still returns an X-Request-ID header you can use for support.
Versioning
Changes are categorized as:
- Additive (new fields, new endpoints) — ship continuously, no notice.
- Deprecation — announced with a 12-month sunset window and a
Deprecationresponse header. - Breaking — land on a new
/v2path;/v1remains available for the deprecation window.
Related
- Migrate from OpenAI — swap base URL and key
- Migrate from Anthropic — point Anthropic clients at Messages
- Migrate to Responses — adopt the structured lifecycle
- Errors — status codes and retry guidance