1. The Messages API
Key Points
- Every capability reaches your code through one endpoint:
POST /v1/messages. There is no separate vision, tools, or streaming endpoint. - The three required top-level fields are
model,max_tokens, and a non-emptymessagesarray. - The system prompt is a top-level
systemparameter, never a{"role":"system"}entry inmessages[](except as a model-gated mid-conversation feature). - The response
contentis always an array of content blocks—always iterate and branch on each block'stype. - The API is stateless: resend the full conversation history on every request.
Reasoning, tool use, vision, extended thinking—everything reaches your code through a single doorway: the Messages API, POST /v1/messages. Every request carries three mandatory headers: x-api-key, anthropic-version: 2023-06-01, and content-type: application/json (a fourth, anthropic-beta, is added for beta features).
Required top-level fields
| Field | Type | Meaning |
|---|---|---|
model | string | A model ID, e.g. claude-opus-4-8 |
max_tokens | positive integer | Maximum tokens Claude may generate in its reply |
messages | non-empty array | The conversation so far |
A minimal, well-formed request:
{
"model": "claude-opus-4-8",
"max_tokens": 1024,
"messages": [
{ "role": "user", "content": "What is the capital of France?" }
]
}
The response's most important field is content—an array of content blocks, not a plain string:
{
"id": "msg_01ABC...",
"type": "message",
"role": "assistant",
"content": [
{ "type": "text", "text": "The capital of France is Paris." }
],
"stop_reason": "end_turn",
"usage": {
"input_tokens": 14,
"output_tokens": 9,
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 0
}
}
The stop_reason tells you why Claude stopped; usage reports token accounting. Because content is always an array, robust code never assumes content[0] is text—with thinking or tool use enabled, the first block may be a thinking or tool_use block. Always iterate and branch on each block's type.
Figure 5.1: Messages API request/response round-trip
System prompts vs. user/assistant messages
Here is the mistake that catches almost every newcomer. The system prompt is a top-level system parameter, not an entry in the messages array:
{
"model": "claude-opus-4-8",
"max_tokens": 1024,
"system": "You are a terse assistant. Answer in one sentence.",
"messages": [
{ "role": "user", "content": "Explain photosynthesis." }
]
}
The tempting error is to write {"role": "system", "content": "..."} inside messages[]. Think of the system parameter as the job description posted on the wall; messages is the transcript of the actual conversation. The system parameter accepts a plain string or an array of text blocks (the array form is needed for cache_control). Within messages, valid roles are user and assistant. Newer models such as Claude Opus 4.8 allow a model-gated mid-conversation system message; on models that don't support it, that entry returns a 400: role 'system' is not supported on this model. The rule to memorize: the primary system prompt is always the top-level system parameter.
Statelessness and multi-turn conversations
The Messages API is stateless—the server keeps no memory of prior calls. To hold a multi-turn conversation, you resend the full history on every request:
{
"model": "claude-opus-4-8",
"max_tokens": 1024,
"messages": [
{ "role": "user", "content": "My name is Alice." },
{ "role": "assistant", "content": "Nice to meet you, Alice." },
{ "role": "user", "content": "What's my name?" }
]
}
Three rules govern the array: (1) the first message must be user; (2) roles conventionally alternate; (3) content may be a plain string or an array of blocks (text, image, document, tool_use, tool_result, thinking, redacted_thinking). Because history is resent every turn, long conversations grow in cost—which is exactly why prompt caching matters.