Claude API Request Response Format Explained
What the Claude API request and response format looks like
The Claude API uses a JSON-based request/response format built around a messages array, similar in shape to other modern LLM APIs but with a few Claude-specific fields. A request is a POST with a model, a messages array, and a max_tokens value; the response is a JSON object containing a content array, a stop_reason, and a usage object with token counts.
If you're integrating Claude for the first time, the fastest way to understand the format is to look at a minimal request and its corresponding response side by side, then walk through what each field actually means and when it changes shape (tool use, streaming, multi-turn conversations).
The request format
A standard request to Claude's Messages API looks like this:
curl https://api.anthropic.com/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d '{
"model": "claude-sonnet-4-5-20250929",
"max_tokens": 1024,
"messages": [
{"role": "user", "content": "Explain what a race condition is."}
]
}'
Key fields:
model— the model identifier string. This determines cost and capability, and changes across versions.max_tokens— required. This caps the output, not the total conversation length.messages— an array of turns, each with arole(userorassistant) andcontent. Content can be a plain string or an array of content blocks (text, images, tool results).system(optional) — a top-level field, separate frommessages, used for system-level instructions.temperature,top_p,top_k(optional) — sampling controls.stream(optional) — set totrueto receive server-sent events instead of a single JSON body.tools(optional) — an array of tool definitions if you want Claude to call functions.
One detail that trips people up: content is not always a string. For multi-modal input or when you're passing tool results back, content becomes an array of typed blocks, e.g. {"type": "text", "text": "..."} or {"type": "image", "source": {...}}.
The response format
A non-streaming response has this general shape:
{
"id": "msg_01XYZ...",
"type": "message",
"role": "assistant",
"model": "claude-sonnet-4-5-20250929",
"content": [
{
"type": "text",
"text": "A race condition occurs when..."
}
],
"stop_reason": "end_turn",
"stop_sequence": null,
"usage": {
"input_tokens": 14,
"output_tokens": 187
}
}
Fields worth knowing:
contentis always an array, even for plain text answers. Don't assumecontent[0].textwill always be the only block — with tool use,contentcan include multiple blocks (text plustool_useblocks).stop_reasontells you why generation stopped:end_turn,max_tokens,stop_sequence, ortool_use. Checking this matters more than people think — a response cut off bymax_tokenslooks identical to a complete one unless you inspect this field.usagegives input and output token counts, which is what you'd use for cost tracking or rate-limit budgeting.roleis always"assistant"for a response — Claude doesn't return a role you didn't ask for.
How the format changes with tool use
When Claude decides to call a tool, stop_reason becomes "tool_use" and content includes a block like:
{
"type": "tool_use",
"id": "toolu_01A...",
"name": "get_weather",
"input": {"location": "Berlin"}
}
Your application executes the tool, then sends a follow-up request where the messages array includes a tool_result content block referencing that id. This request/response round-trip is the core pattern for function calling with Claude — see the tools documentation for the full schema.
How the format changes with streaming
With "stream": true, instead of one JSON object you get a sequence of SSE events: message_start, content_block_start, content_block_delta (repeated), content_block_stop, message_delta, and message_stop. Each content_block_delta carries a small text fragment, and you concatenate them to reconstruct the full response. The final message_delta event carries the cumulative usage and stop_reason.
Simplifying the format with SubToAPI
If you're already paying for Claude through a subscription and want an HTTP API without managing separate API billing, SubToAPI turns your existing Claude access into a standard HTTPS endpoint. The request and response format is intentionally close to what's described above — messages, max_tokens, content blocks, usage, stop_reason — so anything you build against the standard Messages format works with minimal changes. You authenticate with an application key (sub_live_...) instead of managing raw provider credentials:
curl https://api.subtoapi.app/v1/messages \
-H "Authorization: Bearer $SUBTOAPI_KEY" \
-H "content-type: application/json" \
-d '{
"model": "claude-sonnet-4-5-20250929",
"max_tokens": 1024,
"messages": [
{"role": "user", "content": "Explain what a race condition is."}
]
}'
The response comes back in the same shape — content array, stop_reason, usage — so you can point existing code at it. Full field-by-field reference is in the messages docs, streaming details are in the streaming docs, and a working end-to-end example is in the quickstart. Plans start at €9/month with a free trial at signup; seat pricing for teams is on the pricing page.
Practical tips for working with the format
- Always check
stop_reasonbefore trusting that a response is complete — don't just readcontent[0].textand move on. - Treat
contentas an array from day one, even if your first few tests only ever return one text block. Tool use and multi-block responses will break naivecontent[0]assumptions later. - Log
usage.input_tokensandusage.output_tokensper request if you're tracking cost — it's more accurate than estimating from string length. - Keep
systemout of themessagesarray — it's a separate top-level field, and mixing it into auser/assistantturn is a common mistake when porting code from other APIs.
Questions
Is the Claude API request format the same for every model version? Yes, the messages/max_tokens/content structure is stable across model versions — you generally only change the model string, not the request shape.
Why is content an array instead of a plain string in the response? Because a single response can contain multiple typed blocks — text, tool calls, or (in some cases) multiple text segments — so the array format keeps the schema consistent whether there's one block or several.
Do I need to parse SSE manually for streaming responses? You need to handle the event stream (accumulating content_block_delta events), but most official and third-party SDKs, including standard HTTP clients against SubToAPI's streaming endpoint, handle the parsing for you.