Anatomy of a Messages request
headers first, then the prompt prefix in order
- Headers
x-api-key,anthropic-version,content-type model·max_tokensboth required on every requesttoolsfirst part of the cacheable prefixsystemrole, rules, long stable contextmessagesthe whole conversation, as content blocks
The request and the response
Three fields are required: model, messages and max_tokens. Everything else is optional — system, tools, tool_choice, temperature, stop_sequences, stream, thinking, metadata, service_tier. Three headers travel with every raw HTTP call: your API key, the API version, and the content type. The client SDKs set the version header for you and read the key from the ANTHROPIC_API_KEY environment variable.
The response is a message object: an id, the model that answered, role of assistant, a content array of blocks, a stop_reason, and usage. Usage is where cost and caching become visible — input_tokens, output_tokens, cache_creation_input_tokens and cache_read_input_tokens. Reading stop_reason correctly is the subject of task statement 4.1; here it is enough to know it exists and that tool_use means the model is waiting on you.
from anthropic import Anthropic
client = Anthropic() # reads ANTHROPIC_API_KEY from the environment
resp = client.messages.create(
model=MODEL_ID, # pin this in configuration, not at the call site
max_tokens=1024, # required: the ceiling on output tokens
system="You are a claims triage assistant.",
messages=[{"role": "user", "content": "Summarise claim A-1042."}],
)
print(resp.stop_reason) # end_turn, tool_use, max_tokens, ...
print(resp.usage.input_tokens, resp.usage.output_tokens)
print(resp.content[0].text) # content is a list of blocks, not a stringTwo habits follow from that snippet. Never treat content as a string: it is a list, and a single turn can hold thinking, text and a tool request together. And never hard-code the model identifier at the call site — 2.6 covers why it belongs in configuration.
Content blocks: the data patterns
The messages array is not plain text. Each message's content is a list of typed blocks, and the block type is what determines how Claude receives the data. Knowing which block goes where is the single most testable piece of Messages API mechanics.
| Block | Direction | What it carries |
|---|---|---|
text | Both | Plain text |
image | You send | base64, url, or file source with a file_id |
document | You send | A PDF or text file, optionally with citations enabled |
tool_use | Claude returns | id, name, input — a request for you to run something |
tool_result | You send back | tool_use_id matching the request, plus content and optional is_error |
thinking | Claude returns | Reasoning text with a signature |
Images. Claude accepts JPEG, PNG, GIF and WebP, up to 8000 by 8000 pixels, with a base64 size limit of 10 MB on the Claude API (5 MB via Bedrock and Google Cloud). Token cost is geometric, not per-file: the image is divided into 28-by-28 pixel patches, so cost rises with area — roughly 1,296 tokens for a 1000-by-1000 image. Oversized images are downscaled automatically. Putting the image before the text performs best, and for multi-turn conversations the Files API is the better pattern: upload once, then reference file_id instead of re-encoding the same megabytes on every turn.
Tools. A tool definition is a name, a description and an input_schema in JSON Schema. tool_choice decides how freely Claude may use them: auto (default), any (must call something), a named tool, or none. When you return the result, tool_use_id must match the request, and a failure is reported with is_error rather than by inventing a plausible answer. Tool definitions are not free — they sit in the prefix and cost input tokens, plus a few hundred tokens of system overhead when tools is present at all. Tool design proper is domain 8; what matters here is the round-trip shape.
Streaming
Set stream: true and the response arrives as server-sent events instead of one body. The sequence is fixed: message_start carries the message shell with empty content; then, for each content block, a content_block_start, a run of content_block_delta events, and a content_block_stop; then one or more message_delta events; then message_stop. ping events may appear anywhere, and errors arrive as error events inside the stream.
The delta type tells you what is arriving: text_delta for text, thinking_delta and signature_delta for reasoning, and input_json_delta for a tool call's arguments. That last one catches people out — tool input arrives as fragments of JSON text, so you accumulate the partial_json strings and parse once, at content_block_stop. stop_reason and the final usage numbers appear on message_delta, not on message_start, which is why a streaming client cannot decide it is finished from the text alone.
The shape of a stream
stream: truemessage_start · empty contentcontent_block_startpartial_jsoncontent_block_stop · parse nowmessage_delta · stop_reason, usagemessage_stopcontent_block_stop, and stop_reason arrives on message_delta.Streaming is also an operational requirement, not only a user-experience choice. The documentation puts a ten-minute ceiling on a request and tells you to stream or use batches for anything expected to run longer; a large max_tokens on a non-streaming request is the classic way to hit it. The SDKs provide accumulators — get_final_message() in Python, finalMessage() in TypeScript — so you can stream for reliability and still work with one complete message at the end.
Thinking
Thinking gives the model room to reason before answering, and the reasoning comes back as thinking blocks with a signature. Thinking tokens are billed as output tokens and count inside the context window. On current models the parameter is thinking with type of adaptive, and depth is steered by output_config.effort at low, medium or high — the model decides how much reasoning each input deserves.
One multi-turn detail matters for tool loops: whether previous thinking blocks are preserved when you send the conversation back differs by model, and where they are preserved they are billed as input tokens. Either way, you pass back what the API gave you rather than editing the assistant turn, and you keep history append-only.
Prompt caching
Caching stores the processed form of a prompt prefix so repeated requests skip re-processing it. You mark the end of the cacheable region with cache_control of type ephemeral on a block — at most four explicit breakpoints per request. The default lifetime is five minutes, refreshed on each hit, with a one-hour option at a higher write price. Cache writes cost more than ordinary input; cache reads cost a fraction of it.
The prefix is assembled in a fixed order — tools, then system, then messages — and a change at any level invalidates that level and everything after it. Hence the one rule worth memorising: put the breakpoint at the end of the last block that is identical on every request. A timestamp, a user name or a per-request question above the breakpoint means a miss every single time.
Where the breakpoint goes
Never hitspython
system=[
{"type": "text",
"text": POLICY_MANUAL},
{"type": "text",
"text": f"Now: {now()}",
"cache_control":
{"type": "ephemeral"}},
]Hits every timepython
system=[
{"type": "text",
"text": POLICY_MANUAL,
"cache_control":
{"type": "ephemeral"}},
]
messages=[{"role": "user",
"content": question}]usage.cache_read_input_tokens to see which case you are in.Two consequences are worth carrying into design. Caching has a minimum size — short prefixes are simply not cached, with no error — so verify by reading cache_creation_input_tokens and cache_read_input_tokens rather than assuming. And on most models cached reads do not count toward your input-tokens-per-minute rate limit, so a well-cached design buys throughput headroom as well as money.
Realtime or batch
The Message Batches API takes up to 100,000 requests in one submission (256 MB), processes them asynchronously at a 50% discount, and guarantees results within 24 hours — most batches finish in under an hour. Each request carries a custom_id; results come back as JSONL, streamed from a results URL, not in submission order, with a result type per line of succeeded, errored, canceled or expired. Only successes are billed.
Two paths for the same model
Realtime (Messages API)
- A person or a caller is waiting on the answer
- Stream for perceived speed; ten-minute ceiling per request
- Full price; rate limits apply per minute
- Work arrives one item at a time
Batch (Message Batches API)
- Nobody waits; results within 24 hours
- 50% discount; up to 100,000 requests per batch
- No streaming; match results by
custom_id - The whole workload is known up front
Batches support the features you would expect — vision, tool use, system prompts, thinking, and prompt caching on a best-effort basis — but not stream: true, since results are collected as a file rather than pushed. A mixed design is common and correct: batch the overnight backlog, serve the live queue in realtime, and share the same prompts between them.
Third-party vendor access
Claude is also available through cloud providers — Amazon Bedrock, Google Cloud and Microsoft Foundry — and this appears on the exam because it changes integration mechanics, not model behaviour. The Messages request and response shape is the same. What changes is authentication, identifiers and feature coverage.
| Claude API | Amazon Bedrock | |
|---|---|---|
| Authentication | x-api-key header | AWS credentials or bearer token, IAM controlled |
| SDK client | Anthropic | AnthropicBedrock, from the bedrock extra |
| Model identifier | e.g. claude-sonnet-4-6 | Prefixed, e.g. anthropic.claude-sonnet-4-6, or an inference-profile ARN |
| Residency | Anthropic-operated | Global endpoints, or regional endpoints at a premium |
| Feature coverage | Everything | No Message Batches, no Files API; explicit cache breakpoints only |
The pattern to remember: a requirement expressed as “it must run in our cloud account” is satisfiable, but it removes some features and changes every identifier in your configuration. Check the feature list against your functional requirements before promising a migration — a design that depends on the Batches API cannot simply be repointed at Bedrock.
Traps the wrong answers are built from
| Tempting but wrong | Do this instead |
|---|---|
| Putting the cache breakpoint after per-request content such as a timestamp | Place it at the end of the last block that is byte-identical on every request. |
| Parsing a tool call's arguments from streaming deltas as they arrive | Accumulate partial_json and parse once at content_block_stop. |
| Re-encoding the same image on every turn of a conversation | Upload once with the Files API and reference file_id. |
Setting a very large max_tokens on a non-streaming request | Stream, or use the Batches API, for anything that may run beyond the ten-minute ceiling. |
| Assuming batch results come back in submission order | Match every result to its request by custom_id and handle errored and expired lines. |
| Promising a cloud-provider migration without checking feature coverage | Compare the required features against that platform's list first — batches and Files are not everywhere. |
You should now be able to
- Assemble a Messages request correctly and read
stop_reasonandusagefrom the response. - Choose the right content block for text, images, documents and tool results, and match
tool_use_id. - Consume a stream in order and know where
stop_reason, usage and tool arguments appear. - Place
cache_controlbreakpoints so the cache actually hits, and verify it from usage fields. - Decide between realtime and batch from whether a person is waiting and whether work is enumerable.
- State what changes when Claude is accessed through a cloud provider instead of the Claude API.