The anatomy of a Messages API call
Everything you will ever build with Claude — chat, extraction, RAG, agents — goes through one endpoint: the Messages API. Tools, structured output, caching, and streaming are all features of this single call, not separate products. Understand its shape deeply once and every later lesson becomes a variation on a theme.
The minimal call, and what each part means
import anthropic
client = anthropic.Anthropic() # reads ANTHROPIC_API_KEY from the environment
response = client.messages.create(
model="claude-opus-5",
max_tokens=1024,
messages=[
{"role": "user", "content": "Summarize why HTTP/2 multiplexing matters, in 3 bullets."}
],
)
for block in response.content:
if block.type == "text":
print(block.text)Three parameters are required, and each carries a design decision:
model— the exact ID string from the previous lesson.max_tokens— a hard cap on generated output, not a suggestion. If the model hits it, generation stops mid-thought and the response reportsstop_reason: "max_tokens". On current models the cap covers internal thinking plus visible text together, so a tightly-sized cap can truncate answers that would have fit under older models. Don't lowball it: around 16,000 is a sane default for non-streaming calls, and only go low when you have a hard reason (a classifier that answers in one word can take 256).messages— the conversation so far, as a list of{role, content}turns. The first message must be fromuser, and roles alternate betweenuserandassistant.
Note what you read back: response.content is a list of typed blocks, not a string. Today it might hold one text block; tomorrow — when you enable thinking or tools — it will hold several kinds. Code that assumes content[0].text is code that breaks the day you adopt any advanced feature. Iterate and check block.type from day one.
The API is stateless — and that is a feature
Claude does not remember your previous call. Every request carries the full conversation history, and the model sees exactly what you send — nothing more. This feels wasteful until you realise what it buys you: total control over context. You decide what history to keep, what to summarize, what to inject, and what to drop. Every context-engineering technique in this course exists because the API is stateless.
Multi-turn is therefore just an append loop:
messages = []
def send(user_text: str) -> str:
messages.append({"role": "user", "content": user_text})
response = client.messages.create(
model="claude-opus-5", max_tokens=16000, messages=messages,
)
reply = next(b.text for b in response.content if b.type == "text")
messages.append({"role": "assistant", "content": reply})
return reply
send("My name is Priya and I'm debugging a race condition.")
send("What's my name and what am I working on?") # Claude knows — because you resent itErrors are typed — handle them as a chain
The SDK raises a specific exception class per failure mode, and the useful split is retryable versus not. Rate limits (429) and server errors (5xx) are retried automatically by the SDK with exponential backoff — configure with max_retries — while 4xx errors mean your request is wrong and retrying is pure waste:
import anthropic
try:
response = client.messages.create(...)
except anthropic.NotFoundError:
... # bad model ID — fix the string, don't retry
except anthropic.RateLimitError as e:
retry_after = e.response.headers.get("retry-after")
... # back off (the SDK already retried twice for you)
except anthropic.APIStatusError as e:
... # other HTTP error: e.status_code, e.message
except anthropic.APIConnectionError:
... # network failure before any responseTwo production habits worth forming now. Log response._request_id (it is public despite the underscore) — it is how you and Anthropic trace a specific request when something goes wrong. And read response.usage on every call: input_tokens and output_tokens are your cost meter, and later, your cache-hit meter.
Counting tokens before you spend them
The count_tokens endpoint answers "how big is this prompt?" against the actual model tokenizer — which matters because tokenizers differ between model generations, and third-party estimators like tiktoken are built for other providers' models and miscount Claude's tokens badly:
n = client.messages.count_tokens(
model="claude-opus-5",
messages=[{"role": "user", "content": open("big_doc.md").read()}],
).input_tokensUse it to enforce input budgets, to decide when to summarize history, and to re-baseline costs when you change models.
What to take into the next lesson
One endpoint, stateless, typed blocks in, typed blocks out, typed errors when it fails. The two beginner-proofing habits — iterate content by block type, and never treat max_tokens as decoration — will save you real debugging time within the week. Next: the parameter we deliberately skipped, and the highest-leverage one there is — the system prompt.