The Messages API Spec β Every Field In, Every Field Out
π§ Why this page exists: an agent is two JSON shapes and a loop. The request you send and the response you get back are the entire contract between your deterministic code and the model β every SDK, every agent framework, Claude Code itself, is sugar over these two objects. Learn both field-by-field once and roughly 80% of "agent knowledge" collapses into "and then my code branches on that field." The rest of this guide assumes you can see the JSON under every SDK call.
π New here? Why the contract is so small
The Claude API is stateless and synchronous: one HTTPS POST, one JSON in, one JSON out. It keeps nothing between calls β no session, no memory of your conversation. That is a design choice, not a limitation: it means the only state that exists is the state you hold and re-send, so your harness can inspect, edit, cache, compact, replay, or audit every byte of it. Every capability that looks stateful (multi-turn chat, tools, agents) is built by your code appending to the messages array and calling again. Once you see that, the request and response stop being "API details" and become the two data structures your whole system is organized around.
flowchart LR
H[Your harness deterministic code] -->|"REQUEST JSON model Β· max_tokens Β· system tools Β· messages[]"| A["POST /v1/messages"]
A -->|"RESPONSE JSON content[] Β· stop_reason usage"| H
H -->|"branch on stop_reason append assistant turn verbatim append tool_result β call again"| H
π‘ Every field name below is clickable β it opens a drill-down card with the same six tiles every time: type Β· required Β· who sets it Β· possible values Β· changes per turn? Β· side effects β plus origin, example and its place in the loop. Cards link to each other; the breadcrumb takes you back.
The envelope β headers and the endpoint
Piece
Value
Note
Endpoint
POST https://api.anthropic.com/v1/messages
Same body on Bedrock / Vertex β different base URL and cloud auth
Auth header
x-api-key: $ANTHROPIC_API_KEY
The SDK reads the env var for you
Version header
anthropic-version: 2023-06-01
Pins the wire format; the SDK sets it
Beta header
anthropic-beta: <flag>
Only for beta features (batches of flags, comma-separated)
Rejected (400) on current models β use effort instead
πͺ€ Trap: system and tools are not insidemessages. They ride alongside it as separate fields, re-sent each call β which is exactly what makes them a byte-identical stable prefix that prompt caching can hit while messages grows.
messages[] β anatomy
Each element is { "role": "user" | "assistant", "content": string | block[] } β drill into role and content. A string is shorthand for one text block.
Rule
Detail
First message is user
The conversation always starts with you
Roles alternate in spirit
Consecutive same-role messages are allowed and get merged β but the loop's natural rhythm is user β assistant β user β¦
The assistant turn is echoed back verbatim
After a response, append {role:"assistant", content: <response.content>} β the blocks, not just the text
Tool results ride in a user message
There is no "tool" role: the outside world reports back as user
One tool_result per tool_use, in ONE user message
Parallel tool calls β all their results in a single user message; a tool_result must reference a tool_use_id that exists in history
No assistant prefill
Ending messages with an assistant message to "start the answer" is rejected (400) on current models β use output_config.format or tool_choice instead
Its lifecycle in production β four decisions your harness makes about this array, every turn. None of them is the model's:
Concern
What decides
When in the loop
Drill down
Inspect
token count vs window and budget Β· injection scan of tool results Β· audit log keyed by response id
true β the API guarantees tool_use.input validates exactly against the schema
How the model chooses among many β it reads this catalogue as text, every call, and picks by the meaning of name + description + parameter descriptions. No registry, no index. The full story with an annotated definition: how the model picks a tool.
RESPONSE β every top-level field
HTTP 200 carries this object. Read it in this order: stop_reason β content[] β usage.
usage fields: input_tokens (uncached, full price) Β· output_tokens Β· cache_creation_input_tokens (written to cache, ~1.25Γ) Β· cache_read_input_tokens (served from cache, ~0.1Γ). If cache_read_input_tokens stays 0 across identical-prefix calls, a silent cache invalidator is in your prompt.
Must be preserved when you append β it replaces the compacted history
πͺ€ Trap: content[0].text is the bug that ships. A text block may precede the tool_use block ("Let me look that up." then the call), thinking blocks may come first, and a tool_use-only turn has no text at all. Always iterate and branch on type.
stop_reason β the closed enum your loop branches on
Output is intentionally cut; stop_sequence says which
refusal
Safety classifier declined
Read stop_details; escalate β do not auto-retry in a loop
pause_turn
A long server-tool turn paused
Send the response back as-is to resume
π― Exam lens: the set is defined by Anthropic and closed β your harness and your tools can't add values. "Which field tells the harness whether to run a tool or return to the user?" is stop_reason, not the presence of a tool_use block (though in practice they agree).
Malformed body β bad field, orphan tool_result, prefill, rejected param
Fix the request; never retry
401
authentication_error
Bad / missing key
Fix credentials
403
permission_error
Key lacks access to this model/feature
Fix entitlement
404
not_found_error
Unknown model or resource
Fix the id
413
request_too_large
Body over the size limit
Shrink inputs
429
rate_limit_error
Your throughput ceiling
Exponential backoff + jitter; honor retry-after
500
api_error
Anthropic-side fault
Retry with backoff
529
overloaded_error
Anthropic-side saturation
Retry with backoff; consider a fallback model
The SDKs raise typed exceptions for these (RateLimitError, BadRequestError, β¦) and auto-retry 429/5xx a couple of times β but the decision "retryable vs not" is yours to encode.
Streaming β the same fields, delivered as events
With "stream": true the response arrives as server-sent events. Nothing new is invented: the events reassemble into the exact object above.
Event
Carries
message_start
The message skeleton: id, model, role, empty content, usage.input_tokens
content_block_start
A new block and its index (text / tool_use / thinking)
The SDK's stream.get_final_message() / finalMessage() does the reassembly β you get back the same Message object, stop_reason and all.
The loop in 14 lines β where each field is read
PYTHON
# Abridged/pseudocode: MODEL, tools, run_tool(), and deliver() are yours.
messages = [{"role": "user", "content": "Where is order A-1042?"}]
while True:
resp = client.messages.create(model=MODEL, max_tokens=1024,
system=SYSTEM, tools=tools, messages=messages)
messages.append({"role": "assistant", "content": resp.content}) # echo verbatim (blocks, not text)
if resp.stop_reason == "tool_use": # 1. branch on stop_reason FIRST
results = [{"type": "tool_result", "tool_use_id": b.id, # 2. one result per tool_useβ¦
"content": run_tool(b.name, b.input)}
for b in resp.content if b.type == "tool_use"] # β¦walking content[] by type
messages.append({"role": "user", "content": results}) # 3. β¦all in ONE user message
continue
if resp.stop_reason == "max_tokens": raise Truncated(resp.id) # never parse a truncated turn
return deliver([b.text for b in resp.content if b.type == "text"]) # end_turn (refusal β escalate)
Every line touches a field on this page β nothing else. That is the harness; Domain 1 adds the budgets, gates and audit trail around it.
π― Exam lens β what CCAR-F actually tests here: (1) the API is stateless, so the full messages array is re-sent; (2) stop_reason is checked before anything is parsed, and max_tokens means incomplete; (3) a tool_result answers a specific tool_use_id, rides in a user message, and parallel results share one message; (4) system/tools live outside messages and form the cacheable prefix; (5) input_schema with additionalProperties: false is a closed schema β the model is steered by it, your harness still validates.
π See it run β one full request β response β request cycle, every field visible
Call 1 β request (the customer's message just arrived via a webhook):
JSON
{
"model": "claude-sonnet-5",
"max_tokens": 1024,
"system": "You are a support agent for an online storeβ¦",
"tools": [{ "name": "get_order", "description": "β¦", "input_schema": { "β¦": "β¦" }, "strict": true }],
"messages": [{ "role": "user", "content": "Where is order A-1042?" }]
}
Call 1 β response β stop_reason says run a tool; note the text block before the tool_use:
Harness (deterministic): sees tool_use β validates input against the schema β queries the order database β builds the next request. Call 2 β request: system and tools unchanged (cacheable prefix), messages grew by two:
Call 2 β response β end_turn, one text block, and cache_read_input_tokens shows the prefix hit:
JSON
{
"id": "msg_02β¦", "type": "message", "role": "assistant", "model": "claude-sonnet-5",
"content": [ { "type": "text", "text": "Order A-1042 is in transit and should arrive by August 26." } ],
"stop_reason": "end_turn", "stop_sequence": null, "stop_details": null,
"usage": { "input_tokens": 96, "output_tokens": 23, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 790 }
}
The harness delivers the text. Two calls, two JSON shapes, one if on stop_reason β that is the whole agent.
See it in code β dual knowledge
Every </> chip below (and the SOURCE row on every field card) points at Anthropic's cookbook β the canonical example of the field in use. Locally the chip opens the file in the running IntelliJ at that line; on the public site it opens GitHub, pinned to the exact commit the refs were resolved against, so the line numbers stay true.
β οΈ Field drift is real. Anthropic adds fields (stop_details, effort, compaction blocks) and retires others (budget_tokens, sampling knobs, prefill) between model generations. The shape on this page is stable; the exact set of optional fields is whatever the current API reference↗ says β read the response as an open object and branch only on the fields you know.