Anthropic Messages API
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.
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).
Authentication
Send your EZAT key (ezat_sk_…) in either header:
x-api-key: <key>(what the Anthropic SDKs send), orAuthorization: Bearer <key>(what Claude Code sends withANTHROPIC_AUTH_TOKEN).
If a request has both, EZAT uses x-api-key. See Authentication for key safety and the 401 and 403 errors.
Create a message
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."}]
}'The response is Anthropic's message object:
{
"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
}
}modelis exactly what you sent. Send an alias and you get the alias back. Claude Code and the SDKs depend on this.- Read the
textblocks.contentcan holdthinkingortool_useblocks before or instead of text, so don't assumecontent[0]is text. request-idin 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.
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."}]
}'| 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}.
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?"}]
}'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
curl --fail-with-body https://api.router.ezat.io/v1/models \
-H "x-api-key: $EZAT_API_KEY" \
-H "anthropic-version: 2023-06-01"{
"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) 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 exampleanthropic/claude-sonnet-5-5. - Aliases. Anthropic's own names (for example
claude-sonnet-5-5andclaude-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 page lists every id and its aliases.
- An unknown name returns
404not_found_errorwith the messagemodel: <name>. A model the key isn't allowed to use returns403permission_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. |
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.
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.
{
"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:
{
"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, 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.
{
"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. 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), 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 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
504timeout_errorand the messageThe 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
pingevent after 15 seconds of silence, so the connection stays open while the model thinks. Stream anything with a largemax_tokens, thinking, or a long input.
Errors
Errors use Anthropic's format at Anthropic's status codes:
{
"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 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 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 |
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.
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
- Base URL
https://api.router.ezat.io(no/v1: the SDKs add it). Key from theEZAT_API_KEYenvironment variable, sent asx-api-keyorAuthorization: Bearer. - Use a model id from
GET https://api.router.ezat.io/v1/models, or an Anthropic model name, which is an alias. The response'smodelechoes what you sent. - Read text from the
textblocks incontent, not fromcontent[0]. - Stream (
"stream": true) any request that may take more than a few minutes. Non-streaming requests end with504timeout_errorafter 590 seconds. - Don't use Message Batches, the Files API, server tools (web search, code execution) or the MCP connector: they aren't available.
- On an error, check
error.typeand Errors before retrying. Reportrequest-idto support.