How to Handle Claude Opus 5 Refusals in Production: stop_reason, stop_details, and the Fallbacks Parameter
Claude Opus 5's safety classifiers can decline requests with stop_reason: refusal, even on legitimate security work. How to handle refusals in the Claude API, use the new fallbacks parameter, and design pipelines that degrade gracefully.
There's a class of production incident that only exists with frontier models: the request that succeeds at the HTTP layer and fails at the content layer. Claude Opus 5, which launched this week, makes this class bigger and more structured at the same time. It ships with real-time cybersecurity safety classifiers, and a request they decline comes back as a 200 with stop_reason: "refusal". Sometimes the content array is empty. Sometimes there's partial output, if the classifier fired mid-stream.
We do a lot of work for security-adjacent clients: SOC tooling documentation, compliance automation, log-analysis agents. That's exactly the neighborhood where false positives live. So before rolling any client onto Opus 5, I built the refusal-handling layer properly. Here's what the Claude API gives you and how we wired it.
Check stop_reason before reading content
The crash we saw within an hour of testing: code that reads response.content[0].text unconditionally. On a pre-output refusal, content is empty and that line throws an IndexError, which your error tracker then miscategorizes as a parsing bug. Every Opus 5 call site needs this branch:
response = client.messages.create(
model="claude-opus-5",
max_tokens=16000,
messages=messages,
)
if response.stop_reason == "refusal":
handle_refusal(response)
else:
text = next(b.text for b in response.content if b.type == "text")
Refusals carry a stop_details object with structured metadata: a category (such as "cyber") and an explanation. Treat it as informational rather than as the branch condition, because stop_details can be sparse and the reliable signal is stop_reason itself. Do log the category, though. It's the difference between knowing the classifier flagged offensive-security content and guessing.
Two billing details worth knowing. A refusal that fires before any output costs nothing. A mid-stream refusal bills the partial output that already streamed, and that partial output should be discarded rather than shown to users as a complete answer.
The Claude Opus 5 fallbacks parameter
Before Opus 5, handling a false-positive refusal meant catching it and re-sending the whole conversation to another model yourself: more code, another round trip, a cold prompt cache on the second model. Opus 5 introduces a server-side fallbacks parameter (beta) that does the retry inside a single API call:
response = client.messages.create(
model="claude-opus-5",
max_tokens=16000,
fallbacks="default",
messages=messages,
)
With fallbacks: "default", a request the classifiers decline is automatically re-run on a fallback model, and you get that model's answer back in the same response. Your application sees one call and one result. A stop_reason: "refusal" on the final response now means something stronger: the fallback path declined too, so this is a genuine content decision rather than a transient classifier disagreement.
Our policy after a week of testing is fallbacks: "default" on every route, with the response's model field logged so we can see when a fallback actually served the answer. The fallback rate on our benign security-documentation workload has been a fraction of a percent. But a fraction of a percent of an unattended overnight pipeline is the difference between a finished job and a 3 a.m. page.
Design the terminal case per surface
Fallbacks reduce refusals without eliminating them, so the pipeline still needs a terminal branch. What goes there is a product decision. Our defaults:
- Interactive tools: surface the refusal honestly. "The model declined this request (category: cyber)" with a link to our escalation channel. Never silently retry the same prompt in a loop; it wastes money and it doesn't work.
- Batch pipelines: quarantine the item with its
stop_details, continue the batch, and report refusal counts in the run summary. One refused document should never kill a 10,000-document job. - Agent loops: feed the refusal back as context and let the agent route around it. Told that a sub-task was declined, Opus 5 reformulates or flags the sub-task for a human instead of stalling.
Legitimate security work and the Cyber Verification Program
The classifiers target offensive-cyber capability, but defensive work lives next door, and adjacency is where false positives come from. Two mitigations that actually help.
Context in the prompt matters more than you'd expect. Our SOC-documentation agent refuses measurably less when the system prompt states the defensive context, the authorization, and the deliverable plainly.
And Anthropic runs a Cyber Verification Program for organizations doing legitimate security work. If that's your business, apply rather than engineering around refusals forever. Anthropic's Mythos-class models exist for exactly this population, but access is invitation-only through Project Glasswing; verification is the path most teams can take today.
The whole layer, in thirty lines
def resilient_call(messages):
response = client.messages.create(
model="claude-opus-5",
max_tokens=16000,
fallbacks="default",
messages=messages,
)
if response.stop_reason == "refusal":
category = response.stop_details.category if response.stop_details else None
raise ContentDeclined(category=category, request_id=response._request_id)
return response
Written once, imported everywhere. The teams that get burned by model safety layers aren't the ones doing sketchy work. They're the ones who assumed a 200 always contains an answer. Opus 5 makes the contract explicit. Check stop_reason, opt into fallbacks, design the terminal case, and the classifiers become a logged, budgeted, monitored part of your system instead of a mystery outage.
Related reading
An independent comparison of the three agentic models that launched this month. Real pricing, production failure modes, cost per completed task, and the routing table we actually run — not a benchmark aggregation.
A step-by-step tutorial for bulk AI document extraction with the Claude Opus 5 Batch API: structured outputs with JSON schemas, 50% batch pricing, the 300K output beta, and the real cost of processing 40,000 vendor contracts.
How to use the Claude Opus 5 effort parameter (low, medium, high, xhigh, max) to cut Claude API costs. Real per-request cost numbers, working Python code, and why we retired our Haiku/Sonnet/Opus routing layer.
Let's build something great.
Have a project in mind? We are an elite software and AI development studio ready to bring your ideas to production. Let's talk about your roadmap.