Skip to main content
Free AI courses. Free certificates of completion.Learn how
Interactive AI Lesson• 11 min

The Messages API: Requests, Content Blocks and Stop Reasons

Make a first Claude API call, read the response as typed content blocks, and branch on stop_reason instead of assuming the reply is finished text.

Why this matters

A developer wires Claude into an internal tool in an afternoon. It works in testing. In production, roughly one reply in fifty ends mid-sentence, a few replies render as an empty box, and once a week a request returns a perfectly valid HTTP 200 that contains no answer at all. The code read response.content[0].text and printed it. Every one of those failures was visible in the response object. Nobody looked.

The Claude API is simple to call and easy to call carelessly. Almost every production problem in this course traces back to one of three habits: treating the response as a string instead of a list of typed blocks, ignoring why generation stopped, and forgetting that the API remembers nothing between requests. This lesson builds the opposite habits from the first line of code.

This is an independent course. It is not affiliated with or endorsed by Anthropic. Model names, prices and limits change; each lesson states when a figure was checked and links to the current documentation.

The idea

Everything goes through one endpoint, POST /v1/messages. Tools, structured output, thinking and caching are all features of that same request, not separate APIs. A request has four parts you will use constantly.

model names a specific model. At the time of writing (October 2026), current models include claude-opus-5-5, claude-sonnet-5-5 and claude-haiku-5-5. Use exact identifiers from the models page; do not invent version suffixes.

max_tokens is a hard ceiling on output length. The model does not see it as a target. If the answer would be longer, generation simply stops. Setting it too low is the most common cause of truncated answers.

system carries instructions that apply to the whole conversation: role, rules, output format, background facts. It is separate from the conversation itself.

messages is the conversation, a list of turns with role set to user or assistant. The first message must be from the user. The API is stateless: it keeps no memory of earlier calls. A multi-turn chat is your application sending the whole history again every time.

The response is not a string. response.content is a list of content blocks, each with a type. A plain answer is usually one text block, but the same list can contain thinking blocks, tool_use blocks when Claude wants to call a tool, and others. Code must check each block's type before reading .text.

Finally, stop_reason tells you why generation ended. end_turn means Claude finished. max_tokens means your ceiling cut it off. tool_use means Claude is waiting for your code to run a tool. refusal means the request was declined, and the HTTP status is still 200. pause_turn appears in some agentic flows when a long server-side operation pauses and should be continued. A reliable integration branches on this field every time.

The response also carries usage: input and output token counts, plus cache figures you will meet in a later lesson. Logging it from day one makes cost visible before the invoice arrives.

Two more habits pay off early. Every response has an id, and every HTTP response carries a request ID; log both, because they are what you quote when investigating a problem with the provider or correlating a user complaint with a specific call. And when you need to know the size of a prompt before sending it, for example to stay inside a context window or estimate cost, use the token-counting endpoint rather than a rule of thumb or a tokeniser from a different model. Token counts differ between model generations, so an estimate that was accurate last year can be noticeably wrong today.

Diagram

One stateless request in, a list of typed content blocks out. The stop reason decides what your code does next.Model, system prompt and full message history form a request to the Messages API. The response holds content blocks, usage and a stop reason, which branches to display, continue a tool loop, warn about truncation or handle a refusal.Model + max_tokensexact identifierSystem promptstable instructionsFull historyresent every callContent blockstext, thinking, tool_useUsagetokens in and outBranch on stop_reasonend, tools, cut-off, refusal
Figure 1. One stateless request in, a list of typed content blocks out. The stop reason decides what your code does next. Model, system prompt and full message history form a request to the Messages API. The response holds content blocks, usage and a stop reason, which branches to display, continue a tool loop, warn about truncation or handle a refusal.

Worked example

You are building a "summarise this incident report" button. Reports run from 300 to 4,000 words. Your first version sets max_tokens=300 to keep summaries short, puts the report in the user message, and prints response.content[0].text.

In a test run of 40 reports, 37 summaries look fine. Three end mid-sentence. Logging stop_reason shows those three as max_tokens: the longest reports produced summaries longer than 300 tokens. The ceiling was doing the job a format instruction should have done.

You change two things. The system prompt now states the format: "Summarise in at most five bullet points, each one sentence." And max_tokens goes up to 1,024, so the ceiling is a safety net rather than an editor. In the rerun, all 40 responses end with end_turn, and the longest summary uses 212 output tokens.

Next, a colleague adds a follow-up feature: "ask a question about this report". Their first attempt sends only the new question. Claude answers as if it has never seen the report, because it has not: the API is stateless. The fix is to send the report, the earlier summary as an assistant turn, and the new question as the latest user turn. The token count rises accordingly, which is exactly what usage.input_tokens now shows in the logs.

Last, you replace content[0].text with a loop that collects every text block and a branch on stop_reason. A refusal now shows the user a clear message instead of an empty box. max_tokens is logged as a warning with the request ID, so truncation shows up on a dashboard rather than in a complaint.

Code

Install the official Python SDK with pip install anthropic and set ANTHROPIC_API_KEY in your environment. Running this example makes one paid API request. It is shown for study and is not executed by this site.

import anthropic

client = anthropic.Anthropic()  # reads ANTHROPIC_API_KEY from the environment

report = open("incident.txt", encoding="utf-8").read()

response = client.messages.create(
    model="claude-opus-5-5",
    max_tokens=1024,
    system="You summarise incident reports in at most five one-sentence bullet points.",
    messages=[{"role": "user", "content": f"<report>\n{report}\n</report>\n\nSummarise this report."}],
)

if response.stop_reason == "refusal":
    print("The request was declined.")
else:
    text = "".join(block.text for block in response.content if block.type == "text")
    if response.stop_reason == "max_tokens":
        print("WARNING: output hit max_tokens and may be truncated")
    print(text)

print("tokens in/out:", response.usage.input_tokens, response.usage.output_tokens)

For a follow-up question, append {"role": "assistant", "content": response.content} and a new user message to the same list, then call messages.create again with the full history.

Common mistakes

"The answer is always in content[0].text." It is tempting because it works for the first simple test. The first block may be a thinking block or a tool call, and an empty or refused response may have no text block at all. Iterate over blocks and check type.

"A 200 response means success." HTTP status reports transport success. A refusal or truncation still returns 200. Check stop_reason on every response.

"Claude remembers the conversation." Chat interfaces make it feel that way. The API keeps no state between requests; your code must resend history, and cost grows with it. Later lessons show how caching and compaction manage that growth.

"max_tokens controls length, so set it tight." It feels like a length knob. It is a cut-off that truncates, and the model does not plan around it. Ask for length in the prompt and keep the ceiling generous.

Your turn

Write a script that sends ten inputs of varying length to Claude and logs, for each: stop_reason, the number of content blocks and their types, input tokens, output tokens, and the request ID. Deliberately include one input that triggers max_tokens by setting a very low ceiling.

Success criterion: your log shows at least two different stop reasons, and your code handles each one explicitly rather than falling through to "print the first block".

Recap

  • Every Claude feature in this course is part of one request to the Messages API.
  • The API is stateless, so multi-turn conversations resend the full history and cost grows with it.
  • The response is a list of typed content blocks; code must check each block's type.
  • stop_reason explains why generation ended and must be handled on every response, including refusals that arrive with HTTP 200.
  • max_tokens is a hard cut-off, not a length target; length belongs in the instructions.

Sources

  • Anthropic, Models overview, accessed October 2026. Current model identifiers, context windows and output limits.
  • Anthropic, Errors, accessed October 2026. HTTP error codes and their meaning for the Messages API.
  • Anthropic, Python SDK repository, accessed October 2026. Installation, client configuration and typed response objects.
Knowledge Checkpoint

A response comes back with stop_reason "max_tokens". What does this tell you about the content you received?

Read to the end of the lesson