Messages API Best Practices
A collected set of practices for structuring roles, turns, content blocks, and system prompts reliably when building against the Messages API.
Search across all documentation pages
A collected set of practices for structuring roles, turns, content blocks, and system prompts reliably when building against the Messages API.
Treat this as a working reference to check your code against, not a one-time read.
assistant, so build conversations from the first user input forward.user or assistant turns - combine multiple pieces of user input into one turn's content instead of splitting them across separate array entries.response.content, not just extracted text, as the next assistant turn. This preserves tool_use blocks and any other structured content needed for the conversation to continue correctly.user turn - system is the correct place for persona, scope, and constraints that apply to the whole conversation.system make it a different string on every request, silently breaking prompt caching.messages, system is not cumulative - pass the identical value each time you want consistent behavior.role: "system" message preserves your cached prefix better than rewriting the top-level system field mid-conversation.text for prose, image for visual input, tool_use for a model-initiated tool call, and tool_result for the answer to one - don't force everything into plain strings.block.input as the parsed object it is; never string-match the serialized JSON, since exact escaping can vary.is_error: true instead of omitting it.text - branch on block.type first, since assistant turns can include tool_use alongside or instead of text.count_tokens(), not character-count estimates. Token counts don't map linearly to string length, especially for code or non-English text.tool_use, drop the corresponding user turn's tool_result too - never leave one half of a pair orphaned.assistant or with adjacent same-role turns; fix this before sending.RateLimitError, NotFoundError, and AuthenticationError from the broader APIStatusError instead of catching one generic exception class.stop_reason before trusting a response is complete. A stop_reason of max_tokens means the reply was cut off, not finished naturally.max_tokens. Non-streaming requests with a high token ceiling risk hitting client-side HTTP timeouts; streaming avoids that failure mode.max_tokens deliberately, not as an afterthought. Too low truncates output mid-thought and forces a retry; size it to the task, with headroom for thinking or tool calls where relevant.Broken role alternation - either the array doesn't start with user, or two same-role turns appear back to back. Check the structure before checking the content.
Because system isn't cumulative like messages - each request is evaluated independently, so omitting it or changing it silently shifts behavior and can invalidate a prompt cache built on the prior text.
No - every tool_use block needs a matching tool_result, even if the tool call failed. Use is_error: true for a failed call rather than omitting the result entirely.
No - token counts don't map linearly to character or word counts, especially for code or non-English text, so estimates can be significantly off in either direction.
Because summarization is a comparatively simple task that doesn't need your primary model's full reasoning capability - a faster, cheaper model reduces the cost of trimming without a meaningful quality loss.
The stop_reason field - if it's max_tokens rather than end_turn, the reply was cut off and may be missing content you expect to be there.
Whenever max_tokens is set high enough that a non-streaming request risks a client-side HTTP timeout - streaming avoids that failure mode entirely.
Because the exact JSON serialization of tool input can vary in escaping between requests or models - always parse block.input as structured data rather than pattern-matching the raw text.
No - a shared, mutable list used by concurrent conversations will interleave turns from different users and break the required role alternation for all of them.
It makes the system prompt a different string on every request, which silently breaks prompt caching since caching relies on a stable, unchanged prefix.
Generally no - the official SDK retries 429 and 5xx responses automatically with backoff by default, so hand-rolled retry logic is usually unnecessary unless you need custom behavior.
Stack versions: Written against the Claude model lineup current as of ~June 2026 - Claude Fable 5, Claude Opus 4.8, Claude Sonnet 5 (the default), and Claude Haiku 4.5 - and the official
anthropicPython SDK (latest 0.x release). Model names, SDK versions, and pricing move quickly - verify current specifics at platform.claude.com/docs before relying on them.
Reviewed by Chris St. John·Last updated Jul 18, 2026