πŸŽ“ Claude Architect KB CCAR-F Β· CCAR-P study guide

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)
Body application/json The request object below
Sibling endpoint POST /v1/messages/count_tokens Same body, returns {input_tokens} β€” no generation, not billed: tokens & counting them

REQUEST β€” every top-level field

The two required fields are model and max_tokens, plus a non-empty messages array. Everything else shapes how the model answers.

Field Type Required What it does Production / exam note
model string βœ… Which Claude answers (claude-sonnet-5, claude-opus-5, …) Exact id, no date suffix on current models
max_tokens int βœ… Hard ceiling on output tokens for this one call Too low β†’ stop_reason: max_tokens and a truncated response
messages array βœ… The whole conversation, oldest β†’ newest (anatomy below) Stateless: you re-send all of it every call
system string | array of text blocks β€” The operator instructions, outside the conversation Array form lets you put cache_control on it
tools array β€” The functions the model may request (anatomy below) Your code executes them β€” the API never does
tool_choice object β€” {type:"auto"} (default) Β· {type:"any"} Β· {type:"tool", name} Β· {type:"none"} tool = force one tool β€” the structured-output trick
stop_sequences array of strings β€” Your custom cut-offs; matched string returned in stop_sequence Halt right after the part you want
stream bool β€” true β†’ server-sent events instead of one JSON (below) Required by SDKs for very large max_tokens
thinking object β€” {type:"adaptive"} on current models β€” the model decides how much to reason Older models used {type:"enabled", budget_tokens}; current ones reject it
output_config object β€” effort (low…max) and format (a JSON Schema the whole answer must obey) format is the structured-outputs door; effort trades cost for depth
metadata object β€” {user_id} β€” an opaque id for abuse detection Never PII
cache_control object β€” Top-level {type:"ephemeral"} auto-caches the last cacheable block Or place breakpoints on individual blocks (Domain 5)
temperature Β· top_p Β· top_k number β€” Legacy sampling knobs Rejected (400) on current models β€” use effort instead

πŸͺ€ Trap: system and tools are not inside messages. 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 before every create inspecting & editing the history
Edit policy: shrink old tool_results, stub read images, redact secrets β€” never split a tool_use/tool_result pair after tool execution, before the next request what you edit, what you never touch
Cache where you put cache_control: tools β†’ system β†’ a moving breakpoint on the last message; verified by usage fixed once; the moving breakpoint every turn what to cache
Compact a measured threshold (tokens vs % of window / cost) β†’ drop Β· clear Β· summarize; or API-side compaction at a turn boundary, never mid tool call when and how to compact

Block types you SEND (inside content arrays):

Block Shape Where
text {type:"text", text} user or assistant
image {type:"image", source:{type:"base64"|"url", …}} user
document {type:"document", source:{…pdf/text…}, citations?} user
tool_result {type:"tool_result", tool_use_id, content, is_error?} user β€” answers a tool_use
tool_use Β· thinking echoed back exactly as received assistant β€” never fabricated by you

tools[] β€” anatomy

JSON
{
  "name": "get_order",
  "description": "Look up one order's shipping status by order id.",
  "input_schema": {
    "type": "object",
    "required": ["order_id"],
    "properties": { "order_id": { "type": "string" } },
    "additionalProperties": false
  },
  "strict": true
}
Field Role
name What the model writes in a tool_use block β€” and what your dispatcher switches on
description The model's only understanding of when to use it β€” write it for a reader who has never seen your code
input_schema JSON Schema for the arguments; additionalProperties: false makes it closed
strict 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.

Field Type What it tells you Your code does
id string Message id (msg_…) Log it β€” the correlation key for support and audit
type "message" Object kind Nothing (errors come as type: "error" β€” below)
role "assistant" Always the model Append the whole message under this role
model string The exact model that answered Assert it's what you asked for; pin in audit trail
content block[] An ARRAY of typed blocks β€” never a string Walk it; branch on each block's type
stop_reason enum WHY generation ended The first thing you check β€” the loop's control flow hangs off it
stop_sequence string | null Which of your stop_sequences fired Only non-null when stop_reason is stop_sequence
stop_details object | null Refusal category + explanation Populated only when stop_reason is refusal; guard before reading
usage object Token accounting for this call Meter cost; verify cache hits

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.

content[] β€” block types you RECEIVE

Block Shape Meaning Your code does
text {type:"text", text, citations?} Prose for the user Show it / parse it β€” after checking stop_reason
tool_use {type:"tool_use", id:"toolu_…", name, input} "Run this function with these arguments" Dispatch on name, validate input, execute, answer with a tool_result carrying the same id
thinking {type:"thinking", thinking, signature} The model's reasoning (text may be empty/summarized) Echo back unchanged on the next call; never edit
redacted_thinking {type:"redacted_thinking", data} Reasoning the API withheld Echo back unchanged
server_tool_use Β· web_search_tool_result … server-tool blocks Anthropic-hosted tools ran server-side Read results; nothing to execute
compaction (beta) {type:"compaction", …} Server summarized old history 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

Value Meaning Your code does
end_turn The model finished naturally Deliver the answer; the loop's terminal state
tool_use The model requested β‰₯1 tool Execute every tool_use block, append results in ONE user message, call again
max_tokens Output hit your ceiling Treat as incomplete β€” never parse; raise max_tokens, or continue
stop_sequence One of your strings matched 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).

When it is NOT a 200 β€” the error envelope

JSON
HTTP 429
{ "type": "error", "error": { "type": "rate_limit_error", "message": "…" } }
HTTP error.type Cause Your code does
400 invalid_request_error 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)
content_block_delta text_delta (text) Β· input_json_delta (tool arguments, partial JSON) Β· thinking_delta
content_block_stop That block is complete
message_delta stop_reason, stop_sequence, usage.output_tokens
message_stop Done
ping Β· error Keepalive Β· a mid-stream error object

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.

Raw JSON β†’ SDK β€” the translation table

Wire (this page) Python SDK TypeScript SDK So when you see… Source
POST /v1/messages + body client.messages.create(**body) client.messages.create(body) …a create call, picture the request table cookbook:patterns/agents/util.py:23
response.content[] resp.content β€” list of typed blocks resp.content β€” discriminated union …block.type == "text" β†’ block.text cookbook:tool_use/memory_demo/demo_helpers.py:81
response.stop_reason resp.stop_reason resp.stop_reason …the if/match at the top of every loop cookbook:tool_use/utils/visualize.py:74
response.usage.* resp.usage.input_tokens … resp.usage.input_tokens … …cost meters and cache-hit checks cookbook:misc/prompt_caching.ipynb
append assistant turn messages.append({"role":"assistant","content": resp.content}) messages.push({role:"assistant", content: resp.content}) …the line that makes the API "remember" cookbook:tool_use/memory_demo/demo_helpers.py:171
stream: true client.messages.stream(...) β†’ .get_final_message() client.messages.stream(...) β†’ .finalMessage() …events reassembled into the same object cookbook:capabilities/content_moderation/pipeline.py:151
the whole loop above client.beta.messages.tool_runner(...) client.beta.messages.toolRunner(...) …the SDK running this loop over your tools cookbook:tool_use/automatic-context-compaction.ipynb
the loop + built-in tools + context mgmt Claude Agent SDK query() query() …Claude Code's harness as a library β€” same two shapes underneath cookbook:claude_agent_sdk/research_agent/agent.py:69

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

JSON
{
  "id": "msg_01…", "type": "message", "role": "assistant", "model": "claude-sonnet-5",
  "content": [
    { "type": "text", "text": "Let me look that up." },
    { "type": "tool_use", "id": "toolu_01…", "name": "get_order", "input": { "order_id": "A-1042" } }
  ],
  "stop_reason": "tool_use", "stop_sequence": null, "stop_details": null,
  "usage": { "input_tokens": 812, "output_tokens": 57, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0 }
}

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:

JSON
"messages": [
  { "role": "user", "content": "Where is order A-1042?" },
  { "role": "assistant", "content": [ { "type": "text", "text": "Let me look that up." },
                                      { "type": "tool_use", "id": "toolu_01…", "name": "get_order", "input": { "order_id": "A-1042" } } ] },
  { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": "toolu_01…",
                                   "content": "{\"status\":\"in_transit\",\"eta\":\"2026-08-26\"}" } ] }
]

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.

Concept Cookbook (canonical)
The raw call cookbook:patterns/agents/util.py:23
system on the request cookbook:patterns/agents/util.py:26
tools[] β€” the catalogue cookbook:tool_use/utils/customer_service_tools.py:339
Walk content[] by type cookbook:tool_use/memory_demo/demo_helpers.py:81
tool_use β†’ execute cookbook:tool_use/memory_demo/demo_helpers.py:100
Build the tool_result cookbook:tool_use/memory_demo/demo_helpers.py:116
Branch on stop_reason cookbook:tool_use/utils/visualize.py:304
The loop cookbook:tool_use/memory_demo/demo_helpers.py:122
Human in the loop cookbook:managed_agents/CMA_gate_human_in_the_loop.ipynb
usage β†’ cost, cache hits cookbook:misc/prompt_caching.ipynb
Streaming cookbook:capabilities/content_moderation/pipeline.py:151
Thinking blocks echoed back cookbook:tool_use/memory_demo/demo_helpers.py:82
Reading a response into a tree cookbook:tool_use/utils/visualize.py:74

⚠️ 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.