Foundations · Messages API Basics

The Messages API, Basics

Every call to Claude — from a one-off question to a sprawling multi-turn conversation — boils down to a single POST request. This page breaks down exactly what goes into that request and what comes back, from required parameters to the error codes you'll eventually hit.

☺ Explain it like I'm 10

Imagine texting a friend who reads your message, replies, and then instantly forgets the entire conversation ever happened. If you want them to remember what you said last time, you have to copy the whole chat into every new text you send. That's exactly how talking to Claude works: one request in, one reply out, and nothing remembered in between unless your own code carries it forward.

🐦Your host for this topic: Pip the Hummingbird (the messenger — ferries every request to Claude and every reply back, never remembering a thing on her own).

The anatomy of a request

☺ Like you're 10: It's like mailing a letter — you need a return address (your API key), a postmark rule everyone agrees on (the version header), and the actual letter (the JSON body). Skip any one and it never arrives.

The Claude API is a REST API. The endpoint you'll use for almost everything in this course is POST /v1/messages, sent to https://api.anthropic.com. Every request needs three headers: x-api-key (or an Authorization: Bearer token), anthropic-version (a date string like 2023-06-01 that pins the API's request/response shape), and content-type: application/json.

The JSON body has two required fields — model and max_tokens — plus a messages array. Each entry in messages is an object with a role ("user" or "assistant") and content. The first message in the array must have role: "user" — you can't open a conversation by putting words in Claude's mouth.

Python
import anthropic

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

message = client.messages.create(
    model="claude-opus-5",
    max_tokens=1000,
    system="You are a concise, friendly assistant.",
    messages=[
        {"role": "user", "content": "What's the capital of France?"}
    ],
)

print(message.content)
JavaScript
import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic(); // reads ANTHROPIC_API_KEY from env

const message = await client.messages.create({
  model: "claude-opus-5",
  max_tokens: 1000,
  system: "You are a concise, friendly assistant.",
  messages: [
    { role: "user", content: "What's the capital of France?" }
  ]
});

console.log(message.content);
⚠ Careful

The system prompt is a top-level parameter, not a message with role: "system". This trips up almost everyone coming from other chat APIs. Instructions, persona, and standing context belong in system; the messages array is reserved for the actual back-and-forth between user and assistant turns.

max_tokens is a hard ceiling on how many tokens Claude is allowed to generate in this response — it's not a target length, and Claude may stop well short of it. If Claude fills the entire budget without finishing its thought, the response is truncated mid-stream, which you can detect by checking stop_reason (covered below).

Sampling parameters: temperature, top_p, top_k

☺ Like you're 10: temperature is like how adventurous a chef is with a recipe — turn it down and dinner is the same reliable dish every night; turn it up and the chef starts improvising, for better or worse.

Alongside model, max_tokens, system, and messages, the Messages API accepts a handful of optional parameters that shape how Claude picks each next token, rather than what it's allowed to talk about:

In practice, most applications only ever touch temperature, and many leave every sampling parameter at its default. Treat top_p and top_k as fine-tuning knobs you reach for once you've already validated behavior with temperature alone.

ParameterRequired?What it controls
modelRequiredWhich Claude model handles the request, e.g. claude-opus-5 or claude-sonnet-5
max_tokensRequiredThe hard cap on tokens Claude may generate in this response
messagesRequiredThe conversation so far — an array of {role, content} turns, starting with role: "user"
systemOptional, top-levelPersona and standing instructions that apply to the whole conversation — not part of messages
temperatureOptionalRandomness of token selection — lower is more focused and repeatable, higher is more varied
top_pOptionalNucleus sampling — limits choices to the smallest token set covering cumulative probability p
top_kOptionalLimits choices to only the k most likely next tokens

Claude has no memory: multi-turn conversations

☺ Like you're 10: It's like calling a goldfish — every single call, you have to reintroduce yourself and repeat the whole conversation, because the goldfish forgot everything the second you hung up.

Here's the fact that shapes everything about building on top of the Messages API: Claude is stateless between calls. There is no server-side session, no thread ID, no "continue where we left off." Every single request stands alone — Claude only knows what's in the messages array you send this time. To hold a multi-turn conversation, your client is responsible for resending the entire history, turn by turn, on every call.

Turn 1 requestmessages: [user 1] Claude repliesassistant 1 appended Turn 2 requestfull history + user 2 Claude repliesassistant 2 appended

Each call sends a strictly longer list than the one before it. Practically, that means your application code needs to own a growing list of turns and append to it after every exchange: the user's new message goes on before the call, and Claude's reply goes on after it.

Python
import anthropic

client = anthropic.Anthropic()
messages = []

def send(user_text):
    messages.append({"role": "user", "content": user_text})
    response = client.messages.create(
        model="claude-opus-5",
        max_tokens=1000,
        system="You are a concise, friendly assistant.",
        messages=messages,
    )
    reply_text = next(block.text for block in response.content if block.type == "text")
    messages.append({"role": "assistant", "content": reply_text})
    return reply_text

print(send("What's the capital of France?"))
print(send("What's a good day trip from there?"))
JavaScript
import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic();
const messages = [];

async function send(userText) {
  messages.push({ role: "user", content: userText });
  const response = await client.messages.create({
    model: "claude-opus-5",
    max_tokens: 1000,
    system: "You are a concise, friendly assistant.",
    messages
  });
  const block = response.content.find((b) => b.type === "text");
  const replyText = block && block.type === "text" ? block.text : "";
  messages.push({ role: "assistant", content: replyText });
  return replyText;
}

console.log(await send("What's the capital of France?"));
console.log(await send("What's a good day trip from there?"));

Notice that the second call's messages array contains four items by the time you're three turns in — every prior user message and every prior assistant reply. This is also why long conversations get more expensive per turn: you're re-sending (and re-paying for, as input tokens) the entire history on every single call, not just the newest message.

🎬 At the Claude Crew
🦊

Foxy: Wait — if I tell Claude my name, will it remember that for my next message?

🦉

Professor Owl: Not unless you remind it. There's no session, no memory — each call is a blank slate.

🦊

Foxy: So I have to retype my whole conversation every single time?!

🐦

Pip: That's my job, not yours. I carry every past message and reply back to Claude on every trip — so it looks like Claude remembers, even though it never actually does.

◆ Pattern

Keep a single, ever-growing messages list in your application, append the user's turn before calling, append Claude's reply after, and resend the whole thing every time.

⚠ Anti-pattern

Sending only the newest user message and expecting Claude to "remember" earlier turns — without the prior history in the request, Claude has no idea a previous conversation ever happened.

Reading the response: content blocks and stop_reason

A successful response is a JSON object. The two fields you'll touch on nearly every call are content and stop_reason:

{
  "id": "msg_01XyZAbc123",
  "type": "message",
  "role": "assistant",
  "model": "claude-opus-5",
  "content": [
    {
      "type": "text",
      "text": "The capital of France is Paris."
    }
  ],
  "stop_reason": "end_turn",
  "stop_sequence": null,
  "usage": {
    "input_tokens": 14,
    "output_tokens": 9
  }
}

content is always an array of typed blocks, never a plain string. In the pages ahead you'll see responses that also carry tool_use or thinking blocks alongside text, so the safe habit to build now is checking block.type before reading block.text, rather than assuming content[0] is always text.

Python
for block in message.content:
    if block.type == "text":
        print(block.text)
JavaScript
for (const block of message.content) {
  if (block.type === "text") {
    console.log(block.text);
  }
}

stop_reason tells you why Claude stopped generating — it's what lets your code tell "Claude finished its answer" apart from "Claude got cut off."

stop_reasonMeaning
end_turnClaude finished naturally — this is the normal, healthy outcome
max_tokensClaude hit your max_tokens cap before finishing — the response is truncated
stop_sequenceClaude generated a custom stop sequence you configured

You'll meet a few more stop_reason values — tool_use, pause_turn, refusal, model_context_window_exceeded — once later pages like Tool Use introduce tool calling and longer agentic loops. For a plain text-in, text-out call like the ones on this page, end_turn and max_tokens are the two you'll see in practice.

Basic error handling

☺ Like you're 10: It's a vending machine — press the wrong button and pressing it again won't fix anything (400); the machine's just jammed for a minute and needs a beat before it works (429/500/529).

Not every call succeeds. The Claude API communicates failures through standard HTTP status codes plus a JSON error body, and the handling strategy differs by code:

StatusError typeWhat to do
400invalid_request_errorFix the request — malformed body, bad message ordering, missing required field. Retrying unchanged won't help.
429rate_limit_errorYou've exceeded your organization's rate limit. Read the retry-after header and back off before retrying.
500api_errorAn unexpected error on Anthropic's side. Retry with exponential backoff.
529overloaded_errorThe API is temporarily overloaded. Retry with exponential backoff.
import anthropic

client = anthropic.Anthropic()

try:
    message = client.messages.create(
        model="claude-opus-5",
        max_tokens=1000,
        messages=[{"role": "user", "content": "Hello, Claude"}],
    )
except anthropic.APIStatusError as e:
    if e.status_code == 429:
        print("Rate limited — check retry-after and back off.")
    elif e.status_code in (500, 529):
        print("Server-side error — retry with exponential backoff.")
    elif e.status_code == 400:
        print(f"Invalid request: {e.message}")
    else:
        raise
→ Tip

The official 429 response also carries detailed anthropic-ratelimit-* headers (requests/tokens limit, remaining, and reset time) alongside retry-after. You rarely need to implement retry logic by hand: both official SDKs automatically retry transient failures — connection errors, 429s, and 5xxs — with exponential backoff twice by default, honoring retry-after when present, and the retry count is configurable via a max_retries client option.

→ Tip

In VS Code, keep your key out of source entirely: put ANTHROPIC_API_KEY=sk-ant-... in a .env file (gitignored) and load it into the integrated terminal or a Jupyter cell before you run anything. Both SDKs pick the variable up automatically from the environment — no client configuration needed.

✎ Try it yourself

Write a small script that keeps a messages list and lets you type questions into the terminal in a loop. After each reply, print stop_reason alongside the answer. Then deliberately set max_tokens to something tiny (like 10) and ask a question that needs a long answer — confirm you see stop_reason switch to max_tokens and that the printed text is visibly cut off mid-sentence.

🐦 Pip's checkpoint

You should now be able to explain what a Messages API request needs (model, max_tokens, messages, and system as a separate top-level field), why Claude has no memory of its own and whose job it is to resend history, how to read a response's content array and stop_reason, and which HTTP errors are worth retrying versus fixing. Next, head to Prompting Fundamentals to start shaping what goes inside those messages.

Check your answers
  1. What three fields does every Messages API request need, and where does the system prompt live? Every request needs model, max_tokens, and a messages array starting with role: "user". The system prompt is a separate top-level parameter, never an entry inside messages.
  2. Why does a long conversation cost more per turn, and whose job is it to track history? Claude is stateless — it only ever sees what's in the messages array on that one call. Your application must resend the entire conversation every time, so you're re-paying (as input tokens) for every earlier turn on each new request.
  3. What's the difference between stop_reason "end_turn" and "max_tokens," and what should you check before reading a content block's text? end_turn means Claude finished its answer naturally; max_tokens means it was cut off by your token cap before finishing. Always check block.type == "text" first, since content is an array that can hold other block types too.