Lesson 2.1
The Messages API
The anatomy of a Messages API request and response, the rules for roles and content blocks, and the fields you must read on every reply.
Everything you build with Claude goes through one endpoint: POST /v1/messages. Tools, vision, caching, structured output and streaming are all options on this single call. The exam expects you to know the required fields, the shape of a response, and the handful of rules that produce 400 errors when broken.
The request
A minimal request has three required fields: model, max_tokens and messages.
{
"model": "claude-opus-5-5",
"max_tokens": 1024,
"system": "You are a concise technical writer.",
"messages": [
{"role": "user", "content": "Explain idempotency in one paragraph."}
]
}
modelis a model ID or alias. IDs change as models are released, so read them from the models page on docs.anthropic.com rather than memorising them.max_tokensis the hard ceiling on output tokens. It is enforced, not a target; if the reply hits it,stop_reasonbecomesmax_tokensand the output is truncated.messagesis the conversation. The first message must haverole: "user", and roles areuserorassistant. Consecutive same-role messages are merged into one turn.systemis a top-level field, not a message. It sets persona, rules and context for the whole conversation.
Optional fields you will meet repeatedly: temperature (randomness, where the model supports it), stop_sequences (strings that end generation), tools and tool_choice, metadata.user_id (an opaque identifier for abuse detection), stream, and thinking for extended reasoning on models that support it.
Content: strings or blocks
content can be a plain string or an array of typed blocks. A string is shorthand for one text block. Blocks are how you send images, documents and tool results, and how Claude returns text, tool calls and thinking.
{"role": "user", "content": [
{"type": "image", "source": {"type": "url", "url": "https://example.com/chart.png"}},
{"type": "text", "text": "What trend does this chart show?"}
]}
Response blocks you must recognise: text, tool_use (with id, name, input), and thinking. Request-only blocks include image, document and tool_result.
The response
{
"id": "msg_01XFDUDYJgAACzvnptvVoYEL",
"type": "message",
"role": "assistant",
"model": "claude-opus-5-5",
"content": [{"type": "text", "text": "Idempotency means..."}],
"stop_reason": "end_turn",
"stop_sequence": null,
"usage": {"input_tokens": 31, "output_tokens": 98,
"cache_creation_input_tokens": 0, "cache_read_input_tokens": 0}
}
Read three things on every response. content is always an array, so iterate and branch on type rather than assuming content[0].text. stop_reason tells you whether the turn finished (end_turn), wants tools (tool_use), was cut off (max_tokens), hit a stop_sequence, paused (pause_turn) or was declined (refusal). usage is your billing record and the only reliable way to verify prompt caching.
Calling it from the SDKs
import anthropic
client = anthropic.Anthropic() # reads ANTHROPIC_API_KEY
resp = client.messages.create(
model=MODEL, max_tokens=1024,
system="You are a concise technical writer.",
messages=[{"role": "user", "content": "Explain idempotency in one paragraph."}],
)
print(next(b.text for b in resp.content if b.type == "text"))
print(resp.usage.input_tokens, resp.usage.output_tokens)
const resp = await client.messages.create({
model: MODEL, max_tokens: 1024,
system: "You are a concise technical writer.",
messages: [{ role: "user", content: "Explain idempotency in one paragraph." }],
});
for (const block of resp.content) if (block.type === "text") console.log(block.text);
With raw HTTP you send x-api-key, anthropic-version: 2023-06-01 and content-type: application/json headers. The SDKs add these for you.
Multi-turn conversations
The API is stateless. To continue a conversation you resend the entire history with the new user turn appended:
history = [
{"role": "user", "content": "My name is Priya."},
{"role": "assistant", "content": "Nice to meet you, Priya."},
{"role": "user", "content": "What is my name?"},
]
Appending the previous resp.content array (not just its text) as the assistant turn preserves tool calls and other blocks that later turns may depend on.
Common validation errors
| Mistake | Result |
|---|---|
First message is assistant | 400 invalid_request_error |
Missing max_tokens | 400 |
Empty messages array | 400 |
| Unknown or unavailable model ID | 404 not_found_error |
system placed inside messages as a role on older models | 400 |
Exam focus
model,max_tokensandmessagesare required;systemis a separate top-level field, not a message role.- The first message must be from
user; the API merges consecutive same-role messages. contentis an array of typed blocks in responses; always branch ontype.max_tokensis an enforced ceiling, andstop_reason: "max_tokens"means the output was truncated.- The API is stateless: resend the full history each turn, appending the full assistant
contentarray. usagereports input, output and cache token counts and is the source of truth for cost.
Check yourself
5 questions
1.A developer sends this request body to POST /v1/messages and receives a 400 invalid_request_error: { "model": "<current-model-id>", "messages": [{"role": "user", "content": "Hello"}] } Which required parameter is missing?
2.Coming from another provider, a developer writes: messages = [ {"role": "system", "content": "You are a terse SQL assistant."}, {"role": "user", "content": "List all tables."} ] What happens when this is sent to the Messages API?
3.A response from the API looks like: {"id": "msg_01", "type": "message", "role": "assistant", "content": [{"type": "text", "text": "There are 12 tables."}], "stop_reason": "end_turn", "usage": {"input_tokens": 40, "output_tokens": 9}} The developer's code does console.log(response.content.text) and prints undefined. What is wrong?
4.A developer calls the API with raw HTTP: curl https://api.anthropic.com/v1/messages \ -H "Authorization: Bearer sk-ant-api03-..." \ -H "content-type: application/json" \ -d '{"model": "...", "max_tokens": 256, "messages": [...]}' They get a 401 authentication_error. Which change fixes the request?
5.A request sets stop_sequences: ["</answer>"]. The response has stop_reason "stop_sequence" and stop_sequence "</answer>". What happened?
