The Messages API, One Request at a Time

The request and response shapes of the Claude Messages API — roles, system, sampling, the full stop_reason set, the usage object as it actually comes back, count_tokens, and multi-turn structure.

Every capability in this series — streaming, tool use, prompt caching, whole agents — is built on a single HTTP call: POST /v1/messages, the Messages API. There is no second endpoint for talking to Claude. Learn what you send and what comes back once, and every later chapter is a variation you’ll recognize rather than a new thing to memorize.

The surface is deliberately small: three required fields and a handful of options. The depth is all in the response — a set of fields most developers never read until a truncated answer or a surprise bill sends them looking for an explanation. That asymmetry is the lesson here. The request is easy; the response is where the control flow, the cost model, and the traps live.

This is also where the exam’s largest domain begins. The orientation chapter mapped the blueprint. Applications & Integration is a third of the whole exam, and its foundation is exactly this one request. Get its shape into your hands and the rest of the domain is detail.

The smallest real call

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

resp = client.messages.create(
    model="claude-haiku-4-5",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Say hi in three words."}],
)
print(resp.content[0].text)

Three things are required: model, max_tokens, and messages. The one that surprises people is max_tokens — it’s not optional, and it caps the output, not the total. Set it too low and the model gets cut off mid-sentence — you’ll see it in stop_reason, below. Set it absurdly high and you’ve only raised a ceiling, not a cost, since you pay for tokens actually generated. It exists so a runaway generation can’t bill you indefinitely.

messages is an ordered list of turns, each {"role": ..., "content": ...}. Roles alternate user and assistant; the conversation must start with user. content can be a plain string (shorthand for a single text block) or a list of typed content blocks: text, images, tool results. The list form is how multi-format input and tool use are expressed. We’ll use it constantly from the next chapter on.

The system prompt is a parameter, not a message

There is no {"role": "system"} turn. The system prompt is a top-level parameter:

import anthropic
client = anthropic.Anthropic()

resp = client.messages.create(
    model="claude-haiku-4-5",
    max_tokens=1024,
    system="You are a terse assistant for a bookshop.",
    messages=[{"role": "user", "content": "Do you have any Le Carré?"}],
)

This trips up developers coming from other APIs where system is the first message in the array. Here it’s separate, and it applies to the whole conversation. It can itself be a list of blocks, so you can attach a cache breakpoint to it — a detail that matters for Arc 4. Where instructions belong (system vs. user turn) is a Domain 6 question we’ll return to; mechanically, system is its own slot.

Sampling: temperature, top_p, top_k

Three optional knobs control randomness:

  • temperature (0.0–1.0) — the main dial. Near 0, output is near-deterministic, though never guaranteed identical: LLM sampling is non-deterministic even at temperature 0, which the exam’s Domain 5 tests. Higher values widen the distribution for more varied, creative output.
  • top_p (nucleus sampling) and top_k: alternative ways to truncate the token distribution. You rarely need these alongside temperature; the documented guidance is to adjust either temperature or top_p, not both.

For extraction, classification, and tool-driven work — most of what a developer builds — you want temperature low. For drafting and brainstorming, higher. There’s no universal “correct” value; it’s a task decision.

The response object, field by field

Here’s what actually came back from a small call (system prompt “You are terse.”, asked to say hi in three words), printed field by field:

id:          msg_011CdXcwWkHd9SauWux4MJYN
type:        message
role:        assistant
model:       claude-haiku-4-5-20251001
stop_reason: end_turn
stop_sequence: None
content:     [TextBlock(type='text', text='Hey there friend.')]

Details worth noticing, and easy to get wrong from memory:

  • model echoes back the fully-pinned id: you asked for claude-haiku-4-5 (an alias) and got claude-haiku-4-5-20251001 (the dated snapshot the alias resolved to). Logging this is how you know, months later, which exact model produced an output. Alias-vs-snapshot pinning is a Domain 2 configuration topic.
  • content is always a list of blocks, even for a one-line answer. You reach text via resp.content[0].text only when you know block 0 is text; with tools or thinking in play, you filter by block.type. Assuming content[0].text is the classic bug that breaks the moment a tool call or a thinking block appears first.
  • role is always assistant on a response — it’s the turn you append back into messages to continue the conversation.

stop_reason: why the model stopped

stop_reason is the single most important field for control flow. Here is the full set:

  • end_turn: the model finished naturally. The normal case.
  • max_tokens: it hit your max_tokens ceiling and was cut off. The output is incomplete. If you ever see truncated responses, check this first; the fix is a higher ceiling or a shorter task, not a retry.
  • stop_sequence: it emitted one of your stop_sequences strings; stop_sequence on the response tells you which.
  • tool_use: it wants to call a tool. This is the signal that drives the agentic loop (Arc 2) — you run the tool and feed the result back.
  • pause_turn: a long-running server-side turn was paused and should be continued.
  • refusal: the model declined to generate for safety reasons.
  • model_context_window_exceeded: the request plus generation would overflow the context window.

Branching on stop_reason — rather than parsing the text to guess what happened — is the reliable pattern. An agent loops while it’s tool_use; a batch job flags anything that isn’t end_turn; a UI warns the user on max_tokens.

The usage object, as it actually comes back

Billing and cost modeling (Domain 5) start here. The usage object is richer than the two token counts most people expect. Verbatim from a live call:

Usage(
  input_tokens=20,
  output_tokens=7,
  cache_creation_input_tokens=0,
  cache_read_input_tokens=0,
  cache_creation=CacheCreation(ephemeral_1h_input_tokens=0, ephemeral_5m_input_tokens=0),
  inference_geo='not_available',
  output_tokens_details=None,
  server_tool_use=None,
  service_tier='standard',
)
  • input_tokens / output_tokens: the core counts you’re billed on, at the input and output rates respectively. Output is several times more expensive per token, which is why capping verbosity saves real money.
  • cache_read_input_tokens / cache_creation_input_tokens: prompt-caching accounting, zero here because we cached nothing. Cache reads bill at roughly a tenth of the input rate; the cache_creation breakdown splits writes by TTL (ephemeral_5m vs ephemeral_1h). This is the whole subject of Arc 4’s caching chapter, and the object is already carrying the fields for it.
  • service_tier: which capacity tier served the request (standard here). You can request a tier via the service_tier parameter; the response tells you what you actually got.

The cost of a call is input_tokens × input_rate + output_tokens × output_rate, adjusted for any cached tokens. Every one of those numbers is on this object — you never have to estimate after the fact.

Counting tokens before you send

You can price or size a request before making it, with a separate free endpoint:

import anthropic
client = anthropic.Anthropic()

count = client.messages.count_tokens(
    model="claude-haiku-4-5",
    messages=[{"role": "user", "content": "Say hi in three words."}],
)
print(count.input_tokens)   # -> 15

One subtlety that’s a genuine trap: count_tokens only counts what you pass it. The call above returned 15. The real create call earlier reported input_tokens=20 for the same user message — because that call also carried the system prompt “You are terse.”, which count_tokens above didn’t include. If you’re using count_tokens to predict cost or stay under a budget, pass it the same system prompt, tools, and images the real call will carry. Otherwise your estimate runs low. Count the whole request, not just the message.

Multi-turn: you carry the history

The API is stateless. It has no memory of prior calls; a conversation is just the full messages list, resent each turn, with the model’s own replies appended:

import anthropic
client = anthropic.Anthropic()

messages = [{"role": "user", "content": "Recommend one spy novel."}]
r1 = client.messages.create(model="claude-haiku-4-5", max_tokens=256, messages=messages)

messages.append({"role": "assistant", "content": r1.content})   # append the reply verbatim
messages.append({"role": "user", "content": "Who's the author?"})  # ask a follow-up
r2 = client.messages.create(model="claude-haiku-4-5", max_tokens=256, messages=messages)

You append the assistant’s content list as-is — not a stringified version — because it may contain tool-use blocks the next turn needs to reference. This is the manual bookkeeping the Agent SDK and frameworks automate for you (Arc 2), but every one of them is doing exactly this underneath: growing a list and resending it. It’s also why long conversations get expensive: you re-send and re-pay for the whole history each turn. That’s the problem context engineering (Arc 5) exists to solve.

SDK ergonomics you’ll rely on

A few client-level facts that save debugging later:

  • The client reads ANTHROPIC_API_KEY from the environment automatically; pass api_key=... to override.
  • client.max_retries defaults to 2: the SDK already retries transient failures (429s, connection errors, overloaded) twice with backoff before raising. You don’t hand-roll that for the common case; you tune it (Anthropic(max_retries=5)) or handle what’s left. Error types and recovery are the next chapters.
  • Set per-request timeouts with timeout=; the default is generous, which matters once you add extended thinking.
  • There’s an AsyncAnthropic client with the identical surface (await client.messages.create(...)) for concurrent workloads — Domain 5’s “async programming” foundation.

Final thoughts

The Messages API is small on the surface — model, max_tokens, messages, an optional system and a sampling knob — and the depth is all in the response. stop_reason is your control flow. usage is your cost model, and it carries the caching fields before you’ve cached anything. count_tokens predicts a request only if you give it the whole request. The API is stateless, so a conversation is a list you own and resend. Hold those four ideas — stop-reason branching, the usage object, honest token counting, and stateless multi-turn — and every later chapter is a variation you’ll recognize.

Next: streaming responses — the same call, delivered as server-sent events, and how a tool call arrives one JSON fragment at a time.

Comments