Skip to content

Claude Messages Protocol

Claude Messages is Anthropic’s native conversation protocol. If your client is already developed according to Anthropic specifications, simply replace the Base URL and API Key with RouteAPI to use it directly without modifying the request structure.

Claude Messages uses a messages array to represent multi-turn conversations, an independent system field for system prompts, and requires explicit declaration of max_tokens. Compared to OpenAI-compatible format, its content block structure is more unified: text, images, tool calls, and tool results are all different type values in the same array.

Applicable scenarios:

ScenarioDescription
Claude CodeAnthropic’s official coding agent, only recognizes /v1/messages
Anthropic SDKanthropic Python / TypeScript SDK, just change base_url
Native message format clientsApplications already organized with content block structure
Extended thinking & prompt cachingDepends on Claude-specific capabilities like thinking, cache_control

If your client only supports OpenAI protocol, please use Chat Completions instead. RouteAPI will complete the necessary format adaptation internally, but prioritize the protocol natively supported by the client for best compatibility.

POST /v1/messages

Full address:

https://api.routeapi.ai/v1/messages

Request headers support two authentication methods, both using the same RouteAPI Token:

Authorization: Bearer sk-your-routeapi-token
Content-Type: application/json
x-api-key: sk-your-routeapi-token
anthropic-version: 2023-06-01
Content-Type: application/json

x-api-key is the default method for Anthropic SDK. RouteAPI automatically recognizes it as Token on the /v1/messages path, so official SDK requires no additional configuration. anthropic-version is passed through to upstream as-is, and official SDK will automatically include it.

FieldTypeRequiredDescription
modelstringYesModel ID, must be an available model for current account
messagesarrayYesConversation message list, at least one, role must alternate
max_tokensintegerYesMaximum output tokens, required by Claude protocol
systemstring/arrayNoSystem prompt, independent field, not in messages
temperaturenumberNoSampling temperature, range 0 to 1
top_pnumberNonucleus sampling parameter
top_kintegerNoSample only from the K most probable tokens
streambooleanNoWhether to use SSE streaming output
stop_sequencesarrayNoCustom stop sequences
toolsarrayNoTool definition list
tool_choiceobjectNoTool selection strategy
thinkingobjectNoExtended thinking configuration, depends on model support
metadataobjectNoRequest metadata, Claude-specific

This is the most common pitfall when migrating from OpenAI. OpenAI’s max_tokens uses model default limit when omitted, Claude protocol has no default value, and upstream will return invalid_request_error when missing.

{
"model": "claude-sonnet-4-5",
"max_tokens": 1024,
"messages": [{ "role": "user", "content": "Hello" }]
}

max_tokens is an output limit, does not include input tokens, and is not an exact length commitment: the model may finish early (stop_reason: "end_turn") or be truncated exactly at the limit (stop_reason: "max_tokens"). For production, set based on expected response length with some margin, and check stop_reason to determine if truncated.

Claude protocol does not accept messages with role: "system". System prompts must be placed in the top-level system field, and the messages array can only contain user and assistant.

Correct usage:

{
"model": "claude-sonnet-4-5",
"max_tokens": 1024,
"system": "You are a rigorous technical assistant, keep answers concise.",
"messages": [{ "role": "user", "content": "Explain what an API gateway is" }]
}

Incorrect usage (Claude protocol will reject):

{
"messages": [
{ "role": "system", "content": "You are a rigorous technical assistant." },
{ "role": "user", "content": "Explain what an API gateway is" }
]
}

system also supports array form for setting prompt caching on different paragraphs individually:

{
"system": [
{ "type": "text", "text": "You are a code review assistant." },
{
"type": "text",
"text": "Here is the complete project coding standard...",
"cache_control": { "type": "ephemeral" }
}
]
}

metadata carries request meta-information, currently only has one user_id field for upstream abuse detection. Do not place personally identifiable information like email or phone number here, recommend passing hash values or internal IDs.

{
"metadata": {
"user_id": "a3f1c2d4e5b6"
}
}

Each message in the messages array contains role and content fields. role can only be user or assistant, must alternate, and the first must be user.

content supports two forms. String is shorthand for single text:

{ "role": "user", "content": "Please introduce RouteAPI in one sentence" }

Array form consists of content blocks, each distinguished by type:

typeLocationDescription
textuser / assistantPlain text content
imageuserImage input, supports base64 and URL
documentuserDocument input, depends on model support
tool_useassistantModel requests to call tool
tool_resultuserTool execution result returned by client
thinkingassistantExtended thinking content block

Images are passed through the source field. Base64 method requires providing media_type as well:

{
"role": "user",
"content": [
{
"type": "image",
"source": {
"type": "base64",
"media_type": "image/jpeg",
"data": "/9j/4AAQSkZJRgABAQAAAQ..."
}
},
{ "type": "text", "text": "What controls are in this image?" }
]
}

URL method is more concise but requires the image address to be accessible by upstream:

{
"role": "user",
"content": [
{
"type": "image",
"source": {
"type": "url",
"url": "https://example.com/screenshot.png"
}
},
{ "type": "text", "text": "Describe the layout of this interface" }
]
}

Placing text blocks after image blocks usually works better. Multiple images can be included in one request, but will significantly increase input tokens, recommend compressing size first.

Claude’s tool definition is a flat structure with parameter schema field called input_schema:

{
"tools": [
{
"name": "get_weather",
"description": "Query current weather for specified city. Use full city name in Chinese.",
"input_schema": {
"type": "object",
"properties": {
"city": { "type": "string", "description": "City name, e.g. Beijing" },
"unit": { "type": "string", "enum": ["celsius", "fahrenheit"] }
},
"required": ["city"]
}
}
]
}

Compared to OpenAI’s nested structure, the difference is Claude has no outer type: "function" wrapper, no function nesting layer, and parameters is renamed to input_schema:

{
"tools": [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Query current weather for specified city.",
"parameters": { "type": "object", "properties": {} }
}
}
]
}

The quality of description directly determines whether the model will correctly choose the tool, recommend clearly stating purpose, parameter format, and boundary conditions.

FormatBehavior
{ "type": "auto" }Model decides whether to call tools, default value
{ "type": "any" }Must call tool, but model chooses which one
{ "type": "tool", "name": "get_weather" }Force call specified tool
{ "type": "none" }Prohibit tool calls

Adding "disable_parallel_tool_use": true can limit the model to initiate only one tool call at a time.

Tool calling is a complete conversational round-trip. After the model returns a tool_use block, you need to pass back the original assistant message along with execution results.

Step one, model returns tool call request:

{
"role": "assistant",
"content": [
{ "type": "text", "text": "Let me check the weather in Beijing." },
{
"type": "tool_use",
"id": "toolu_01A09q90qw90lq917835lq9",
"name": "get_weather",
"input": { "city": "Beijing", "unit": "celsius" }
}
]
}

Step two, add this assistant message as-is to messages, then append a user message carrying the result. tool_use_id must exactly match the id from previous step:

{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_01A09q90qw90lq917835lq9",
"content": "Beijing, sunny, temperature 23 celsius, humidity 45%."
}
]
}

When tool execution fails, use is_error marker to let the model know it needs to change strategy rather than retry:

{
"type": "tool_result",
"tool_use_id": "toolu_01A09q90qw90lq917835lq9",
"content": "Weather service timeout, no data retrieved.",
"is_error": true
}

Note that tool_result belongs to user role, Claude protocol has no independent role: "tool" like OpenAI. If the model returns multiple tool_use blocks at once, all corresponding tool_result must be placed in the content array of the same user message.

{
"id": "msg_01XFDUDYJgAACzvnptvVoYEL",
"type": "message",
"role": "assistant",
"model": "claude-sonnet-4-5",
"content": [
{
"type": "text",
"text": "RouteAPI is an API gateway that uniformly manages multiple AI model providers."
}
],
"stop_reason": "end_turn",
"stop_sequence": null,
"usage": {
"input_tokens": 24,
"output_tokens": 18
}
}

content is always an array, even with only one text segment. Clients should not assume content[0] is a text block; when the model enables extended thinking or initiates tool calls, the first block might be thinking or tool_use.

stop_reason values:

ValueMeaning
end_turnModel naturally finished response
max_tokensReached max_tokens limit and truncated
stop_sequenceHit a sequence in stop_sequences
tool_useModel requests to call tool, waiting for result

Setting stream: true returns SSE. Claude’s streaming format differs significantly from OpenAI: each event has an explicit event: type name, and the end marker is a message_stop event, not data: [DONE].

event: message_start
data: {"type":"message_start","message":{"id":"msg_01XFD","type":"message","role":"assistant","content":[],"model":"claude-sonnet-4-5","usage":{"input_tokens":24,"output_tokens":1}}}
event: content_block_start
data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"RouteAPI"}}
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" is an"}}
event: content_block_stop
data: {"type":"content_block_stop","index":0}
event: message_delta
data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":18}}
event: message_stop
data: {"type":"message_stop"}

Event type descriptions:

EventDescription
message_startMessage start, carries initial usage (output_tokens not accurate yet)
content_block_startA content block starts, index identifies position
content_block_deltaIncremental content, text uses text_delta, tool parameters use input_json_delta
content_block_stopCurrent content block ends
message_deltaMessage-level increment, carries final stop_reason and cumulative output_tokens
message_stopEntire response ends
pingHeartbeat event, can be ignored
errorError occurred mid-stream

Tool call parameters are returned as JSON strings piece by piece, need to concatenate all input_json_delta’s partial_json before parsing:

event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"{\"city\":"}}
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"\"Beijing\"}"}}

Group and accumulate by index, do not attempt to parse intermediate states during concatenation.

FieldDescription
input_tokensInput token count, excludes cache hit portion
output_tokensOutput token count
cache_creation_input_tokensTokens written to prompt cache
cache_read_input_tokensTokens read from prompt cache
server_tool_useServer-side tool usage, e.g. web_search_requests

In streaming response, output_tokens should use the value in message_delta event, the one in message_start is initial placeholder. Billing is based on console logs, actual supported fields depend on selected model.

Claude MessagesOpenAI Chat CompletionsDifference
modelmodelSame
system (top-level field)messages[0] with role: "system"Different location, Claude rejects system messages
messagesmessagesClaude only allows alternating user / assistant
max_tokensmax_tokens / max_completion_tokensClaude required, OpenAI optional
stop_sequencesstopDifferent name
temperaturetemperatureClaude limit 1, OpenAI limit 2
top_kNo equivalentOpenAI does not support
tools[].input_schematools[].function.parametersDifferent hierarchy and field name
tool_choice: {"type":"any"}tool_choice: "required"Different format
metadata.user_iduserDifferent location
thinkingreasoning_effortDifferent control method
No equivalentnClaude does not support generating multiple candidates
No equivalentfrequency_penalty / presence_penaltyClaude does not support
No equivalentresponse_formatClaude uses tools or prompts to constrain output structure

Response structure differences:

ItemClaude MessagesOpenAI Chat Completions
Top-level contentcontent arraychoices[0].message
Text locationcontent[0].textchoices[0].message.content
Stop reasonstop_reasonfinish_reason
Tool callstool_use blocks in contentmessage.tool_calls
Tool result roletool_result block in user messageIndependent role: "tool"
Input usageusage.input_tokensusage.prompt_tokens
Output usageusage.output_tokensusage.completion_tokens
Total fieldNone, must sum manuallyusage.total_tokens
Streaming endmessage_stop eventdata: [DONE]

When migrating from OpenAI to Claude Messages, check in this order:

  1. Move system message from messages array to top-level system field.
  2. Add max_tokens, this is required.
  3. Confirm first message in messages is user, and roles strictly alternate with no consecutive same-role messages.
  4. Remove type and function wrapper layers from tool definitions, rename parameters to input_schema.
  5. Change tool results from role: "tool" to tool_result block in user message, and align tool_use_id.
  6. If temperature was originally greater than 1, adjust down to Claude’s value range.
  7. Change response parsing to iterate through content array and dispatch by type, do not assume fixed indices.
  8. Change streaming parsing to dispatch by event: type, replace end condition with message_stop.

If refactoring cost is high, you can continue using OpenAI protocol to call Claude series models, with RouteAPI completing format conversion. The tradeoff is that some Claude-specific capabilities (like complete control of extended thinking, fine-grained prompt caching) cannot be fully expressed in OpenAI format.

curl:

Terminal window
curl https://api.routeapi.ai/v1/messages \
-H "x-api-key: $ROUTEAPI_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "Content-Type: application/json" \
-d '{
"model": "claude-sonnet-4-5",
"max_tokens": 1024,
"system": "You are a rigorous technical assistant, keep answers concise.",
"messages": [
{ "role": "user", "content": "Please introduce RouteAPI in one sentence" }
]
}'

Anthropic Python SDK, just change base_url:

import os
from anthropic import Anthropic
client = Anthropic(
api_key=os.environ["ROUTEAPI_KEY"],
base_url="https://api.routeapi.ai",
)
message = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=1024,
system="You are a rigorous technical assistant, keep answers concise.",
messages=[
{"role": "user", "content": "Please introduce RouteAPI in one sentence"},
],
)
print(message.content[0].text)
print(message.usage.input_tokens, message.usage.output_tokens)

base_url only needs the domain, SDK will automatically append /v1/messages. Streaming calls use client.messages.stream():

with client.messages.stream(
model="claude-sonnet-4-5",
max_tokens=1024,
messages=[{"role": "user", "content": "Explain step by step what an API gateway is"}],
) as stream:
for text in stream.text_stream:
print(text, end="", flush=True)
final = stream.get_final_message()
print()
print(final.stop_reason, final.usage.output_tokens)

curl, using base64:

Terminal window
curl https://api.routeapi.ai/v1/messages \
-H "x-api-key: $ROUTEAPI_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "Content-Type: application/json" \
-d '{
"model": "claude-sonnet-4-5",
"max_tokens": 1024,
"messages": [
{
"role": "user",
"content": [
{
"type": "image",
"source": {
"type": "base64",
"media_type": "image/jpeg",
"data": "'"$(base64 -w 0 screenshot.jpg)"'"
}
},
{ "type": "text", "text": "What UI controls are in this image?" }
]
}
]
}'

Python SDK:

import base64
import os
from anthropic import Anthropic
client = Anthropic(
api_key=os.environ["ROUTEAPI_KEY"],
base_url="https://api.routeapi.ai",
)
with open("screenshot.jpg", "rb") as f:
image_data = base64.standard_b64encode(f.read()).decode("utf-8")
message = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=1024,
messages=[
{
"role": "user",
"content": [
{
"type": "image",
"source": {
"type": "base64",
"media_type": "image/jpeg",
"data": image_data,
},
},
{"type": "text", "text": "What UI controls are in this image?"},
],
}
],
)
print(message.content[0].text)

Complete two-round trip, including result passing:

import json
import os
from anthropic import Anthropic
client = Anthropic(
api_key=os.environ["ROUTEAPI_KEY"],
base_url="https://api.routeapi.ai",
)
tools = [
{
"name": "get_weather",
"description": "Query current weather for specified city. Use full city name in Chinese.",
"input_schema": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "City name, e.g. Beijing"},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
},
"required": ["city"],
},
}
]
def get_weather(city: str, unit: str = "celsius") -> str:
# Replace with actual weather service call
return f"{city}, sunny, temperature 23 celsius, humidity 45%."
messages = [{"role": "user", "content": "What's the weather like in Beijing now?"}]
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=1024,
tools=tools,
messages=messages,
)
# Only need to execute tool and pass back when stop_reason is tool_use
if response.stop_reason == "tool_use":
# Original assistant message must be added back as-is, otherwise tool_use_id cannot align
messages.append({"role": "assistant", "content": response.content})
tool_results = []
for block in response.content:
if block.type != "tool_use":
continue
try:
result = get_weather(**block.input)
is_error = False
except Exception as exc:
result = f"Tool execution failed: {exc}"
is_error = True
tool_results.append(
{
"type": "tool_result",
"tool_use_id": block.id,
"content": result,
"is_error": is_error,
}
)
# All tool results from same round go in one user message
messages.append({"role": "user", "content": tool_results})
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=1024,
tools=tools,
messages=messages,
)
print(response.content[0].text)

Corresponding curl second-round request:

Terminal window
curl https://api.routeapi.ai/v1/messages \
-H "x-api-key: $ROUTEAPI_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "Content-Type: application/json" \
-d '{
"model": "claude-sonnet-4-5",
"max_tokens": 1024,
"tools": [
{
"name": "get_weather",
"description": "Query current weather for specified city.",
"input_schema": {
"type": "object",
"properties": { "city": { "type": "string" } },
"required": ["city"]
}
}
],
"messages": [
{ "role": "user", "content": "What'\''s the weather like in Beijing now?" },
{
"role": "assistant",
"content": [
{
"type": "tool_use",
"id": "toolu_01A09q90qw90lq917835lq9",
"name": "get_weather",
"input": { "city": "Beijing" }
}
]
},
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_01A09q90qw90lq917835lq9",
"content": "Beijing, sunny, temperature 23 celsius, humidity 45%."
}
]
}
]
}'
  • Actual parameter support depends on selected model and upstream service capabilities, optional capabilities like thinking, cache_control, mcp_servers should be verified in test environment first.
  • Optional parameters explicitly passed as 0 or false will be treated as user explicit settings, not discarded as default values.
  • Production environment should fix model ID, do not rely on temporary aliases or display names.
  • Record request ID, model ID, status code, and token usage for each request to facilitate troubleshooting latency and cost anomalies.
  • Error responses follow Claude’s {"type": "error", "error": {...}} structure, see Error Handling for details.