Claude Messages API

Beginner3 min

The three required parameters include max_tokens, which is optional almost everywhere else. Content comes back as an array of typed blocks, so indexing the first one breaks the day you enable tools.

#api

The three parameters a request cannot omit

A request to the Messages API needs model, messages, and max_tokens. Leave any one out and the call fails before a model sees it.

const response = await client.messages.create({
  model: 'claude-sonnet-5',
  max_tokens: 1024,
  messages: [{ role: 'user', content: 'Summarize this ticket in one sentence.' }],
})

The first two are unsurprising. The third is the one people coming from other APIs forget, because elsewhere it is optional.

Why max_tokens is required and what it does not promise

max_tokens is "the maximum number of tokens to generate before stopping." It is a ceiling, not a target, and the reference is explicit that "our models may stop before reaching this maximum."

Two consequences follow, and they pull in opposite directions.

Set it too low and long answers stop mid-sentence. The request succeeds, the response looks fine, and the truncation shows up as a parse failure somewhere downstream. stop_reason is how you catch it, and inference covers why truncation arrives disguised as success.

Set it high and nothing bad happens directly, since you are billed for tokens generated rather than tokens allowed. What a high ceiling costs is the ability to bound a runaway response. The value is also capped per model: "Different models have different maximum values for this parameter", so a number that works on one model is rejected by another.

The system prompt is a parameter, not a message

There is no system role in this API. The reference says so in one sentence: "there is no "system" role for input messages in the Messages API."

const response = await client.messages.create({
  model: 'claude-sonnet-5',
  max_tokens: 1024,
  system: 'You are a support assistant. Answer only from the supplied policy.',
  messages: [{ role: 'user', content: 'Can I return this after 40 days?' }],
})

If you have used an API where the system prompt is the first entry in the message array, this is the difference worth internalizing. The instruction sits outside the conversation, which means it is not something the model can be talked out of by a later turn in the same way, and it means a stable instruction stays in the same place in the request every time. System prompting covers what belongs there and why the placement shows up on the bill.

Turns, and what happens when two of the same kind are adjacent

The models are "trained to operate on alternating user and assistant conversational turns", and you build a conversation by sending the prior turns each time.

A natural worry is what happens when your code appends two user turns in a row, which is easy to do when tool results and user text both arrive as user messages. The answer is that nothing breaks: "Consecutive user or assistant turns in your request will be combined into a single turn."

That cuts both ways. Your code does not need to enforce strict alternation, and you should not rely on two adjacent turns staying separate, because the model sees one.

Reading the response: blocks, stop reason, usage

The response is not a string.

// response.content is an array of blocks, not text.
const text = response.content
  .filter((block) => block.type === 'text')
  .map((block) => block.text)
  .join('')

content is a list of typed blocks. A plain answer arrives as a single text block, which is why response.content[0].text works in tutorials and then stops working the moment a tool call or a thinking block appears in the same response. Filtering by type is the version that keeps working.

Two other fields carry information your code should read rather than ignore.

stop_reason says why generation ended. end_turn means the model finished. Anything else, and in particular a length-based stop, means the response is incomplete even though the request succeeded.

usage reports the token counts the request is billed on, including cached and uncached input separately. Reading them from the response is more reliable than counting tokens yourself, and it is what cost and latency monitoring is built on.

Further reading

Knowledge check

Question 1 of 4

Your first request is rejected before a model sees it. Which parameter is most often the missing one for developers coming from other APIs?

Sign in to save your progress and pick up where you left off.

Open this article on its own page