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.
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.
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.
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)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);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:
- temperature controls randomness in token selection. Lower values push Claude toward the most probable, most deterministic continuation — useful for extraction, classification, or anything where you want the same answer every time. Higher values let Claude wander into less-probable, more varied phrasing — useful for brainstorming or creative writing.
- top_p (nucleus sampling) restricts token choices to the smallest set of candidates whose cumulative probability reaches
p, discarding the long tail of unlikely tokens. - top_k restricts token choices to only the
kmost likely next tokens at each step, regardless of their cumulative probability.
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.
| Parameter | Required? | What it controls |
|---|---|---|
model | Required | Which Claude model handles the request, e.g. claude-opus-5 or claude-sonnet-5 |
max_tokens | Required | The hard cap on tokens Claude may generate in this response |
messages | Required | The conversation so far — an array of {role, content} turns, starting with role: "user" |
system | Optional, top-level | Persona and standing instructions that apply to the whole conversation — not part of messages |
temperature | Optional | Randomness of token selection — lower is more focused and repeatable, higher is more varied |
top_p | Optional | Nucleus sampling — limits choices to the smallest token set covering cumulative probability p |
top_k | Optional | Limits 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.
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.
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?"))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.
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.
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.
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.
for block in message.content:
if block.type == "text":
print(block.text)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_reason | Meaning |
|---|---|
end_turn | Claude finished naturally — this is the normal, healthy outcome |
max_tokens | Claude hit your max_tokens cap before finishing — the response is truncated |
stop_sequence | Claude 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:
| Status | Error type | What to do |
|---|---|---|
| 400 | invalid_request_error | Fix the request — malformed body, bad message ordering, missing required field. Retrying unchanged won't help. |
| 429 | rate_limit_error | You've exceeded your organization's rate limit. Read the retry-after header and back off before retrying. |
| 500 | api_error | An unexpected error on Anthropic's side. Retry with exponential backoff. |
| 529 | overloaded_error | The 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:
raiseThe 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.
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.
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.
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
- What three fields does every Messages API request need, and where does the system prompt live? Every request needs
model,max_tokens, and amessagesarray starting withrole: "user". Thesystemprompt is a separate top-level parameter, never an entry insidemessages. - 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
messagesarray 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. - 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_turnmeans Claude finished its answer naturally;max_tokensmeans it was cut off by your token cap before finishing. Always checkblock.type == "text"first, sincecontentis an array that can hold other block types too.