EZAT RouterDocs

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.

MethodPathWhat it does
POST/v1/messagesCreates a message. Add "stream": true to stream it.
POST/v1/messages/count_tokensCounts the input tokens of a request. Free.
GET/v1/modelsLists 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), or
  • Authorization: Bearer <key> (what Claude Code sends with ANTHROPIC_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
  }
}
  • 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.

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."}]
  }'
EventWhen
message_startFirst. Carries the message with model set to what you sent, and the input and cache token counts.
content_block_startA text, thinking or tool use block begins.
content_block_deltaPart of a block: text_delta, thinking_delta or input_json_delta.
content_block_stopThe block is complete.
message_deltaThe stop_reason and the final output token count.
message_stopLast event. The connection closes once EZAT has recorded the request's usage.
pingSent after 15 seconds without other events, so proxies keep the connection open.
errorA 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 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 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:

HeaderRequiredWhat EZAT does with it
x-api-key or Authorization: BearerYesYour EZAT key.
content-type: application/jsonYes, on POST
anthropic-versionNoAccepted and not used. The SDKs send their own value; any value works, and you can leave it out.
anthropic-betaNoPassed to the model provider unchanged on /v1/messages. A beta the provider doesn't accept fails there.
x-ezat-sessionNoNames 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-idNoYour own id for the request (letters, digits, ., _, : and -, up to 128 characters). EZAT echoes it back.

Response headers:

HeaderWhat
request-idEZAT's id for the request. Set on every request that passed the key, model, limit and balance checks.
x-request-idYour x-request-id, or one EZAT made. Error bodies repeat it as request_id.
retry-afterOn 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 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:

{
  "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

FeatureStatusNotes
Messages, streaming and notSupported
Token countingSupportedFree; not shown in Usage
Models list and lookupSupported
Tools you define (tools, tool_choice)Supported
Images and PDFs as base64SupportedUp to 32 MB per request body
Images by URL (source.type: "url")Not supportedSend base64, or use the OpenAI-compatible API
ThinkingSupported
Prompt caching, 5 minutes and 1 hourSupportedExcept models with capabilities.promptCaching false; see Prompt caching
Structured outputs (output_config.format)Supported
temperature, top_p, top_kPassed throughOnly models whose capabilities.sampling is true accept them; the others reject values other than the defaults with a 400
anthropic-betaPassed throughThe model provider decides which betas work
Message Batches API (/v1/messages/batches)Not availableReturns 404
Files API (/v1/files)Not availableReturns 404; send files inline as base64
Server tools: web search, code execution and the others Anthropic runsNot 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

  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 before retrying. Report request-id to support.

On this page