Anthropic Messages API
POST /v1/messages, served natively — point the Anthropic SDK or Claude Code at Apodex and the core models answer.
/v1/messageshttps://api.apodex.aiOverview
POST /v1/messages is the Anthropic Messages API, served natively: our inference engine implements the protocol itself, and we forward your request body untouched — there is no protocol conversion anywhere in the chain. The official Anthropic SDKs, and tools built on them such as Claude Code, therefore work against Apodex with nothing but a base URL and an API key. Only the core models are served on this path — the deep research SKUs are not.
These are the same models, the same weights and the same prices as on Core Models — only the wire protocol differs. Pick whichever protocol your client already speaks.
Authentication
Both header styles are accepted, so you can keep whichever one your client already sends:
x-api-key: YOUR_API_KEY— what the Anthropic SDKs send by default.Authorization: Bearer YOUR_API_KEY— the same header the rest of the Apodex API uses.- If a request carries both,
Authorizationwins.
anthropic-version and anthropic-beta are passed through to the model upstream. Sending them is fine, and so is leaving them out — neither header is required.
Create a message
/v1/messagesSet the base URL and your API key, and the official Anthropic SDKs work unchanged. Note that max_tokens is required by the Anthropic protocol.
Headers
| Header | Description |
|---|---|
x-api-keyYOUR_API_KEYRequired | Your Apodex API key. Alternatively send Authorization: Bearer YOUR_API_KEY. |
content-typeapplication/jsonRequired | Always application/json. |
anthropic-version2023-06-01 | Optional. Passed through to the upstream; the Anthropic SDKs set it for you. |
X-Session-Idmy-conversation-42 | Optional. A stable key for one logical conversation — sending the same value across turns significantly improves prefix cache hit rate (lower latency, lower cost). Omit it and the server derives a key from the request content. |
x-api-keyYour Apodex API key. Alternatively send Authorization: Bearer YOUR_API_KEY.
content-typeAlways application/json.
anthropic-versionOptional. Passed through to the upstream; the Anthropic SDKs set it for you.
X-Session-IdOptional. A stable key for one logical conversation — sending the same value across turns significantly improves prefix cache hit rate (lower latency, lower cost). Omit it and the server derives a key from the request content.
Parameters
| Parameter | Description |
|---|---|
modelstringRequired | Required. apodex-1.1 or apodex-1.1-mini. |
messagesarrayRequired | Required. Standard Anthropic message list. Text content only. |
max_tokensintegerRequired | Required by the protocol. Capped at 32768 when the request is non-streaming; uncapped (up to the context window) when streaming. |
systemstring | array | Optional. A plain string or an array of text blocks — both work. |
streamboolean | Optional, defaults to false. Set true for the full Anthropic SSE event stream. |
toolsarray | Optional. Your own function tools, with tool_use / tool_result and tool_choice. Anthropic's server-side built-in tools are not supported — see Limits. |
thinkingobject | Optional. Type enabled or disabled. Omit it and the model uses its default, which has thinking on. |
modelRequired. apodex-1.1 or apodex-1.1-mini.
messagesRequired. Standard Anthropic message list. Text content only.
max_tokensRequired by the protocol. Capped at 32768 when the request is non-streaming; uncapped (up to the context window) when streaming.
systemOptional. A plain string or an array of text blocks — both work.
streamOptional, defaults to false. Set true for the full Anthropic SSE event stream.
toolsOptional. Your own function tools, with tool_use / tool_result and tool_choice. Anthropic's server-side built-in tools are not supported — see Limits.
thinkingOptional. Type enabled or disabled. Omit it and the model uses its default, which has thinking on.
Request
curl -X POST https://api.apodex.ai/v1/messages \
-H "x-api-key: $APODEX_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-H "X-Session-Id: my-conversation-42" \
-d '{
"model": "apodex-1.1",
"max_tokens": 1024,
"system": "You are a concise assistant.",
"messages": [
{"role": "user", "content": "Explain prefix caching in one paragraph."}
]
}'Response
{
"id": "msg_01WxYz",
"type": "message",
"role": "assistant",
"model": "apodex-1.1",
"content": [
{
"type": "thinking",
"thinking": "The user wants a single paragraph, so keep it tight..."
},
{
"type": "text",
"text": "Prefix caching reuses the already-computed attention state..."
}
],
"stop_reason": "end_turn",
"stop_sequence": null,
"usage": {
"input_tokens": 24,
"cache_read_input_tokens": 4096,
"output_tokens": 187
}
}Streaming
With stream: true you get the standard Anthropic SSE event sequence, so the SDK helpers (client.messages.stream) work as documented by Anthropic. The full event set is emitted:
ping events arrive as keep-alives and carry no content — ignore them. thinking_delta and text_delta arrive on content_block_delta, distinguished by the delta's own type.
Per the Anthropic protocol, the input side of usage arrives on message_start and the final output_tokens only on message_delta — so a stream you cut off early reports less output than was produced. See Billing.
Event sequence on the wire
event: message_start
data: {"type":"message_start","message":{"id":"msg_01WxYz","type":"message","role":"assistant","model":"apodex-1.1","content":[],"stop_reason":null,"usage":{"input_tokens":24,"cache_read_input_tokens":4096,"output_tokens":1}}}
event: content_block_start
data: {"type":"content_block_start","index":0,"content_block":{"type":"thinking","thinking":""}}
event: ping
data: {"type":"ping"}
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":"The user wants"}}
event: content_block_stop
data: {"type":"content_block_stop","index":0}
event: content_block_start
data: {"type":"content_block_start","index":1,"content_block":{"type":"text","text":""}}
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":"Prefix caching"}}
event: content_block_stop
data: {"type":"content_block_stop","index":1}
event: message_delta
data: {"type":"message_delta","delta":{"stop_reason":"end_turn"},"usage":{"output_tokens":187}}
event: message_stop
data: {"type":"message_stop"}Streaming example
curl -N -X POST https://api.apodex.ai/v1/messages \
-H "x-api-key: $APODEX_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-H "X-Session-Id: my-conversation-42" \
-d '{
"model": "apodex-1.1",
"max_tokens": 8192,
"stream": true,
"messages": [
{"role": "user", "content": "Write a short essay about caching."}
]
}'Thinking blocks
The models think before answering, and that reasoning comes back as a standard Anthropic thinking content block — streamed as thinking_delta. No proprietary field is involved, so any Anthropic-compatible client renders it.
Thinking is on by default. Omit the thinking parameter and you get the model default, which thinks.
Send thinking with type disabled to turn thinking off, or enabled to ask for it explicitly.
Thinking configuration
// Default — thinking is on, nothing to send
{"model": "apodex-1.1", "max_tokens": 1024, "messages": [...]}
// Thinking off
{"model": "apodex-1.1", "max_tokens": 1024, "thinking": {"type": "disabled"}, "messages": [...]}
// Thinking explicitly on
{"model": "apodex-1.1", "max_tokens": 1024, "thinking": {"type": "enabled"}, "messages": [...]}budget_tokens is accepted and ignored (no error), and display: omitted does not suppress anything — the thinking content is returned either way. Thinking tokens are part of output_tokens, so they count against max_tokens and are billed as output.
max_tokens enough headroom. Thinking spends the same budget as the answer, so a max_tokens that is too small can be consumed entirely by thinking: the response comes back with a thinking block, no text block at all, and stop_reason: max_tokens — which reads like the model said nothing. Either raise max_tokens, or send thinking with type disabled when you want the answer only.Tool use
Your own function tools work the standard Anthropic way: declare them in tools, the model answers with a tool_use block and stop_reason: tool_use, you execute the call and send the outcome back as a tool_result block in the next user message. tool_choice is honoured.
Anthropic's server-executed tools are not supported (web search, computer use, bash, text editor) — see Limits. Only tools you define and run yourself.
stop_reason
The mapping is the standard one:
| stop_reason | Description |
|---|---|
end_turn | The model finished its answer on its own. |
max_tokens | The generation hit the max_tokens budget and was truncated. |
tool_use | The model wants a tool call executed — the content carries a tool_use block. |
Tool use round trip
curl -X POST https://api.apodex.ai/v1/messages \
-H "x-api-key: $APODEX_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d '{
"model": "apodex-1.1",
"max_tokens": 1024,
"tool_choice": {"type": "auto"},
"tools": [
{
"name": "get_weather",
"description": "Get the current weather for a city.",
"input_schema": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"]
}
}
],
"messages": [
{"role": "user", "content": "What is the weather in Singapore?"}
]
}'Count tokens without generating
/v1/messages/count_tokensPOST /v1/messages/count_tokens tokenizes a request body and returns the input size without generating anything. It goes through authentication and rate limiting like any other call, but it is never billed.
Headers
| Header | Description |
|---|---|
x-api-keyYOUR_API_KEYRequired | Your Apodex API key. Alternatively send Authorization: Bearer YOUR_API_KEY. |
content-typeapplication/jsonRequired | Always application/json. |
x-api-keyYour Apodex API key. Alternatively send Authorization: Bearer YOUR_API_KEY.
content-typeAlways application/json.
Request
curl -X POST https://api.apodex.ai/v1/messages/count_tokens \
-H "x-api-key: $APODEX_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d '{
"model": "apodex-1.1",
"messages": [
{"role": "user", "content": "Explain prefix caching in one paragraph."}
]
}'Response
{
"input_tokens": 14
}Limits and unsupported features
Everything below is a real limitation of this endpoint, not a transient bug — please design around it rather than filing a ticket.
| Limits and unsupported features | Description |
|---|---|
| Server-side built-in tools are not supported | Anthropic's server-executed tools — web_search_*, computer_*, bash_*, text_editor_* — are silently skipped: nothing runs on our side and no error is raised. Function tools you define yourself keep working normally. If you depend on a server-side tool, run it in your own code and feed the outcome back as a tool_result. |
| redacted_thinking blocks are rejected | Replaying a redacted_thinking block from an earlier Anthropic conversation returns HTTP 400. Strip those blocks before you resend the history. |
| thinking.budget_tokens and thinking.display have no effect | budget_tokens is ignored (accepted without an error, but it does not shape the thinking length), and display: omitted does not hide the thinking content. To get no thinking at all, disable it — see Thinking blocks. |
| Non-streaming max_tokens is capped at 32768 | Exactly as on /v1/chat/completions: with stream: false, a max_tokens above 32768 is rejected before any generation starts. Long generations must stream. |
| A non-streaming request is capped at about 600s of wall clock | Independently of max_tokens, a single non-streaming request has to finish generating within roughly 600 seconds. That ceiling belongs to the gateway in front of the models, so when it trips you get a gateway-level timeout — an HTTP 504 with an HTML body, not the Anthropic error envelope. This can bite you well inside the 32768 cap: under high concurrency with cold prefix caches, decoding slows down and a request that normally completes in about 190s can approach the wall. Stream anything long, and if you must stay non-streaming, set your client timeout to 600s or more — several SDKs default to exactly 600s, which lands right on the boundary. |
| Text only | Multimodal input is not supported. Image, document and other non-text content blocks are rejected — send text. |
| Account-level rate limits | Concurrency and tokens-per-minute are enforced per account, exactly as on the other endpoints, and surface as HTTP 429 with rate_limit_error. See Rate limits. |
Billing
/v1/messages is billed at exactly the same rates as /v1/chat/completions — the same three tiers (input, cached input, output), the same price list. Nothing on this endpoint is priced differently.
One semantic difference you must account for
usage.input_tokens is a post-cache number: it excludes cache_read_input_tokens. OpenAI's prompt_tokens is the opposite — it includes the cached tokens. We bill on the true total context, with the cache-hit part at the cached rate and the rest at the input rate. So if you take the input_tokens field straight from a response and multiply it by the input price, your numbers will not reconcile with your invoice.Reconciliation formula
Rebuild the true context total first, then split it across the tiers:
total_context = usage.input_tokens + usage.cache_read_input_tokens
cached_input = usage.cache_read_input_tokens
fresh_input = usage.input_tokens
output = usage.output_tokens
cost = fresh_input * price_input
+ cached_input * price_cached_input
+ output * price_output
# 2x multiplier on the whole request when total_context > 200_000
if total_context > 200_000:
cost = cost * 2Footnote: if a response ever reports a non-zero cache_creation_input_tokens, that count is part of the true context total as well and is billed at the input rate. Under the current deployment the field is always absent, which is why it does not appear in the formula above.
The “context above 200K is billed at 2× the listed rates for the whole request” rule is judged on that same true total (input_tokens + cache_read_input_tokens), not on the input_tokens field alone. A 250K-token conversation still crosses the threshold even when almost all of it is served from cache.
If a stream is cut off mid-generation, output_tokens can be under-reported, because the Anthropic protocol only carries the final output usage on the closing events. We bill what we observed, so that error can only ever be in your favour.
POST /v1/messages/count_tokens is free: it passes authentication and rate limiting, but it generates nothing and is never billed.
Current list prices are on the Pricing page.
Anthropic SDKs and Claude Code
Do not put /v1 in the base URL
The official SDKs append /v1/messages themselves. Set base_url / baseURL to the base URL above with no path suffix — adding /v1 produces requests to /v1/v1/messages, which 404s.
Claude Code: you must set CLAUDE_CODE_ATTRIBUTION_HEADER=0
By default Claude Code injects a per-request hash at the top of the system prompt. The prompt prefix then changes on every turn, so prefix caching never hits and the whole conversation is re-prefilled each turn — latency and cost climb steeply on long sessions. Set the variable before you start:
export ANTHROPIC_BASE_URL=https://api.apodex.ai
export ANTHROPIC_AUTH_TOKEN=$APODEX_API_KEY
export ANTHROPIC_MODEL=apodex-1.1
# Required: without this Claude Code puts a per-request hash at the top of
# the system prompt, so the prefix cache never hits.
export CLAUDE_CODE_ATTRIBUTION_HEADER=0
# Small/fast model for background tasks. Newer Claude Code reads the first
# name, older versions the second — setting both is version-proof.
export ANTHROPIC_DEFAULT_HAIKU_MODEL=apodex-1.1-mini
export ANTHROPIC_SMALL_FAST_MODEL=apodex-1.1-mini
claudeClaude Code also calls a small, fast model for background work such as titles and summaries. Point it at apodex-1.1-mini, which serves /v1/messages too — leave it unset and those background calls go to a model id we do not serve and come back 404. The variable was renamed along the way: recent Claude Code reads ANTHROPIC_DEFAULT_HAIKU_MODEL, older versions read ANTHROPIC_SMALL_FAST_MODEL. Setting both is harmless and saves you checking which version you have.
Send X-Session-Id on long conversations
Sending the optional X-Session-Id header with a stable value for one logical conversation lets us route the turns of that conversation consistently, which significantly improves prefix cache hit rate — lower time-to-first-token and a cheaper cached-input rate on the shared prefix. If you omit the header, the server derives a key from the request content automatically. The full guidance is under Core Models.
Quote X-Llm-Request-Id when something goes wrong
Every response, errors included, carries an X-Llm-Request-Id response header. That is the id we use to find your request in our logs, so please include it in any support request.
Errors
Errors on this endpoint always use the Anthropic error envelope, whichever layer produced them: rejections from our own admission layer and non-2xx responses from the model upstream are both normalised into that shape, so you never have to parse a second error format. Our own error code is folded into message rather than added as an extra field — the official SDKs stringify the whole body, so you still see the code through an SDK.
stream: true.| Status | error.type | Description |
|---|---|---|
400 | invalid_request_error | Malformed body, an unsupported field, multimodal content, or max_tokens above 32768 on a non-streaming request. Replaying a redacted_thinking block also lands here. |
401 | authentication_error | Missing or invalid API key (x-api-key or Authorization). |
402 | billing_error | Insufficient balance. Top up in the console and retry. |
404 | not_found_error | Unknown model, or a model this account is not entitled to — /v1/messages serves the core models only. Also returned for an unknown path under /v1/messages/. |
429 | rate_limit_error | Account concurrency or TPM exceeded. Honour Retry-After and back off. |
503 | overloaded_error | The model upstream is overloaded. Retry with backoff. |
HTTP/1.1 404 Not Found
{
"type": "error",
"error": {
"type": "not_found_error",
"message": "model 'some-other-model' is not available on this endpoint; /v1/messages serves the core models only (see GET /v1/models)"
}
}Next steps
Same models, same prices — compare the two protocols, or read the throttling contract before you scale up.