Console
API reference

Messages

The Anthropic-compatible chat endpoint.

POST/v1/messages

The Anthropic-compatible endpoint. Point the Anthropic SDK or Claude Code at https://demo-api.routerplus.com with your RouterPlus key, and call any chat model in the catalog, GPT models included. The gateway translates requests, streams and errors between provider dialects.

Image models answer on POST /v1/images/generations only. The OpenAI-native endpoint is POST /v1/chat/completions.

POST https://demo-api.routerplus.com/v1/messages

The gateway accepts a query string. Claude Code sends /v1/messages?beta=true, which routes normally.

Examples

A GPT model over the Anthropic wire:

curl
curl -N https://dev2-api.routerplus.com/v1/messages \
  -H "x-api-key: $TM_API_KEY" \
  -H "content-type: application/json" \
  -d '{"model":"gpt-4o-mini","stream":true,"max_tokens":100,"messages":[{"role":"user","content":"Say hello."}]}'
python
import os
from anthropic import Anthropic

client = Anthropic(base_url="https://demo-api.routerplus.com", api_key=os.environ["TM_API_KEY"])

with client.messages.stream(
    model="claude-sonnet-4-5",
    max_tokens=300,
    messages=[{"role": "user", "content": "Count to five."}],
) as stream:
    for text in stream.text_stream:
        print(text, end="")
    print("\n", stream.get_final_message().usage)  # tokens, cache splits, cost
typescript
import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic({
  baseURL: "https://demo-api.routerplus.com",
  apiKey: process.env.TM_API_KEY,
});

const stream = client.messages.stream({
  model: "claude-haiku-4-5",
  max_tokens: 200,
  messages: [{ role: "user", content: "Say hello." }],
});
stream.on("text", (t) => process.stdout.write(t));
console.log("\n", (await stream.finalMessage()).usage);

The examples use the official anthropic and @anthropic-ai/sdk SDKs with only the base URL changed. The SDK appends /v1/messages itself.

Claude Code works unmodified:

bash
ANTHROPIC_BASE_URL=https://dev2-api.routerplus.com ANTHROPIC_AUTH_TOKEN=$TM_API_KEY claude

Headers

HeaderRequiredBehavior
x-api-keyone of these twoYour RouterPlus key, tm_vk_.... The Anthropic-native header.
AuthorizationBearer tm_vk_..., also accepted, same key.
content-typeyesapplication/json.
anthropic-versionnoAccepted for SDK compatibility. The gateway sends anthropic-version: 2023-06-01 to Anthropic providers, whatever you send.
x-tm-auto-cachenooff turns off the automatic prompt caching that the gateway may add for an agent's conversation on Claude (see Automatic caching on Claude). The response header of the same name says whether it was applied.
anthropic-betanoSee below. No effect on other routes.

On an Anthropic-dialect provider, the gateway forwards the anthropic-beta values that do not change how a token is billed:

  • On every Anthropic-dialect provider: the prompt-caching, token-efficient-tools, fine-grained-tool-streaming, interleaved-thinking, claude-code, oauth and computer-use prefixes.
  • On Anthropic's own API also: per-turn-control, mid-conversation-output-config, context-management, mid-conversation-tool-changes and inline-tools (what Claude Code needs on the Claude 5 models).
  • Any other value is dropped and recorded in x-tm-dropped-params as header:anthropic-beta:<value>, for example advisor-tool (a second model that bills at its own rates).

A missing or invalid key returns 401 in the Anthropic error shape with error_type: "auth". To identify your app, send HTTP-Referer and X-Title. They carry no content.

Request

The body is JSON, at most 10 MB (413 request_too_large above that).

ParameterTypeBehavior
modelstring, requiredA catalog id: any chat model, Claude or not. The rp- names of the Claude models (rp-sonnet-5-5) work here too. An unknown id is a 404. An image model is a 400 that names /v1/images/generations. A deployed endpoint id (tm/<name>-v<n>) is served on POST /v1/chat/completions only, and is a 400 here.
max_tokensnumberOutput cap, from 1 to the model's own maximum (max_output_tokens in the catalog). The maximum is 128,000 on the Claude 5 models, and can be below 32,768 (GPT-4o: 16,384). If the catalog declares no maximum, the value is 32,768. When you omit it, the gateway sets the model's own maximum on the forwarded request, on every provider. It lowers the value when needed so that your input and the output fit the context window. null counts as not set. A value under 1 or above the maximum is a 400. max_tokens: 0, which on Anthropic's API only warms the cache, is not served.
messagesarray, requiredRoles user, assistant and system. Content is a string or blocks: text, tool_use (assistant), tool_result (user), thinking / redacted_thinking (assistant). image and document blocks work on a model that takes them (see architecture.input_modalities in GET /v1/models). A block the model does not take is a typed 400 that names it, also inside a tool_result. On Anthropic-dialect routes the messages pass through as sent. See Messages on an OpenAI-dialect route.
systemstring | arrayA string or text blocks.
streambooleanSSE streaming. See Streamed response.
temperature, top_pnumberPassed through.
stop_sequencesstring[]Becomes OpenAI stop on cross-dialect routes.
toolsarray{name, description?, input_schema}. Anthropic server tools (computer use, web search, bash, text editor) pass through on Anthropic-dialect routes, and are a typed 400 when an OpenAI-dialect provider would serve the request. Web search is billed at Anthropic's price per search, inside usage.cost. See Pricing & billing. An empty list goes to an OpenAI-dialect provider as no list.
tool_choiceobject{"type":"auto"}, {"type":"any"}, {"type":"none"}, or {"type":"tool","name":"..."}. disable_parallel_tool_use: true becomes parallel_tool_calls: false on OpenAI-dialect routes.
thinkingobjectPasses through on Anthropic-dialect routes. For OpenAI-dialect routes, see Thinking on an OpenAI-dialect route.
output_configobjectPasses through on Anthropic-dialect routes. On an OpenAI-dialect route, effort becomes reasoning_effort (the nearest level the provider declares). format with type: "json_schema" becomes a strict json_schema response_format with the whole schema, and only providers that take json_schema serve it. When none does, the request is a typed 400 that names output_config.format. Another format type is a typed 400.
context_managementobjectContext editing. Passes to Anthropic's own API with the context-management beta. Dropped and recorded on Bedrock and on OpenAI-dialect routes.
top_k, metadataPass through on Anthropic-dialect routes. Dropped and recorded on OpenAI-dialect routes. On a house route, metadata.user_id becomes a hash with your organization. See Sessions and prompt caching.
cache_controlobjectAutomatic prompt caching, on the request or on blocks. Passes to Anthropic. On Bedrock, the request-level field becomes a mark on the last block. Dropped and recorded on OpenAI and Azure. A request-level field that Anthropic would refuse is dropped and recorded on every route (see Wire compatibility). On a house route, ttl: "1h" is removed (so the TTL is 5 minutes) and recorded. When you send no cache_control, the gateway may add the request-level field for an agent's conversation. See Automatic caching on Claude.
session_idstringA session id, also accepted as the header x-session-id, x-session-affinity or x-claude-code-session-id. It reaches OpenAI or Azure as prompt_cache_key, and Morph as the header x-session-id. Anthropic has no session input. See Sessions and prompt caching.
mcp_servers, containerA typed 400 on every route, because removing them would change who runs what.
providerobjectRouting controls: require_parameters, order, only, ignore, allow_fallbacks and the rest. The gateway reads them and never forwards them. An unknown control is a 400. See Routing policies.
  • Any other top-level key is dropped and recorded on every route, and the x-tm-dropped-params header names it. The gateway never changes it into something else.
  • A top-level api_key, base_url or connection_id is a 400.
  • The gateway never drops content, because the provider would still bill for it. A block the model does not take, or one a translation cannot carry, is a typed 400 that names the field.
  • The gateway decides a translation 400 per deployment. When another deployment of the model speaks the Anthropic dialect, the request goes there instead. See Wire compatibility.

Messages on an OpenAI-dialect route

  • A system message inside the conversation applies from its place onward, as on Anthropic's API. On an OpenAI-dialect provider it goes as a system message at the same place, with its output_config.effort as reasoning_effort.
  • A turn-scoped system message (clear_at: "next_user_message") is left out once a user message follows it. A tool added or removed inside one that still shows is a typed 400.
  • image blocks (base64 or URL) and base64 PDF document blocks in user messages are translated.
  • A document given by URL or as text, an image given by file id, and an image in an assistant message are a typed 400.

Thinking on an OpenAI-dialect route

  • adaptive and enabled leave the model's own reasoning on. budget_tokens has no counterpart, and is dropped and recorded.
  • disabled becomes reasoning_effort: "none".
  • display sets how the reasoning comes back. With "omitted" or "updates", it comes back as thinking blocks with an empty thinking field, and the text travels in the block's signature for the next turn. Otherwise it comes back as readable thinking blocks.
  • A route that serves the model with thinking off, or a model that does not reason (Mistral's Codestral, Large 3 and Ministral 3), drops and records thinking.
  • See Wire compatibility.
Note

Transcripts with thinking blocks round-trip safely. When a model served over the OpenAI dialect returns reasoning, this endpoint encodes it as thinking blocks: in a stream, and as a leading block in a reply that is not streamed. When you send that transcript back to an OpenAI-dialect provider, the gateway drops assistant thinking/redacted_thinking blocks instead of rejecting them. The gateway's own blocks (unsigned, or with the gateway's signature when thinking.display hid their text) are not named in x-tm-dropped-params, and Morph's and Mistral's routes read them back as the model's reasoning. The gateway also leaves them out of the history an Anthropic-dialect route gets, because Anthropic verifies every thinking signature. A conversation that moved to another provider while Anthropic was unavailable therefore continues on Anthropic. The route that produced your transcript never returns a 400 on it.

Streamed response

With stream: true, you get the Anthropic Messages event sequence, in the same encoding on every provider. On a translated stream the message id is msg_<request-id>, which joins with the x-request-id header and /v1/generation?id=.

event: message_start
data: {"type":"message_start","message":{"id":"msg_1c9a7b2e-...","type":"message","role":"assistant","model":"claude-sonnet-4-5","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"cache_read_input_tokens":0,"cache_creation_input_tokens":0,"output_tokens":0}}}

event: content_block_start
data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}

event: content_block_stop
data: {"type":"content_block_stop","index":0}

event: message_delta
data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":12,"cache_read_input_tokens":0,"cache_creation_input_tokens":0,"output_tokens":5,"cost":0.000111}}

event: message_stop
data: {"type":"message_stop"}
  • Block types: text (text_delta), thinking (thinking_delta, plus signature_delta for multi-turn thinking with tools), redacted_thinking, and tool_use (input_json_delta fragments).
  • Streamed stop_reason is one of end_turn, max_tokens, tool_use, refusal.
  • Usage fields follow the billing contract. input_tokens excludes cache reads and writes, which are in cache_read_input_tokens and cache_creation_input_tokens. usage.cost (USD, billed keys only) in the final message_delta is the full request debit, failed-over attempts included, computed with the same math as the ledger.
Note

Read usage from the final message_delta. On same-dialect routes (a Claude model on this endpoint), the gateway relays the stream record for record, so message_start has the provider's real token counts and its own message id. On cross-dialect routes (an OpenAI-dialect provider behind this endpoint), the gateway re-encodes the stream, and the counts in message_start are zeros. The final message_delta has the real numbers on every route.

  • When a started stream is silent for 15 s or more, the gateway sends native ping events ({"type":"ping"}) so that proxies keep the stream open.
  • If the serving provider fails after output started, you get one error event inside the 200 stream, and nothing after it. It has the Anthropic-native type plus a stable error_type. The gateway never restarts the answer on another provider.
  • The same event ends a stream that reaches the request deadline (450 s from dispatch, or a routing policy's timeout_ms).
  • The Anthropic wire has no place for usage outside message_delta. For a billed stream that fails mid-answer, get the request's cost from /v1/generation?id=.

Non-streamed response

json
{
  "id": "msg_1c9a7b2e-...",
  "type": "message",
  "role": "assistant",
  "model": "gpt-4o-mini",
  "content": [{ "type": "text", "text": "Hello!" }],
  "stop_reason": "end_turn",
  "stop_sequence": null,
  "usage": {
    "input_tokens": 12,
    "output_tokens": 5,
    "cache_read_input_tokens": 0,
    "cache_creation_input_tokens": 0,
    "cost": 0.000004
  }
}

On a translated response, stop_reason is end_turn, max_tokens, tool_use, or refusal. On an Anthropic-dialect provider, the body passes through with its native values (including stop_sequence), plus the added cost.

Errors

Errors use the Anthropic error shape, with the native type string your SDK reads (authentication_error, rate_limit_error, billing_error, not_found_error, ...), plus a stable error_type with the canonical class:

json
{
  "type": "error",
  "error": {
    "type": "not_found_error",
    "message": "model \"claude-9\" is not in the catalog; GET /v1/models lists what this key can serve",
    "error_type": "model_unavailable"
  },
  "request_id": "..."
}
CaseStatus and type
Out of credits, or a spend-cap breach429, native type billing_error, error_type: "insufficient_quota". On a cap breach, the message and the x-tm-cap-reset header give the UTC reset time.
A key whose signup email is not verified yet403, native type permission_error, error_type: "email_not_verified".
An organization that has not added creditIts keys and the organization as a whole run at 20 RPM. Above that: 429 rate_limit with retry-after.
A prompt too long for the modelAnthropic's standard 400 invalid_request_error, prompt is too long: N tokens > M maximum, with error_type: "context_overflow".

The gateway never relays a provider's raw error body. When a provider rejected the request, the class says so. The message has the provider's own words (up to 1,000 characters, without keys, account ids and host names), and x-tm-upstream-status has its status. Full table: Errors.

Headers:

  • Every response carries x-request-id.
  • Served requests add x-tm-provider and x-tm-attempts.
  • Failures add x-tm-error-code, and x-tm-error-origin when a limit, a provider or RouterPlus's infrastructure refused the request.
  • x-tm-upstream-status appears whenever a provider responded.
  • The other headers are the same as on POST /v1/chat/completions.

Model discovery

GET https://demo-api.routerplus.com/v1/models returns the catalog. Send an anthropic-version header (the Anthropic SDK does) to get the Anthropic list shape instead of the OpenAI one. That shape lists chat models only. The public directory with prices and context lengths is https://demo.routerplus.com/api/models.json.

See also: POST /v1/chat/completions · Errors · Wire compatibility & billing contract · Migrate in one prompt

POST /v1/messages/count_tokens

An unbilled passthrough to Anthropic-dialect deployments. It takes the same request shape as /v1/messages, and the provider's own tokenizer answers:

bash
curl -s https://dev2-api.routerplus.com/v1/messages/count_tokens \
  -H "x-api-key: $TM_API_KEY" \
  -H "content-type: application/json" \
  -d '{"model": "claude-sonnet-5", "messages": [{"role": "user", "content": "hello"}]}'
json
{ "input_tokens": 8 }
  • The answer is the count only.
  • The call creates no ledger attempt and costs nothing. It counts against your key's rate limit and the provider's pool like any request.
  • An unknown model is a 404.
  • If only OpenAI-dialect deployments (or a Bedrock connection) serve the model, the response is a typed 400. The gateway does not estimate a count.

For agents: /docs/api-messages.md and /llms.txt