Claude API Mechanics

Learning Objectives

Pre-Reading Check — The Messages API

Which three fields are the required top-level fields of a Messages API request body?

model, system, messages model, max_tokens, messages endpoint, max_tokens, content model, max_tokens, stream

Where does the primary system prompt belong in a request?

As a {"role":"system"} entry at the top of messages[] As the top-level system parameter, separate from messages[] As the last entry in messages[] with role assistant In an HTTP header named x-system-prompt

Why must a client resend the entire conversation history on every turn?

Because the API compresses old turns and needs them re-uploaded Because the Messages API is stateless and keeps no memory of prior calls Because each turn uses a different endpoint Because max_tokens resets the server-side session

Why should robust code never assume content[0] is a text block?

content is always a plain string, so index 0 is a character content is an array; the first block may be thinking or tool_use The array is reversed, so text is always last Index 0 always holds usage metadata

1. The Messages API

Key Points

  • Every capability reaches your code through one endpoint: POST /v1/messages. There is no separate vision, tools, or streaming endpoint.
  • The three required top-level fields are model, max_tokens, and a non-empty messages array.
  • The system prompt is a top-level system parameter, never a {"role":"system"} entry in messages[] (except as a model-gated mid-conversation feature).
  • The response content is always an array of content blocks—always iterate and branch on each block's type.
  • The API is stateless: resend the full conversation history on every request.

Reasoning, tool use, vision, extended thinking—everything reaches your code through a single doorway: the Messages API, POST /v1/messages. Every request carries three mandatory headers: x-api-key, anthropic-version: 2023-06-01, and content-type: application/json (a fourth, anthropic-beta, is added for beta features).

Required top-level fields

FieldTypeMeaning
modelstringA model ID, e.g. claude-opus-4-8
max_tokenspositive integerMaximum tokens Claude may generate in its reply
messagesnon-empty arrayThe conversation so far

A minimal, well-formed request:

{
  "model": "claude-opus-4-8",
  "max_tokens": 1024,
  "messages": [
    { "role": "user", "content": "What is the capital of France?" }
  ]
}

The response's most important field is content—an array of content blocks, not a plain string:

{
  "id": "msg_01ABC...",
  "type": "message",
  "role": "assistant",
  "content": [
    { "type": "text", "text": "The capital of France is Paris." }
  ],
  "stop_reason": "end_turn",
  "usage": {
    "input_tokens": 14,
    "output_tokens": 9,
    "cache_creation_input_tokens": 0,
    "cache_read_input_tokens": 0
  }
}

The stop_reason tells you why Claude stopped; usage reports token accounting. Because content is always an array, robust code never assumes content[0] is text—with thinking or tool use enabled, the first block may be a thinking or tool_use block. Always iterate and branch on each block's type.

Figure 5.1: Messages API request/response round-trip

sequenceDiagram participant App as Your Application participant API as Messages API (POST /v1/messages) App->>API: POST request (model, max_tokens, messages) Note over API: Claude generates the reply API-->>App: Response (content[] blocks, stop_reason, usage) Note over App: Iterate content[] and branch on each block's type

System prompts vs. user/assistant messages

Here is the mistake that catches almost every newcomer. The system prompt is a top-level system parameter, not an entry in the messages array:

{
  "model": "claude-opus-4-8",
  "max_tokens": 1024,
  "system": "You are a terse assistant. Answer in one sentence.",
  "messages": [
    { "role": "user", "content": "Explain photosynthesis." }
  ]
}

The tempting error is to write {"role": "system", "content": "..."} inside messages[]. Think of the system parameter as the job description posted on the wall; messages is the transcript of the actual conversation. The system parameter accepts a plain string or an array of text blocks (the array form is needed for cache_control). Within messages, valid roles are user and assistant. Newer models such as Claude Opus 4.8 allow a model-gated mid-conversation system message; on models that don't support it, that entry returns a 400: role 'system' is not supported on this model. The rule to memorize: the primary system prompt is always the top-level system parameter.

Statelessness and multi-turn conversations

The Messages API is stateless—the server keeps no memory of prior calls. To hold a multi-turn conversation, you resend the full history on every request:

{
  "model": "claude-opus-4-8",
  "max_tokens": 1024,
  "messages": [
    { "role": "user", "content": "My name is Alice." },
    { "role": "assistant", "content": "Nice to meet you, Alice." },
    { "role": "user", "content": "What's my name?" }
  ]
}

Three rules govern the array: (1) the first message must be user; (2) roles conventionally alternate; (3) content may be a plain string or an array of blocks (text, image, document, tool_use, tool_result, thinking, redacted_thinking). Because history is resent every turn, long conversations grow in cost—which is exactly why prompt caching matters.

[Animation slot: two-column diagram — left "Top-level parameters" (model, max_tokens, system, tools), right "messages[]" showing alternating user/assistant bubbles — making the separation visually unmistakable.]
Post-Reading Check — The Messages API

Which three fields are the required top-level fields of a Messages API request body?

model, system, messages model, max_tokens, messages endpoint, max_tokens, content model, max_tokens, stream

Where does the primary system prompt belong in a request?

As a {"role":"system"} entry at the top of messages[] As the top-level system parameter, separate from messages[] As the last entry in messages[] with role assistant In an HTTP header named x-system-prompt

Why must a client resend the entire conversation history on every turn?

Because the API compresses old turns and needs them re-uploaded Because the Messages API is stateless and keeps no memory of prior calls Because each turn uses a different endpoint Because max_tokens resets the server-side session

Why should robust code never assume content[0] is a text block?

content is always a plain string, so index 0 is a character content is an array; the first block may be thinking or tool_use The array is reversed, so text is always last Index 0 always holds usage metadata
Pre-Reading Check — Streaming and Realtime Behavior

In the canonical streaming sequence, which event carries the final stop_reason and cumulative usage.output_tokens?

message_start content_block_delta message_delta message_stop

Which delta.type streams a chunk of a tool call's JSON input piecemeal?

text_delta input_json_delta thinking_delta signature_delta

What is the primary tradeoff of the Message Batches API compared to realtime?

Lower output quality in exchange for lower cost Higher latency in exchange for a 50% token discount, with identical quality Faster responses at a higher price Access to models unavailable in realtime

When collecting batch results, why must you key them by custom_id rather than by position?

Because positions are 1-indexed in batches Because results can arrive in any order Because custom_id is required to authenticate results Because positional access truncates the response

2. Streaming and Realtime Behavior

Key Points

  • Set stream: true to receive the response incrementally as Server-Sent Events (SSE); strongly recommended for large max_tokens to avoid HTTP timeouts.
  • Memorize the event sequence: message_startcontent_block_startcontent_block_deltacontent_block_stopmessage_deltamessage_stop.
  • The final stop_reason and cumulative output tokens arrive on message_delta—not on the text deltas.
  • The Batch API trades latency for a 50% discount on all tokens with identical quality.
  • Batch results arrive in any order—key them by custom_id, never by position.

By default a request blocks until Claude finishes, then returns the whole response at once. Streaming (stream: true, or an SDK helper such as client.messages.stream()) pushes the response back incrementally as Server-Sent Events—the server holds one HTTP connection open and emits small event:/data: chunks. The analogy: non-streaming is a next-day newspaper (complete, but you wait); streaming is a live commentator (each play as it happens).

The canonical event sequence

EventFiresCarries
message_startOnce, at the beginningMessage metadata (id, model, empty content, usage)
content_block_startWhen each block beginsindex and the block's initial shell
content_block_deltaRepeatedly, per chunkIncremental content (see delta types)
content_block_stopWhen each block finishesThe block's index
message_deltaNear the endstop_reason and cumulative usage.output_tokens
message_stopOnce, at the endSignals the stream is complete

Figure 5.2: SSE streaming event sequence

sequenceDiagram participant Client as Streaming Client participant Server as Messages API (stream: true) Client->>Server: POST request with stream: true Server-->>Client: message_start (metadata, empty content) Server-->>Client: content_block_start (index, block shell) loop Per chunk Server-->>Client: content_block_delta (text_delta / input_json_delta / thinking_delta) end Server-->>Client: content_block_stop (index) Server-->>Client: message_delta (stop_reason, cumulative usage) Server-->>Client: message_stop (stream complete)

The heart of the stream is content_block_delta, whose delta.type tells you the increment kind:

Handling partial output

Because the response arrives in pieces, streaming code must accumulate. In practice you rarely hand-assemble deltas: SDK helpers (stream.get_final_message() in Python, stream.finalMessage() in TypeScript) reconstruct the complete Message object while still letting you render text as it arrives. Two cautions: a stream can be interrupted mid-flight (design your UI to tolerate partial responses), and you must track message_delta to capture the final stop_reason.

Realtime vs. batch tradeoffs

Realtime—streamed or one-shot—gives you an answer now, ideal for anything interactive. The Message Batches API (POST /v1/messages/batches) is the async alternative: submit up to 100,000 {custom_id, params} pairs, processed in the background at 50% of the standard token price. You poll processing_status until it reads "ended". Most batches finish within an hour (max 24h; results retained 29 days). Critically, results arrive in any order—key by custom_id, never position.

DimensionRealtimeBatch
CostStandard50% cheaper on all tokens
LatencyImmediate"Right-time" — usually < 1h, max 24h
ThroughputPer-minute rate limitsVery high volume in one submission
QualityIdenticalIdentical
Best forChat, agents, user-facingBulk classification, summarization, evals, labeling

The essential exam point: there is no quality difference between batch and realtime—you trade latency for cost and throughput, nothing more.

[Animation slot: a streaming timeline that lights up each SSE event in order as tokens flow in, with the message_delta event flagged as "where stop_reason arrives."]
Post-Reading Check — Streaming and Realtime Behavior

In the canonical streaming sequence, which event carries the final stop_reason and cumulative usage.output_tokens?

message_start content_block_delta message_delta message_stop

Which delta.type streams a chunk of a tool call's JSON input piecemeal?

text_delta input_json_delta thinking_delta signature_delta

What is the primary tradeoff of the Message Batches API compared to realtime?

Lower output quality in exchange for lower cost Higher latency in exchange for a 50% token discount, with identical quality Faster responses at a higher price Access to models unavailable in realtime

When collecting batch results, why must you key them by custom_id rather than by position?

Because positions are 1-indexed in batches Because results can arrive in any order Because custom_id is required to authenticate results Because positional access truncates the response
Pre-Reading Check — Multi-Format Input and Vision

An image block's source can be one of three kinds. Which set is correct?

base64, url, file binary, path, stream base64, hex, link url, blob, upload

How is vision priced?

A flat fee per image regardless of size One visual token per 28×28-pixel block One token per pixel Free—images do not consume tokens

In a multi-turn or tool-use flow, what must you do with thinking blocks from a prior assistant turn?

Strip them out before the next request to save tokens Pass them back unchanged, preserving the signature Rewrite them into a summary and resend Move them into the top-level system parameter

On current models (Opus 4.8, Sonnet 5, Fable 5), how do you enable extended thinking?

thinking: {"type": "enabled", "budget_tokens": 8000} thinking: {"type": "adaptive"} stream: true automatically enables it A top-level reasoning: true flag

What does the display sub-field of the thinking parameter control, and what is its billing effect?

It controls visibility only; you are billed for thinking tokens regardless Setting it to "omitted" makes thinking free It controls how many tokens Claude may reason with It switches the model between vision tiers

3. Multi-Format Input and Vision

Key Points

  • Compose multi-format input by mixing image, document, and text blocks in a single user message; place media before the text that references it.
  • Image/document sources come in three kinds: base64, url, and file (a Files API file_id).
  • Vision is priced in 28×28-pixel visual tokens across high-resolution and standard tiers; PDFs rely on vision (each page is text + image).
  • Enable extended thinking with thinking: {"type": "adaptive"}; the deprecated budget_tokens form returns a 400.
  • thinking blocks carry a signature and must be echoed back unchanged in multi-turn/tool-use flows.

The content array is not limited to text. You compose multi-format input by mixing content blocks of different type values inside a single user message.

Figure 5.3: Composing a multi-format user message

flowchart LR Img["image block: source (base64 / url / file)"] --> Msg["Single user message: content[]"] Doc["document block: PDF / text source"] --> Msg Txt["text block: the question about the media"] --> Msg Msg --> Req["Messages API request"]

An image block carries a source of one of three kinds: base64 (raw bytes + media_type, supported everywhere), url (a link Claude fetches), or file (a file_id from the Files API). A vision request:

{
  "model": "claude-opus-4-8",
  "max_tokens": 1024,
  "messages": [
    {
      "role": "user",
      "content": [
        {
          "type": "image",
          "source": {
            "type": "base64",
            "media_type": "image/png",
            "data": "<base64-encoded bytes>"
          }
        },
        { "type": "text", "text": "What's in this image?" }
      ]
    }
  ]
}

Supported image formats: JPEG, PNG, GIF, WebP. Claude works best with image-then-text ordering. Documents use a document block with the same three source types; PDF support relies on vision (each page is processed as both text and image, inheriting vision's costs). Plain-text files (.txt, .csv, .md) work, but binary .xlsx/.docx are not supported.

Vision pricing and tiers

Claude views images in patches: each 28×28-pixel block equals one visual token, so an image costs roughly ⌈width/28⌉ × ⌈height/28⌉ tokens.

TierModelsMax long edgeMax visual tokens
High-resolutionFable 5, Opus 4.8, Opus 4.7, Sonnet 5 (and Mythos 5)2576 px4784
StandardAll other models1568 px1568

A 1000×1000 image costs about 1,296 tokens; a 1920×1080 image on the high-res tier costs roughly 2,691 tokens. Other limits: max 100 images per request on 200K-context models (600 on others), max 8000×8000 px, and up to 10 MB per image on the first-party API.

Thinking blocks in responses

When extended thinking is enabled, reasoning surfaces as thinking content blocks with three fields: type: "thinking", a thinking string, and a signature (an opaque, encrypted string the API uses to verify reasoning across turns). On current models you enable it with adaptive thinkingthinking: {"type": "adaptive"}. The older {"type": "enabled", "budget_tokens": N} form is removed and returns a 400. You control depth with output_config: {"effort": ...}, and a display sub-field controls visibility ("summarized" vs "omitted")—but display affects visibility only: you are billed for thinking tokens regardless.

{
  "model": "claude-opus-4-8",
  "max_tokens": 8000,
  "thinking": { "type": "adaptive", "display": "summarized" },
  "output_config": { "effort": "high" },
  "messages": [
    { "role": "user", "content": "Solve this step by step: 27 * 453" }
  ]
}

One rule you must not violate: in a multi-turn or tool-use conversation on the same model, you must pass thinking blocks back unchanged—echo [thinking_block, tool_use_block] in the assistant turn before returning your tool_result. Modifying a thinking block returns an invalid_request_error. Think of the signature as a wax seal: read the letter, but break the seal and the API won't accept it back.

[Animation slot: an image being tiled into a 28×28 grid, each cell ticking up a visual-token counter, with a tier toggle showing standard vs. high-resolution token totals.]
Post-Reading Check — Multi-Format Input and Vision

An image block's source can be one of three kinds. Which set is correct?

base64, url, file binary, path, stream base64, hex, link url, blob, upload

How is vision priced?

A flat fee per image regardless of size One visual token per 28×28-pixel block One token per pixel Free—images do not consume tokens

In a multi-turn or tool-use flow, what must you do with thinking blocks from a prior assistant turn?

Strip them out before the next request to save tokens Pass them back unchanged, preserving the signature Rewrite them into a summary and resend Move them into the top-level system parameter

On current models (Opus 4.8, Sonnet 5, Fable 5), how do you enable extended thinking?

thinking: {"type": "enabled", "budget_tokens": 8000} thinking: {"type": "adaptive"} stream: true automatically enables it A top-level reasoning: true flag

What does the display sub-field of the thinking parameter control, and what is its billing effect?

It controls visibility only; you are billed for thinking tokens regardless Setting it to "omitted" makes thinking free It controls how many tokens Claude may reason with It switches the model between vision tiers
Pre-Reading Check — Tools and Third-Party Access

After Claude returns a tool_use block with stop_reason: "tool_use", what is the correct next step?

Anthropic's servers execute the tool and continue automatically Your code executes the tool, then sends a tool_result block whose tool_use_id matches Retry the same request until stop_reason becomes end_turn Put the tool output in the top-level system parameter

When Claude requests several tools at once, how should you return the results?

One tool_result per user message, split across several messages All tool_result blocks in a single user message Concatenated into one text block In the system parameter as an array

Which model-ID rule is correct for Amazon Bedrock vs. Google Vertex AI?

Bedrock uses the bare ID; Vertex uses an anthropic. prefix Bedrock uses an anthropic. prefix; Vertex uses the bare (unprefixed) ID Both require the anthropic. prefix Neither accepts a model ID; the region selects the model

Prompt caching is a prefix match. In what order does Claude render the request prefix?

messagessystemtools toolssystemmessages systemtoolsmessages Order does not matter for cache matching

Which usage counter confirms a cache hit, and why might it stay zero across identical-prefix requests?

cache_read_input_tokens; a silent invalidator such as an interpolated datetime.now() or UUID output_tokens; because caching only applies to output cache_creation_input_tokens; because reads are never counted input_tokens; because the cache is disabled by default

Which feature is first-party only (a known gap on both Bedrock and Vertex)?

Extended thinking Explicit cache_control prompt caching The Message Batches API Core Messages and streaming

4. Tools and Third-Party Access

Key Points

  • Tool use is a loop: declare tools → receive a tool_use block (stop_reason: "tool_use") → execute → reply with a matching tool_result block.
  • Return all parallel tool_result blocks in a single user message; splitting them trains Claude to stop making parallel calls.
  • stop_reason is your loop's control signal; stop_details is populated only when stop_reason == "refusal".
  • Invoke via Bedrock (AnthropicBedrockMantle, anthropic.-prefixed IDs, SigV4) or Vertex (AnthropicVertex, bare IDs, GCP ADC)—same Messages surface.
  • Prompt caching is a prefix match (toolssystemmessages) verified by cache_read_input_tokens; automatic caching is first-party only.

Tool use in the Messages API

Tool use lets Claude call functions you define. You declare a tools array; each tool has a name, a description (Claude reads this to decide when to call it), and an input_schema (a JSON Schema object).

{
  "model": "claude-opus-4-8",
  "max_tokens": 1024,
  "tools": [
    {
      "name": "get_weather",
      "description": "Get the current weather for a location.",
      "input_schema": {
        "type": "object",
        "properties": {
          "location": { "type": "string", "description": "City name" }
        },
        "required": ["location"]
      }
    }
  ],
  "messages": [
    { "role": "user", "content": "What's the weather in Paris?" }
  ]
}

The tool-use loop is a conversation, not a single call:

  1. Claude returns a tool_use block and stop_reason becomes "tool_use".
  2. Your code executes the tool.
  3. You send a new user message with a tool_result block whose tool_use_id matches step 1.
  4. Claude reads the result and produces its final answer.

Figure 5.4: The tool_use / tool_result loop

sequenceDiagram participant App as Your Code participant Claude as Claude (Messages API) App->>Claude: user message + tools[] Claude-->>App: tool_use block (stop_reason: "tool_use") Note over App: Execute the requested tool App->>Claude: user message with tool_result (matching tool_use_id) Claude-->>App: final answer (stop_reason: "end_turn")

You control tool selection with tool_choice: auto (default), any, tool (force a specific one), or none. When Claude requests several tools at once, execute them all and return all tool_result blocks in a single user message—splitting them silently trains Claude to stop making parallel calls. Anthropic also offers server-side tools (web search, web fetch, code execution) that run on its own infrastructure.

The stop_reason field is your loop's control signal:

stop_reasonMeaning
end_turnFinished naturally
max_tokensHit the output cap — output is truncated
stop_sequenceHit a custom stop sequence
tool_useClaude wants a tool — execute and continue
pause_turnA server-side tool loop paused — re-send to resume
refusalDeclined for safety — check stop_details

stop_details is populated only when stop_reason == "refusal"; otherwise it is null, so guard before reading it.

Invoking Claude via Amazon Bedrock and Google Vertex AI

Two clouds host Claude, each with a dedicated SDK client class—you do not point the first-party client at a custom URL.

ConcernFirst-party APIAmazon BedrockGoogle Vertex AI
ClientAnthropic()AnthropicBedrockMantleAnthropicVertex
Model IDclaude-opus-4-8anthropic.claude-opus-4-8claude-opus-4-8 (no prefix)
Authx-api-keyAWS SigV4 / IAMGCP ADC (no API key)
Regionn/aRequiredRequired

Feature parity is close but not total. Core Messages, streaming, tool use, extended thinking, PDF input, and explicit prompt caching work on both. Gaps: automatic prompt caching (first-party only; explicit cache_control still works), the Message Batches API, Files API, and Models API are first-party only; web fetch and code execution are unavailable on both; web search is Vertex-only in its basic variant.

Caching within API requests

Prompt caching stores a stable prefix so repeated calls don't re-process it. The mechanism is a prefix match: Claude renders your request as toolssystemmessages, and any byte change anywhere in that prefix invalidates the cache from that point forward. Mark a breakpoint by attaching cache_control to the last block of a stable prefix:

{
  "model": "claude-opus-4-8",
  "max_tokens": 1024,
  "system": [
    {
      "type": "text",
      "text": "<large shared instructions / document>",
      "cache_control": { "type": "ephemeral" }
    }
  ],
  "messages": [
    { "role": "user", "content": "Summarize the key points." }
  ]
}

Default lifetime is 5 minutes (pass "ttl":"1h" for one hour). Up to 4 breakpoints per request; the minimum cacheable prefix is model-dependent (e.g. 4096 tokens on Opus 4.8). Verify with usage counters: cache_creation_input_tokens (tokens written, ~1.25× cost) and cache_read_input_tokens (tokens served, ~0.1× cost). If cache_read_input_tokens stays zero across identical-prefix requests, a silent invalidator is at work—an interpolated datetime.now()/UUID, unsorted JSON, or a changing tool set. Keep the prefix byte-identical.

[Animation slot: the tool-use relay animating Claude → your code → tool_result → final answer, with a side panel showing stop_reason flipping from "tool_use" to "end_turn."]
Post-Reading Check — Tools and Third-Party Access

After Claude returns a tool_use block with stop_reason: "tool_use", what is the correct next step?

Anthropic's servers execute the tool and continue automatically Your code executes the tool, then sends a tool_result block whose tool_use_id matches Retry the same request until stop_reason becomes end_turn Put the tool output in the top-level system parameter

When Claude requests several tools at once, how should you return the results?

One tool_result per user message, split across several messages All tool_result blocks in a single user message Concatenated into one text block In the system parameter as an array

Which model-ID rule is correct for Amazon Bedrock vs. Google Vertex AI?

Bedrock uses the bare ID; Vertex uses an anthropic. prefix Bedrock uses an anthropic. prefix; Vertex uses the bare (unprefixed) ID Both require the anthropic. prefix Neither accepts a model ID; the region selects the model

Prompt caching is a prefix match. In what order does Claude render the request prefix?

messagessystemtools toolssystemmessages systemtoolsmessages Order does not matter for cache matching

Which usage counter confirms a cache hit, and why might it stay zero across identical-prefix requests?

cache_read_input_tokens; a silent invalidator such as an interpolated datetime.now() or UUID output_tokens; because caching only applies to output cache_creation_input_tokens; because reads are never counted input_tokens; because the cache is disabled by default

Which feature is first-party only (a known gap on both Bedrock and Vertex)?

Extended thinking Explicit cache_control prompt caching The Message Batches API Core Messages and streaming

Your Progress

Answer Explanations