Study guides / CCDV-F / Domain 1

Applications & Integration · Lesson 1 of 11

1.1 - The Messages API: Requests and Responses

Understand the shape of a Messages API call and response well enough to debug one from the raw JSON alone.

Every Claude application, however elaborate, is built on one endpoint: POST /v1/messages. A request needs a model string, a max_tokens cap, and a messages array of {role, content} turns alternating user and assistant. Everything else — system, temperature, tools, tool_choice, stream, stop_sequences, metadata — is optional. The system prompt is not a message — it is a separate top-level system parameter, which matters because the exam likes to test whether you know a system prompt cannot appear mid-conversation as a message with role system. (Some current models do support a distinct, separately-documented mid-conversation system-turn feature for injecting fresh context deep in a long tool-use loop — but that is a different mechanism from the top-level system parameter, and it does not change the rule that messages entries are only ever user or assistant.)

content inside a message can be a plain string for a simple text turn, or an array of typed blocks — text, image, document, tool_use, tool_result — when the turn needs more than one part. Roles must alternate, but the API tolerates two consecutive messages of the same role by treating them as one logical turn; it does not tolerate starting the array with an assistant message with nothing before it. max_tokens is a hard output cap, not a target — Claude does not try to fill it, and hitting it mid-generation truncates the response, which is a different outcome from Claude choosing to stop.

Reading a response

A response carries an id, type: "message", a role of assistant, the model that actually served the request, a content array of typed blocks, a stop_reason, an optional stop_sequence (populated only when a custom stop sequence triggered the stop), and a usage object with input_tokens, output_tokens, and, when caching is in play, cache_creation_input_tokens and cache_read_input_tokens. content is an array, not a single string, because one turn can legitimately hold explanatory text and a tool call in the same response — code that assumes content[0] is always the whole answer breaks the moment a tool call shows up. The block types you'll see depend on what the request asked for: plain requests return text; a request with tools can return tool_use; a request with extended thinking enabled returns thinking (or, rarely, redacted_thinking) ahead of the final text.

Key concept

stop_reason is what tells your application loop what happened, not the presence or absence of text. The values worth knowing cold: end_turn (natural completion), max_tokens (hit the output cap mid-generation — treat the content as truncated, not finished), stop_sequence (a custom sequence you supplied was hit), tool_use (Claude wants a tool executed before it can continue), pause_turn (a long-running server-side tool turn paused and should be resumed by sending the response straight back), and refusal (the model declined to continue on an otherwise normal 200 response). Treat anything other than end_turn as "not finished, check why" rather than assuming it's always tool_use.

Multi-turn conversations and statelessness

The API is stateless: there is no server-side conversation object, no session id, nothing you can reference on a later call to mean "continue where we left off." Every call resends the full message history the model needs, including prior assistant turns and any tool results, or Claude genuinely has no memory of anything said before the messages you actually included. This is why context management (Domain 4) and prompt caching (Lesson 1.4) both exist — a long conversation means a growing, re-sent payload on every single call, and both the cost and the latency of a turn scale with how much history you're carrying, not with how long the conversation has felt to the user.

A subtler consequence: because the client owns the entire history, the client can also edit it. Trimming an old tool result, summarizing a stale portion of the conversation, or dropping an irrelevant early turn are all legitimate application-level techniques — the API has no opinion about conversation history beyond validating role alternation and block structure on the call you actually send.

Exam traps

Practice question

A developer's code reads response.content[0].text to get Claude's answer, and it works in testing. In production, this line occasionally throws because content[0] is a tool_use block, not a text block. What is the correct fix?

  • A Always request max_tokens high enough that Claude never needs a tool call.

    max_tokens caps output length; it has no bearing on whether Claude decides to call a tool at all.

  • B Iterate the content array and handle each block by its type field instead of assuming a fixed index. Correct

    content is an ordered array of typed blocks that can mix text and tool_use in one response. Correct code checks each block's type rather than assuming position 0 is text.

  • C Switch to the streaming endpoint, which does not return tool_use blocks.

    Streaming returns the same block types via incremental deltas; it doesn't remove tool use from the picture.

  • D Set tool_choice to none for every request.

    That would disable tool calling outright, defeating the purpose of giving Claude tools in the first place.

Build exercise: Send a raw Messages API call and inspect every field of the response

Beginner · 25 minutes

You'll practice:

  1. Using curl or your language's HTTP client, send a request to the Messages API with a system prompt, one user message, and max_tokens set to 200.

    Building the request by hand, without an SDK abstracting it away, is the fastest way to internalise the exact shape the exam expects you to recognise.

    You should see: A 200 response with id, type, role, model, content, stop_reason, and usage fields.

    Hints
    1. What are the two required top-level fields besides messages, and is system one of them or a sibling of messages?
    2. system is a sibling of messages at the top level of the JSON body, not an entry inside the messages array. content in the request is a plain string for a simple text turn; it only needs to be an array of blocks for images or multi-part content.
    3. 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-5","max_tokens":200,"system":"Reply in one short paragraph.","messages":[{"role":"user","content":"Explain what a stop_reason is."}]}'
  2. Print stop_reason and usage.output_tokens from the response, then lower max_tokens to something small (like 15) and resend the same request.

    This is the cleanest way to see max_tokens truncation happen on purpose instead of just reading about it.

    You should see: The first call returns stop_reason: "end_turn". The second, capped call returns stop_reason: "max_tokens" with output_tokens equal to your cap and visibly truncated text.

    Hints
    1. What would you expect usage.output_tokens to equal if the response got cut off exactly at your cap?
    2. When output_tokens equals max_tokens exactly, that's your signal the model was cut off mid-thought rather than finishing naturally - stop_reason confirms it.
    3. print(response.stop_reason, response.usage.output_tokens)
      # Expect: "max_tokens" 15  <- confirms truncation, not completion
  3. Add a stop_sequences array containing a distinctive string (e.g. "###END") and ask Claude to output that exact string at the end of its answer.

    This exercises the third common stop_reason value and shows how stop_sequence differs from end_turn in the response.

    You should see: stop_reason: "stop_sequence" and a populated stop_sequence field echoing the string that triggered it; the sequence itself is not included in the returned text.

    Hints
    1. Which response field, besides stop_reason, only gets populated when a custom stop sequence is what ended generation?
    2. Pass stop_sequences: ["###END"] in the request, and instruct Claude in the prompt to literally end its answer with that token.
    3. {"model":"claude-sonnet-5","max_tokens":300,"stop_sequences":["###END"],"messages":[{"role":"user","content":"Answer the question, then write ###END on its own line."}]}
  4. Write a small function that iterates response.content and handles each block by its type field (text, tool_use, thinking) rather than indexing into position 0.

    This is the actual defensive pattern a production app needs, and the one the exam rewards over fixed-index access.

    You should see: A function that correctly extracts text even when it isn't in position 0, and doesn't throw on unexpected block types.

    Hints
    1. What happens to a fixed-index read the moment a response contains a thinking block before the text block?
    2. Loop over content, switch on block.type, and accumulate or dispatch based on type rather than assuming an order or count of blocks.
    3. for (const block of response.content) {
        if (block.type === "text") console.log(block.text);
        else if (block.type === "tool_use") handleTool(block);
        else if (block.type === "thinking") console.log("[reasoning]", block.thinking);
      }

Sources