# 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](/docs/api-images) only. The OpenAI-native endpoint is [POST /v1/chat/completions](/docs/api-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:

```bash
curl -N https://demo-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:

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

```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://demo-api.routerplus.com ANTHROPIC_AUTH_TOKEN=$TM_API_KEY claude
```

## Headers

| Header | Required | Behavior |
|---|---|---|
| `x-api-key` | one of these two | Your RouterPlus key, `tm_vk_...`. The Anthropic-native header. |
| `Authorization` | | `Bearer tm_vk_...`, also accepted, same key. |
| `content-type` | yes | `application/json`. |
| `anthropic-version` | no | Accepted for SDK compatibility. The gateway sends `anthropic-version: 2023-06-01` to Anthropic providers, whatever you send. |
| `x-tm-auto-cache` | no | `off` turns off the automatic prompt caching that the gateway may add for an agent's conversation on Claude (see [Automatic caching on Claude](/docs/compat#automatic-caching-on-claude)). The response header of the same name says whether it was applied. |
| `anthropic-beta` | no | See 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).

| Parameter | Type | Behavior |
|---|---|---|
| `model` | string, required | A 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_tokens` | number | Output 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. |
| `messages` | array, required | Roles `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`](/docs/api-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](#messages-on-an-openai-dialect-route). |
| `system` | string \| array | A string or text blocks. |
| `stream` | boolean | SSE streaming. See [Streamed response](#streamed-response). |
| `temperature`, `top_p` | number | Passed through. |
| `stop_sequences` | string[] | Becomes OpenAI `stop` on cross-dialect routes. |
| `tools` | array | `{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](/docs/pricing#web-search). An empty list goes to an OpenAI-dialect provider as no list. |
| `tool_choice` | object | `{"type":"auto"}`, `{"type":"any"}`, `{"type":"none"}`, or `{"type":"tool","name":"..."}`. `disable_parallel_tool_use: true` becomes `parallel_tool_calls: false` on OpenAI-dialect routes. |
| `thinking` | object | Passes through on Anthropic-dialect routes. For OpenAI-dialect routes, see [Thinking on an OpenAI-dialect route](#thinking-on-an-openai-dialect-route). |
| `output_config` | object | Passes 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_management` | object | Context editing. Passes to Anthropic's own API with the `context-management` beta. Dropped and recorded on Bedrock and on OpenAI-dialect routes. |
| `top_k`, `metadata` | | Pass 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](/docs/compat#sessions-and-prompt-caching). |
| `cache_control` | object | Automatic 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](/docs/compat)). 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](/docs/compat#automatic-caching-on-claude). |
| `session_id` | string | A 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](/docs/compat#sessions-and-prompt-caching). |
| `mcp_servers`, `container` | | A typed 400 on every route, because removing them would change who runs what. |
| `provider` | object | Routing 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](/docs/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](/docs/compat).

### 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](/docs/compat#reasoning-and-structured-output-on-openai-dialect-routes).

> [!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=`.

```text
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](/docs/compat). `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": "..."
}
```

| Case | Status and type |
|---|---|
| Out of credits, or a spend-cap breach | 429, 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 yet | 403, native type `permission_error`, `error_type: "email_not_verified"`. |
| An organization that has not added credit | Its 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 model | Anthropic'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](/docs/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](/docs/api-chat-completions#response-headers).

## 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](/docs/api-chat-completions) · [Errors](/docs/errors) · [Wire compatibility & billing contract](/docs/compat) · [Migrate in one prompt](/docs/migration)

## 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://demo-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.
