Search docs

Search docs, endpoints, errors

Anthropic Messages API

POST /v1/messages, served natively — point the Anthropic SDK or Claude Code at Apodex and the core models answer.

POST/v1/messages
https://api.apodex.ai

Overview

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.

Base URL
https://api.apodex.ai
Authentication
x-api-key · Bearer API Key
Format
Anthropic-native
Endpoints
POST /v1/messages · POST /v1/messages/count_tokens
Models
apodex-1.1 · apodex-1.1-mini

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, Authorization wins.

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

POST/v1/messages

Set 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

x-api-key
YOUR_API_KEYRequired

Your Apodex API key. Alternatively send Authorization: Bearer YOUR_API_KEY.

content-type
application/jsonRequired

Always application/json.

anthropic-version
2023-06-01

Optional. Passed through to the upstream; the Anthropic SDKs set it for you.

X-Session-Id
my-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.

Parameters

model
stringRequired

Required. apodex-1.1 or apodex-1.1-mini.

messages
arrayRequired

Required. Standard Anthropic message list. Text content only.

max_tokens
integerRequired

Required by the protocol. Capped at 32768 when the request is non-streaming; uncapped (up to the context window) when streaming.

system
string | array

Optional. A plain string or an array of text blocks — both work.

stream
boolean

Optional, defaults to false. Set true for the full Anthropic SSE event stream.

tools
array

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.

thinking
object

Optional. 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

Non-Streaming
{
  "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:

message_startpingcontent_block_startcontent_block_deltacontent_block_stopmessage_deltamessage_stop

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

SSE
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

JSON
// 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.

Give 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_reasonDescription
end_turnThe model finished its answer on its own.
max_tokensThe generation hit the max_tokens budget and was truncated.
tool_useThe 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

POST/v1/messages/count_tokens

POST /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

x-api-key
YOUR_API_KEYRequired

Your Apodex API key. Alternatively send Authorization: Bearer YOUR_API_KEY.

content-type
application/jsonRequired

Always 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

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 featuresDescription
Server-side built-in tools are not supportedAnthropic'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 rejectedReplaying 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 effectbudget_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 32768Exactly 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 clockIndependently 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 onlyMultimodal input is not supported. Image, document and other non-text content blocks are rejected — send text.
Account-level rate limitsConcurrency 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

Anthropic's 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:

Pseudocode
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 * 2

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

Shell
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

claude

Claude 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.

One caveat, and it lives outside our HTTP layer: in extreme gateway-timeout situations — a non-streaming request hitting the roughly 600s wall-clock ceiling, or a connection dropped after the stream already started — the response can carry a non-JSON body, because it never reaches the code that would rewrap it. Treat a body that does not parse as JSON as a transport or timeout failure rather than an API error, and retry with stream: true.
Statuserror.typeDescription
400invalid_request_errorMalformed body, an unsupported field, multimodal content, or max_tokens above 32768 on a non-streaming request. Replaying a redacted_thinking block also lands here.
401authentication_errorMissing or invalid API key (x-api-key or Authorization).
402billing_errorInsufficient balance. Top up in the console and retry.
404not_found_errorUnknown 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/.
429rate_limit_errorAccount concurrency or TPM exceeded. Honour Retry-After and back off.
503overloaded_errorThe model upstream is overloaded. Retry with backoff.
Error format
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.

Docs | Apodex API Platform