stop_reason: refusal (200 OK)
An Anthropic Messages API response can return HTTP 200 OK with stop_reason: refusal, meaning the model declined to continue and the body carries a refusal instead of the answer your app expected. The request succeeded at the transport layer. The user got a refusal. Your uptime monitor saw a green 200 and stayed silent. 200 OK is a lie here: the success is in the HTTP status, the failure is in the body.
200 OK with stop_reason: refusal and a refusal in the body. Status-code monitoring sees a healthy 200 and never fires. Worse, stop_details.category is sometimes null, so you cannot always tell why. Log stop_reason on every call, alert on the rate of refusal, and handle it as a real failure path.
What it looks like
The response is well-formed and the status is 200. stop_reason is refusal rather than the normal end_turn, and the content is a refusal message:
{
"id": "msg_01...",
"type": "message",
"role": "assistant",
"stop_reason": "refusal",
"stop_details": { "category": "cyber" },
"content": [{ "type": "text", "text": "I'm not able to help with that." }]
}
Nothing in the HTTP transport signals a problem. stop_details.category may name a reason such as cyber, but it can also be null, leaving no machine-readable explanation at all.
Why it happens
Anthropic’s models can decline mid-generation when a request or the response being generated crosses a safety boundary. Rather than throwing an HTTP error, the API completes the request normally and reports the decline through stop_reason: refusal in the response body. The request was accepted and processed, so the status stays 200. In streaming mode the refusal arrives as part of the normal event sequence, not as an error event.
Why your monitoring can’t see it
Uptime checks and status-code dashboards key on HTTP status, and a 200 reads as healthy. Catching this requires inspecting stop_reason on every response, which almost no team logs or alerts on. It is the same blind spot behind OpenAI’s finish_reason: content_filter and mid-stream overloaded_error: the status code says success while the application layer failed. The null-category case is worse, because even teams that do inspect the body cannot always classify why the refusal happened.
400 or 403 rejection, which fails loudly with a non-2xx status your error handling already catches. stop_reason: refusal on a 200 is the dangerous case precisely because it does not.
How to detect and handle it
- 1. Log
stop_reasonon every message - Not just on errors. If your logging only captures non-2xx responses, you will never record a refusal.
- 2. Alert on the rate, not the instance
- A single refusal is noise. A spike means a prompt pattern, a model change, or a new user behavior worth knowing about.
- 3. Handle refusal as a failure path in code
- Do not render the refusal text as if it were a normal answer. Detect
stop_reason: refusal, then retry with an adjusted prompt, fall back, or surface an honest message. - 4. Capture
stop_details.categorywhen present - When the category is populated (for example
cyber) it tells you which boundary was crossed. When it isnull, correlate with the prompt and model instead.
How Dstl8 detects this
Dstl8 reads the actual content of every log line, not just its status code, so this failure gets caught even though the request came back as a clean 200. We track how often it is happening and how that rate moves over time. When it climbs after a deploy or a model swap, Möbius flags it and ties it back to the change that introduced it. The failure your status dashboard cannot see becomes a first-class signal with a root cause attached.
Related patterns
References
- Anthropic docs: handle streaming refusals: the vendor documentation for
stop_reason: refusaland how to handle it.
Third-party API behavior, verified July 2026. Providers change this behavior over time; re-checked quarterly.
Your monitor says 200. Your users got refused. See the difference.
Dstl8 surfaces the failures your status dashboard can’t: silent refusals, content filters, mid-stream errors. Cited to the log line.














