Claude for Developers

Lesson 2 of 14

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

python
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 reports stop_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 from user, and roles alternate between user and assistant.

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:

python
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 it

Errors 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:

python
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 response

Two 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:

python
n = client.messages.count_tokens(
    model="claude-opus-5",
    messages=[{"role": "user", "content": open("big_doc.md").read()}],
).input_tokens

Use 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.

← Previous