# Anthropic Messages API

URL: https://docs.router.ezat.io/anthropic/messages

> Send requests in Anthropic's Messages format, streaming or not, count tokens, list models, and see which features are supported.

## When to use this page

You call AI models through Anthropic's Messages format: with an Anthropic SDK, Claude Code, or plain HTTP. This page covers the endpoints, headers, model names, streaming, timeouts and what isn't supported. Full request and response schemas are in the [API reference](https://docs.router.ezat.io/reference/api.md).

## Base URL and endpoints

The base URL is `https://api.router.ezat.io`. Anthropic's SDKs add `/v1/…` themselves, so give them the base URL without `/v1`.

| Method | Path | What it does |
| --- | --- | --- |
| `POST` | `/v1/messages` | Creates a message. Add `"stream": true` to stream it. |
| `POST` | `/v1/messages/count_tokens` | Counts the input tokens of a request. Free. |
| `GET` | `/v1/models` | Lists the models your key can use. |
| `GET` | `/v1/models/{model_id}` | Returns one model, looked up by id or alias. |

Every other path returns `404` (see [Supported and not supported](#supported-and-not-supported)).

## Authentication

Send your EZAT key (`ezat_sk_…`) in either header:

- `x-api-key: <key>` (what the Anthropic SDKs send), or
- `Authorization: Bearer <key>` (what Claude Code sends with `ANTHROPIC_AUTH_TOKEN`).

If a request has both, EZAT uses `x-api-key`. See [Authentication](https://docs.router.ezat.io/authentication.md) for key safety and the `401` and `403` errors.

## Create a message

```bash tab="curl" test
curl --fail-with-body https://api.router.ezat.io/v1/messages \
  -H "x-api-key: $EZAT_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "content-type: application/json" \
  -d '{
    "model": "anthropic/claude-sonnet-5-5",
    "max_tokens": 64,
    "messages": [{"role": "user", "content": "Say hello in five words."}]
  }'
```

```python tab="Python" test
import os

import anthropic

client = anthropic.Anthropic(
    base_url="https://api.router.ezat.io",
    api_key=os.environ["EZAT_API_KEY"],
)

message = client.messages.create(
    model="anthropic/claude-sonnet-5-5",
    max_tokens=64,
    messages=[{"role": "user", "content": "Say hello in five words."}],
)
print("".join(block.text for block in message.content if block.type == "text"))
print(message.usage)
```

```ts tab="TypeScript" test
import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic({
  baseURL: "https://api.router.ezat.io",
  apiKey: process.env.EZAT_API_KEY,
});

const message = await client.messages.create({
  model: "anthropic/claude-sonnet-5-5",
  max_tokens: 64,
  messages: [{ role: "user", content: "Say hello in five words." }],
});
console.log(message.content.filter((block) => block.type === "text").map((block) => block.text).join(""));
console.log(message.usage);
```

The response is Anthropic's `message` object:

```json
{
  "id": "msg_…",
  "type": "message",
  "role": "assistant",
  "model": "anthropic/claude-sonnet-5-5",
  "content": [{ "type": "text", "text": "Hello, it's nice to meet you." }],
  "stop_reason": "end_turn",
  "stop_sequence": null,
  "usage": {
    "input_tokens": 14,
    "output_tokens": 9,
    "cache_creation_input_tokens": 0,
    "cache_read_input_tokens": 0
  }
}
```

- **`model` is exactly what you sent.** Send an alias and you get the alias back. Claude Code and the SDKs depend on this.
- **Read the `text` blocks.** `content` can hold `thinking` or `tool_use` blocks before or instead of text, so don't assume `content[0]` is text.
- **`request-id`** in the response headers is EZAT's id for the request. Quote it when you contact support.

## Stream a message

Add `"stream": true`. The response is Server-Sent Events in Anthropic's format, unchanged.

```bash tab="curl" test
curl --fail-with-body -N https://api.router.ezat.io/v1/messages \
  -H "x-api-key: $EZAT_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "content-type: application/json" \
  -d '{
    "model": "anthropic/claude-sonnet-5-5",
    "max_tokens": 64,
    "stream": true,
    "messages": [{"role": "user", "content": "Count from 1 to 10."}]
  }'
```

```python tab="Python" test
import os

import anthropic

client = anthropic.Anthropic(
    base_url="https://api.router.ezat.io",
    api_key=os.environ["EZAT_API_KEY"],
)

with client.messages.stream(
    model="anthropic/claude-sonnet-5-5",
    max_tokens=64,
    messages=[{"role": "user", "content": "Count from 1 to 10."}],
) as stream:
    for text in stream.text_stream:
        print(text, end="", flush=True)
    print()
    print(stream.get_final_message().usage)
```

```ts tab="TypeScript" test
import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic({
  baseURL: "https://api.router.ezat.io",
  apiKey: process.env.EZAT_API_KEY,
});

const stream = client.messages.stream({
  model: "anthropic/claude-sonnet-5-5",
  max_tokens: 64,
  messages: [{ role: "user", content: "Count from 1 to 10." }],
});
stream.on("text", (text) => process.stdout.write(text));
const message = await stream.finalMessage();
console.log("\n", message.usage);
```

| Event | When |
| --- | --- |
| `message_start` | First. Carries the message with `model` set to what you sent, and the input and cache token counts. |
| `content_block_start` | A text, thinking or tool use block begins. |
| `content_block_delta` | Part of a block: `text_delta`, `thinking_delta` or `input_json_delta`. |
| `content_block_stop` | The block is complete. |
| `message_delta` | The `stop_reason` and the final output token count. |
| `message_stop` | Last event. The connection closes once EZAT has recorded the request's usage. |
| `ping` | Sent after 15 seconds without other events, so proxies keep the connection open. |
| `error` | A failure after the stream started (the HTTP status is already `200`). The stream ends after it. |

Before the first event, EZAT retries a failing deployment on another one, so a provider error at the start of a stream usually never reaches you. Once events have started, there is no retry.

## Count tokens

`POST /v1/messages/count_tokens` takes the same body as `/v1/messages` (`model` and `messages`, plus `system` and `tools` if you use them) without `max_tokens`, and returns `{"input_tokens": N}`.

```bash tab="curl" test
curl --fail-with-body https://api.router.ezat.io/v1/messages/count_tokens \
  -H "x-api-key: $EZAT_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "content-type: application/json" \
  -d '{
    "model": "anthropic/claude-sonnet-5-5",
    "messages": [{"role": "user", "content": "How many tokens is this?"}]
  }'
```

```python tab="Python" test
import os

import anthropic

client = anthropic.Anthropic(
    base_url="https://api.router.ezat.io",
    api_key=os.environ["EZAT_API_KEY"],
)

count = client.messages.count_tokens(
    model="anthropic/claude-sonnet-5-5",
    messages=[{"role": "user", "content": "How many tokens is this?"}],
)
print(count.input_tokens)
```

```ts tab="TypeScript" test
import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic({
  baseURL: "https://api.router.ezat.io",
  apiKey: process.env.EZAT_API_KEY,
});

const count = await client.messages.countTokens({
  model: "anthropic/claude-sonnet-5-5",
  messages: [{ role: "user", content: "How many tokens is this?" }],
});
console.log(count.input_tokens);
```

Counting is free: it isn't billed and doesn't appear in Usage. It does count toward the key's requests-per-minute limit. `anthropic-beta` isn't applied to it.

## List models

```bash tab="curl" test
curl --fail-with-body https://api.router.ezat.io/v1/models \
  -H "x-api-key: $EZAT_API_KEY" \
  -H "anthropic-version: 2023-06-01"
```

```json
{
  "data": [
    {
      "type": "model",
      "id": "anthropic/claude-sonnet-5-5",
      "display_name": "…",
      "created_at": "2026-10-01T00:00:00.000Z"
    }
  ],
  "has_more": false,
  "first_id": "…",
  "last_id": "…"
}
```

- The list has every model your key can use, with canonical ids. A key limited to some models (see [API keys](https://docs.router.ezat.io/api-keys.md)) sees only those.
- `GET /v1/models/{model_id}` accepts an alias and returns the canonical id. A canonical id contains a slash, so URL-encode it there (`anthropic%2Fclaude-sonnet-5-5`) or use the alias.

## Model ids and aliases

- **Canonical ids** are `author/model`, for example `anthropic/claude-sonnet-5-5`.
- **Aliases.** Anthropic's own names (for example `claude-sonnet-5-5` and `claude-sonnet-5.5`) are aliases of the canonical id, so code written for Anthropic's API works unchanged. Matching is case-insensitive.
- Both forms bill the same. The [Models](https://docs.router.ezat.io/models.md) page lists every id and its aliases.
- An unknown name returns `404` `not_found_error` with the message `model: <name>`. A model the key isn't allowed to use returns `403` `permission_error`.

## Headers

Request headers:

| Header | Required | What EZAT does with it |
| --- | --- | --- |
| `x-api-key` or `Authorization: Bearer` | Yes | Your EZAT key. |
| `content-type: application/json` | Yes, on `POST` | |
| `anthropic-version` | No | Accepted and not used. The SDKs send their own value; any value works, and you can leave it out. |
| `anthropic-beta` | No | Passed to the model provider unchanged on `/v1/messages`. A beta the provider doesn't accept fails there. |
| `x-ezat-session` | No | Names the conversation, so its requests stay on one deployment and keep their prompt cache. Any string; only the first 256 characters are used. See [Prompt caching and sessions](https://docs.router.ezat.io/guides/prompt-caching.md). |
| `x-request-id` | No | Your own id for the request (letters, digits, `.`, `_`, `:` and `-`, up to 128 characters). EZAT echoes it back. |

Response headers:

| Header | What |
| --- | --- |
| `request-id` | EZAT's id for the request. Set on every request that passed the key, model, limit and balance checks. |
| `x-request-id` | Your `x-request-id`, or one EZAT made. Error bodies repeat it as `request_id`. |
| `retry-after` | On `429` and `529`: seconds to wait before retrying. |

EZAT doesn't send Anthropic's `anthropic-ratelimit-*` headers. Its limits are in [Rate limits](https://docs.router.ezat.io/reference/rate-limits.md).

## Tools

Tools you define and run yourself work as on Anthropic's API: `tools`, `tool_choice`, `tool_use` blocks in the response and `tool_result` blocks in your next message.

```json
{
  "model": "anthropic/claude-sonnet-5-5",
  "max_tokens": 1024,
  "tools": [
    {
      "name": "get_weather",
      "description": "Current weather for a city",
      "input_schema": {
        "type": "object",
        "properties": { "city": { "type": "string" } },
        "required": ["city"]
      }
    }
  ],
  "messages": [{ "role": "user", "content": "What's the weather in Oslo?" }]
}
```

Server tools that Anthropic runs for you (web search, code execution) aren't available. See the table below.

## Images and PDFs

Send images as base64 `image` blocks (JPEG, PNG, GIF or WebP) and PDFs as base64 `document` blocks:

```json
{
  "role": "user",
  "content": [
    {
      "type": "image",
      "source": { "type": "base64", "media_type": "image/png", "data": "iVBORw0KGgo…" }
    },
    { "type": "text", "text": "What is in this image?" }
  ]
}
```

On this API EZAT doesn't download images, so sources of type `url` don't work. To send image URLs, use the [OpenAI-compatible API](https://docs.router.ezat.io/openai/chat-completions.md), which downloads them for you. The whole request body can be up to 32 MB; a larger one returns `413` `request_too_large`.

## Thinking

`thinking` passes through to the model, and `thinking` blocks come back in the response and the stream. Thinking tokens are billed as output tokens.

```json
{
  "model": "anthropic/claude-sonnet-5-5",
  "max_tokens": 16000,
  "thinking": { "type": "adaptive" },
  "messages": [{ "role": "user", "content": "Plan a three-day trip to Oslo." }]
}
```

The options are in Anthropic's [extended thinking guide](https://platform.claude.com/docs/en/build-with-claude/extended-thinking). Prefer streaming for requests with thinking: they often run long.

## Prompt caching

`cache_control` works as on Anthropic's API, with a 5-minute TTL (`{"type": "ephemeral"}`) or a 1-hour one (`{"type": "ephemeral", "ttl": "1h"}`). Cache writes and reads are billed at their own prices (see [Pricing](https://docs.router.ezat.io/pricing.md)), and `usage` reports them in `cache_creation_input_tokens` and `cache_read_input_tokens`.

EZAT keeps each conversation on one deployment so its cache keeps working. [Prompt caching and sessions](https://docs.router.ezat.io/guides/prompt-caching.md) explains where to put breakpoints, the session header and the models with caveats.

## Long requests and timeouts

- **Non-streaming requests have a 590-second deadline.** A model that hasn't finished by then ends the request with `504` `timeout_error` and the message ``The model took longer than 590 seconds. Send long requests with "stream": true.`` The request isn't billed.
- **Stream long outputs.** A streamed response has no deadline: EZAT sends a `ping` event after 15 seconds of silence, so the connection stays open while the model thinks. Stream anything with a large `max_tokens`, thinking, or a long input.

## Errors

Errors use Anthropic's format at Anthropic's status codes:

```json
{
  "type": "error",
  "error": { "type": "billing_error", "message": "Your credit balance is too low. Add credits in the EZAT Router dashboard." },
  "request_id": "…"
}
```

A request that fails isn't billed. [Errors](https://docs.router.ezat.io/reference/errors.md) lists every status and type and says which ones to retry.

## Interrupted streams

If you close a stream before it ends, EZAT stops the model and bills what was produced: input and cache tokens exactly, and output tokens estimated from the text already streamed. Usage shows the request as Cancelled, marked as estimated. If the provider fails in the middle of a stream, you get an `error` event and the request isn't billed. [Billing](https://docs.router.ezat.io/billing.md) has the details.

## Supported and not supported

| Feature | Status | Notes |
| --- | --- | --- |
| Messages, streaming and not | Supported | |
| Token counting | Supported | Free; not shown in Usage |
| Models list and lookup | Supported | |
| Tools you define (`tools`, `tool_choice`) | Supported | |
| Images and PDFs as base64 | Supported | Up to 32 MB per request body |
| Images by URL (`source.type: "url"`) | Not supported | Send base64, or use the OpenAI-compatible API |
| Thinking | Supported | |
| Prompt caching, 5 minutes and 1 hour | Supported | Except models with `capabilities.promptCaching` `false`; see [Prompt caching](https://docs.router.ezat.io/guides/prompt-caching.md) |
| Structured outputs (`output_config.format`) | Supported | |
| `temperature`, `top_p`, `top_k` | Passed through | Only models whose `capabilities.sampling` is `true` accept them; the others reject values other than the defaults with a `400` |
| `anthropic-beta` | Passed through | The model provider decides which betas work |
| Message Batches API (`/v1/messages/batches`) | Not available | Returns `404` |
| Files API (`/v1/files`) | Not available | Returns `404`; send files inline as base64 |
| Server tools: web search, code execution and the others Anthropic runs | Not available | |
| MCP connector (`mcp_servers`) | Not available | |

`capabilities` for each model is in `GET https://api.router.ezat.io/api/models` (no key needed) and in the features column on [Models](https://docs.router.ezat.io/models.md).

The unavailable endpoints answer `404` with `{"statusCode": 404, "message": "Cannot POST /v1/messages/batches"}`, which isn't Anthropic's error format. Server tools and the MCP connector aren't blocked by EZAT yet: the request goes to the model provider unchanged, and the provider doesn't run them, so expect an error. Don't send them.

## For AI agents

1. Base URL `https://api.router.ezat.io` (no `/v1`: the SDKs add it). Key from the `EZAT_API_KEY` environment variable, sent as `x-api-key` or `Authorization: Bearer`.
2. Use a model id from `GET https://api.router.ezat.io/v1/models`, or an Anthropic model name, which is an alias. The response's `model` echoes what you sent.
3. Read text from the `text` blocks in `content`, not from `content[0]`.
4. Stream (`"stream": true`) any request that may take more than a few minutes. Non-streaming requests end with `504` `timeout_error` after 590 seconds.
5. Don't use Message Batches, the Files API, server tools (web search, code execution) or the MCP connector: they aren't available.
6. On an error, check `error.type` and [Errors](https://docs.router.ezat.io/reference/errors.md) before retrying. Report `request-id` to support.