Week 6: The Messages API — Requests, Responses & Streaming

This is where the course stops being about talking to Claude and starts being about building software that talks to Claude. This week gets you from a fresh API key to a working call: the Messages API's request/response shape, how a system prompt is set in code, streaming a response token by token instead of waiting for the whole thing, and how the official Python and TypeScript SDKs compare for the exact same call.

Module 3 of 14 Week 6 of 14 ~2.5 Hours Hands-on Exercise Included

By the end of this week, you'll be able to

  • Make a real Messages API call and read the response shape correctly
  • Set a system prompt in code and explain how it differs from a message
  • Stream a response and know when NOT to stream (structured output)

1. Auth & the Messages API

Get an API key from the Anthropic Console, store it in an environment variable — never hardcode it in source, and never ship it to a browser, since anyone could read it out of your client-side code and rack up usage on your account. Every request authenticates with that key in a header.

a raw request, no SDK, to see the actual shape
curl https://api.anthropic.com/v1/messages \
  --header "x-api-key: $ANTHROPIC_API_KEY" \
  --header "anthropic-version: 2023-06-01" \
  --header "content-type: application/json" \
  --data '{
    "model": "claude-sonnet-4-5",
    "max_tokens": 1024,
    "messages": [
      {"role": "user", "content": "Explain what a race condition is in one paragraph."}
    ]
  }'

Every request needs a model, a max_tokens ceiling (the hard cap from Week 1's context-window discussion, but for just the response this time), and a messages array of {role, content} objects — user and assistant turns, in order. The response comes back with the generated text plus metadata: how many tokens were used, and why the response stopped (it finished naturally, hit max_tokens, or something else).

Multi-turn conversation = you resend the history

The API is stateless — Claude doesn't remember previous calls. To continue a conversation, you append the previous assistant reply back into the messages array and send the whole thing again. Your application is responsible for conversation state, not the API.

2. System Prompts in Code

Week 3 covered system prompts conceptually — in the API, it's a distinct top-level parameter, separate from the messages array, not a message with a "system" role mixed into the conversation.

Python SDK — system prompt as its own parameter
import anthropic

client = anthropic.Anthropic()  # reads ANTHROPIC_API_KEY from env

response = client.messages.create(
    model="claude-sonnet-4-5",
    max_tokens=1024,
    system=(
        "You are a code reviewer for a production Node.js API. Flag "
        "unhandled promise rejections first. Never suggest a rewrite "
        "unless there's a real bug. Cite exact line numbers."
    ),
    messages=[
        {"role": "user", "content": "Review this function: ..."}
    ],
)

print(response.content[0].text)

Because it's a separate field, you set it once per request rather than repeating it in every message — and you can vary it call to call (different reviewer rules for different repos, say) without it competing for space in the conversation history itself the way a "system-role message" would.

This is your first real building block

Every technique from Weeks 2–3 — clear instructions, few-shot examples, XML tags, requesting reasoning — applies exactly the same way inside system or a message's content string. Nothing about prompting changes once you're in code; only how you send it does.

3. Streaming Responses

A non-streaming call waits for the entire response before returning anything — fine for short answers, but a bad user experience for a long one, where someone stares at a blank loading state for several seconds. Streaming returns the response incrementally, token by token, as it's generated.

Python SDK — streaming
with client.messages.stream(
    model="claude-sonnet-4-5",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Write a short poem about deploying on a Friday."}],
) as stream:
    for text in stream.text_stream:
        print(text, end="", flush=True)  # prints as each chunk arrives

This is exactly what powers the "typing" effect you see in claude.ai itself — the perceived speed improves dramatically even though the total time to finish the full response is roughly the same, because the user sees progress immediately instead of a blank wait.

Don't stream when you need structured output

If you asked for JSON (Week 3), a partial chunk mid-stream is invalid JSON by definition — trying to parse it as it arrives will fail. For structured output, buffer the full response and parse once it's complete; save streaming for output meant to be read as prose while it arrives.

4. Python & TypeScript SDKs, Side by Side

The official SDKs wrap the raw HTTP API with typed request/response objects, automatic retries on transient errors, and rate-limit handling — worth using instead of hand-rolling HTTP calls for anything beyond a quick test.

the same call, Python vs. TypeScript
// Python
import anthropic
client = anthropic.Anthropic()
msg = client.messages.create(
    model="claude-sonnet-4-5",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Say hello in three languages."}],
)
print(msg.content[0].text)

// TypeScript
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic(); // reads ANTHROPIC_API_KEY from env
const msg = await client.messages.create({
  model: "claude-sonnet-4-5",
  max_tokens: 1024,
  messages: [{ role: "user", content: "Say hello in three languages." }],
});
console.log(msg.content[0].text);

The request shape is identical between languages — same field names, same structure — which is intentional: everything you learn about the Messages API this week transfers directly regardless of which SDK your project uses. Pick based on your stack, not based on any difference in what the API itself can do.

Keep the key server-side

In a TypeScript project, that means the SDK call runs in your backend or a server route/edge function — never directly in browser-shipped frontend code, where the API key would be visible to anyone who opens dev tools.

5. Hands-on Exercise

Hands-on

Your first API call, a system prompt comparison, and a streaming script

Get from zero to a working script in your language of choice.

Part 1 — First call:

  1. Get an API key from the Anthropic Console and set it as an environment variable — do not paste it directly into a script.
  2. Install the SDK for your language (pip install anthropic or npm install @anthropic-ai/sdk) and make one non-streaming call.
  3. Print the response text, the stop reason, and the token usage from the response object.
Hint

If you get an authentication error, double-check the environment variable name matches exactly what the SDK expects (ANTHROPIC_API_KEY) and that it's actually set in the terminal session you're running from.

Part 2 — Prove the system prompt matters:

  1. Send the same user message twice: once with no system parameter, once with a specific system prompt (a persona + 2-3 rules, like Section 2's example).
  2. Compare the two responses and note the concrete difference the system prompt made.

Part 3 — Build a streaming script:

  1. Write a script that streams a longer response (ask for a 300-word explanation of something) and prints each chunk as it arrives.
  2. Time how long it takes before the FIRST character appears vs. a non-streaming call to the same prompt, and note the difference in perceived responsiveness.

6. Knowledge Check

Four quick questions. Expand each to check your answer.

Q1

Why do you have to resend the full conversation history on every Messages API call?

The API is stateless — Claude doesn't retain memory of previous calls on its own. Your application is responsible for tracking conversation state and including it (previous user and assistant turns) in the messages array on each new request.

Q2

How does the system parameter differ from putting instructions in a user message?

It's a distinct top-level field, not part of the messages array or conversation history — you set persistent behavior once per request without it competing for space with or being mixed into the actual back-and-forth conversation.

Q3

Why shouldn't you stream a response when you've asked Claude for JSON output?

A partial chunk of JSON mid-stream isn't valid JSON — trying to parse it before the response completes will fail. Structured output should be buffered in full and parsed once complete; streaming is for output meant to be read as it arrives, like prose.

Q4

Why should an API key never be embedded in frontend/browser-shipped code?

Anything shipped to a browser is visible to the end user via dev tools — an embedded key could be extracted and used by anyone, racking up usage against your account. API calls that need the key belong on a server, backend route, or edge function instead.