API Features & Capabilities

Stop Reasons: Decoding end_turn, max_tokens, stop_sequence, and tool_use

Every successful Messages API response tells you why the model stopped generating. Production code that ignores stop_reason silently truncates answers, drops tool calls, and misreads refusals as content.

Claude 3P 101 · Updated July 2026 · Unofficial guide

A 200 response from the Messages API is not one kind of event — it is several, distinguished by the stop_reason field on the returned message. Some values mean "done, ship it"; others mean "you have work to do before this response is usable". This article walks the values you will actually see and the correct branch for each.

The everyday four

end_turn — the model finished its turn naturally. This is the happy path: the content is complete, render or store it.

max_tokens — generation hit the max_tokens ceiling you set. The output is truncated, and if you were using structured outputs or tool calls, it may be incomplete or invalid JSON — the official guidance is to retry with a higher max_tokens. Remember that on thinking-enabled models, thinking tokens count toward max_tokens too, so a generous reasoning phase can starve the visible answer; raise the limit or lower effort. Treat max_tokens as an error state in pipelines: never persist a truncated answer as if it were complete.

stop_sequence — generation hit one of the custom stop strings you supplied in the request's stop_sequences parameter. The matched string is reported alongside so you can tell which one fired. This is deliberate truncation — your own delimiter logic — so it usually maps to "done", just by your rule rather than the model's.

tool_use — the model is asking your application to run one or more tools. The response contains tool_use content blocks; execute them, then send all results back as tool_result blocks in a single user message and call the API again. A response can contain several tool calls at once, and failures should go back with "is_error": true rather than being swallowed. Nothing here is final output — treating a tool_use response as an answer is one of the most common integration bugs.

The newer values your switch statement probably misses

stop_reasonMeaningCorrect response
pause_turnA server-tool loop (web search, code execution) hit its iteration limit (default 10)Re-send the conversation including the assistant response as-is; the server resumes automatically — do not append a "continue" message
refusalA safety refusal, returned with HTTP 200; on Claude Fable 5 a stop_details.category field says why (e.g. "cyber", "reasoning_extraction")Do not retry verbatim in a loop; surface or log it. With structured outputs, note you are billed and output may not match the schema
model_context_window_exceededOn Claude 4.5+ models, input plus max_tokens overflowed the context window and generation stopped at the boundaryTrim context, or enable compaction / context editing
compactionYou enabled compaction with pause_after_compaction: true and the API stopped after writing the summaryHandle continuation yourself, then resume the conversation

A production-shaped branch

msg = client.messages.create(model="claude-opus-4-8", max_tokens=4096,
                             messages=history, tools=tools)
match msg.stop_reason:
    case "end_turn" | "stop_sequence":
        deliver(msg.content)
    case "tool_use":
        history += [assistant(msg), tool_results(run_tools(msg))]
    case "pause_turn":
        history.append(assistant(msg))          # resend unchanged
    case "max_tokens" | "model_context_window_exceeded":
        handle_truncation(msg)                  # never persist as complete
    case "refusal":
        log_refusal(msg)                        # don't blind-retry

Two habits make this robust. First, make the branch exhaustive with an explicit default that raises or alerts — new stop reasons have been added over time (refusal and model_context_window_exceeded arrived with the Claude 4 generation), and a silent fall-through is how truncation bugs ship. Second, when streaming, remember the final stop_reason arrives in the closing message_delta event, so your handler must not commit the response before the stream ends.

Rule of thumb: only end_turn and your own stop_sequence mean "this content is finished". Everything else is a control-flow signal.

Stop reasons behave the same across the Claude API, Claude Platform on AWS, Amazon Bedrock's Messages surface, Google Vertex AI, and Microsoft Foundry, since all speak the Messages API. Platform differences show up one level down — which features (server tools, compaction) can produce the newer values at all; check the availability matrix.

Where to go next

Stop reasons cover successful responses; error codes cover the failures. For the tool-use loop end to end, see tool choice, and for window overflow, 1M-token context strategies.

Sources