AI agent API errors are no longer a minor developer-experience concern. When an autonomous model receives a vague refusal, it can waste tool calls, burn tokens, repeat blocked actions, and leave the user with the impression that the agent is simply stuck.

A recent discussion in r/SaaS offers a compact but important case study. The builder of a paper-trading product said their MCP server exposes 22 tools, yet an authorization refusal returned only a bare error string—without a stable code, structured reason, or log record. The predictable result: the agent retried the request in a loop. After sharing the missing capability rather than a polished feature, commenters helped turn the response into a more useful contract: a stable error code, the constraint hit, a recovery class, and a deliberately limited payload for the agent. (reddit.com)

That change points to a broader lesson for anyone building tools for AI systems: an error response is not an apology. It is an instruction for the next decision.

The real problem: agents cannot reliably infer intent from a string

Traditional API consumers are usually programs written by developers. They can be configured around a documented 403, a known exception type, or an SDK-specific failure. Human developers can also open logs, inspect a dashboard, read a support article, and interpret a sentence such as “access denied.”

AI agents are different. They are probabilistic planners operating under incomplete context. A bare string may be understandable to a human, but it gives an agent no dependable branching condition. The model has to guess whether it should retry, alter arguments, ask the user for approval, wait until a quota resets, choose another tool, or stop altogether.

That ambiguity produces a particularly expensive failure mode: repeated tool invocation. If the agent sees only a generic refusal, it may treat the result as transient. It attempts the same action again, perhaps with tiny wording or parameter changes that cannot possibly satisfy the underlying authorization rule. The application pays for the repeated inference and tool execution while the user gets no progress.

The r/SaaS post captures this failure precisely. The original response did not say whether a key lacked a permission, a daily cap had been reached, or an owner-controlled lock blocked the action. Those are very different states. They should lead to very different agent behavior. (reddit.com)

This is not unique to finance, MCP, or SaaS products. The same problem appears when an agent cannot send an email, access a CRM record, create a support ticket, make a purchase, modify production infrastructure, or view a document. In every case, the consumer needs to know whether the problem is temporary, fixable, escalatable, or final.

Why AI agent API errors need a recovery contract

A good API error has always needed to be machine-readable. RFC 9457, the IETF standard for HTTP problem details, exists because status codes alone often cannot communicate enough context to non-human API consumers. It defines a structured way to express the nature of a problem instead of forcing every client to parse a custom sentence. (rfc-editor.org)

For agentic software, that principle needs to go further. The most useful error response answers three questions:

  1. What happened? A stable error identifier and an understandable summary.
  2. Why did it happen? The relevant policy, state, validation rule, or system constraint.
  3. What is the agent allowed to do next? Retry later, change inputs, ask for user intervention, select an alternative, or stop.

The third question is the important extension. An ordinary API can assume a developer will decide what to do with an error. An AI agent needs enough information to make a bounded operational decision during a live workflow.

Errors are part of the tool interface

Teams often treat the happy-path schema as the “real” API contract and errors as a secondary concern. For an agent, that is backwards. A tool is defined as much by the conditions under which it cannot execute as by the result it returns when it can.

Consider a tool named place_paper_trade. Its success schema might include an order ID, quantity, status, and timestamp. But the agent also needs predictable negative outcomes:

  • daily_order_limit_reached
  • trading_desk_frozen
  • instrument_not_enabled
  • market_closed
  • invalid_position_size
  • approval_required

Each failure maps to a distinct plan. A stable interface makes that mapping possible without asking a language model to reverse-engineer business policy from prose.

The protocol may carry errors, but your product owns the semantics

MCP tools are model-controlled capabilities: models can discover and invoke them based on tool descriptions and context. The protocol supports tool invocation and error signalling, but it does not know the specific business meaning of an account limit, a user-controlled lock, or an organization’s approval policy. (modelcontextprotocol.io)

That distinction matters. Protocol-level errors should cover malformed messages, unavailable transport, and other failures of the interaction itself. Product-level refusals should live in a documented domain contract that lets the agent understand what constraint it encountered and what recovery behavior is appropriate.

The community insight: classify recovery, not just failure

The strongest idea in the original post is not simply “add an error code.” It is the proposal to attach a recovery class.

The builder described two examples:

  • A daily cap should be marked as retryable after a known reset.
  • A trading desk frozen by its owner should be marked as non-retryable because the agent has no authority to lift the lock.

That distinction is subtle but operationally powerful. Both outcomes may look like a refusal. Neither means the tool implementation crashed. Yet one calls for waiting, and the other calls for ending the attempt or escalating to a human.

A commenter added an adjacent case: silence or an empty result can be worse than an explicit error. If the response shape does not distinguish “the tool ran and found nothing” from “the tool hung” or “the server failed to answer,” an agent may invent its own explanation. The original poster agreed that every non-ideal outcome needs a machine-readable reason, including empty-result states. (reddit.com)

A practical recovery taxonomy

Avoid trying to encode every business rule into a single Boolean such as retryable. It is too coarse. “Yes, retry” lacks timing; “no” does not distinguish an input correction from a human approval flow.

A better starting taxonomy looks like this:

Recovery classMeaningTypical agent action
retry_afterThe same request may succeed after a known time or event.Wait until the reset time, then retry once.
change_inputThe request is invalid or incomplete, but an adjusted request may work.Correct parameters using field-level guidance.
request_approvalThe action requires a user or authorized approver.Ask for confirmation or route to an approval workflow.
use_alternativeThis route is unavailable, but another supported path may satisfy the goal.Select a different tool, account, or method.
do_not_retryRepeating the call cannot change the outcome.Stop the branch and explain the constraint.
investigateThe service cannot safely classify the fault.Avoid automatic loops; record context and surface a concise failure.

These labels are not universal standards. They are domain-level control signals. The value comes from defining them narrowly, documenting them, and testing agent behavior against them.

Retryability is not permission

A common mistake is to interpret “retryable” as “the agent should keep trying.” It should mean only that a future retry under specified conditions could be valid.

For example, a rate limit might permit a retry after 30 seconds. That does not mean an agent should queue 20 identical retries. The contract should include a reset time or delay, and the orchestrator should apply a retry budget, jitter where appropriate, and a maximum attempt count. In many workflows, the best action is to continue with other work and return to the blocked step later.

Design one response envelope for success, refusal, and empty results

The Reddit thread’s proposed solution uses the same broad shape for a refusal as for a success. That is a sound design choice. Consistent envelopes simplify tool consumers and reduce the chance that a model misses important fields because it has switched from one unfamiliar object format to another.

Here is an illustrative result envelope for an MCP-facing tool. It is not an MCP standard; it is a product-level contract that can be carried in the tool’s structured output or content.

{
  "ok": false,
  "outcome": "refused",
  "data": null,
  "error": {
    "code": "DAILY_ORDER_LIMIT_REACHED",
    "constraint": "daily_order_limit",
    "recovery": "retry_after",
    "limit": 25,
    "remaining": 0,
    "reset_at": "2026-09-06T00:00:00Z",
    "safe_message": "The daily paper-trading order limit has been reached."
  },
  "request_id": "req_8f2d1"
}

An owner-controlled freeze can use the same envelope while directing completely different behavior:

{
  "ok": false,
  "outcome": "refused",
  "data": null,
  "error": {
    "code": "DESK_FROZEN_BY_OWNER",
    "constraint": "desk_status",
    "recovery": "do_not_retry",
    "safe_message": "This desk is frozen by its owner and cannot be changed by this agent."
  },
  "request_id": "req_2b91a"
}

The agent can branch on error.code when it needs a precise rule and on error.recovery when it needs a general planning decision. The human-readable safe_message still matters, but it is not the control plane.

Treat empty outcomes as first-class states

Do not overload an empty array, empty text string, null, or missing response field to mean several things. A search tool returning no matching records is a legitimate completed result. A data source returning no answer because it lacks permissions is a refusal. A timeout is an execution problem. These should not collapse into the same representation.

For example:

{
  "ok": true,
  "outcome": "empty",
  "data": {
    "items": []
  },
  "meta": {
    "reason": "NO_MATCHES"
  },
  "request_id": "req_5c7aa"
}

This permits an agent to say, “I searched the selected account and found no matching invoices,” rather than repeatedly expanding its query because it cannot tell if the first query ran.

Stable codes beat clever messages

Natural-language error messages change. Product names are revised, legal language gets updated, localization expands, engineers improve wording, and customer support asks for more context. If an agent branches on message text, every edit risks breaking automation.

A stable error code solves that problem, provided it is designed as an API surface rather than a debugging artifact.

Rules for durable error codes

Use these guidelines when defining codes:

  • Name the domain condition, not the implementation exception. Prefer ACCOUNT_PERMISSION_DENIED to SQL_POLICY_CHECK_FAILED.
  • Keep codes stable across wording changes. A new sentence should not require a new code.
  • Do not reuse a code for a different meaning. Deprecate old codes and introduce new ones if semantics change.
  • Avoid encoding volatile details. LIMIT_REACHED is better than LIMIT_25_REACHED.
  • Document expected agent behavior. State whether a code requires stopping, waiting, changing input, or requesting approval.
  • Separate public and internal identifiers. A safe external code should not expose system topology, account IDs, or sensitive policy logic.

The HTTP problem-details model similarly emphasizes a machine-readable problem type alongside human-readable fields. It is a helpful precedent even when the tool transport is JSON-RPC or MCP rather than a conventional REST endpoint. (rfc-editor.org)

Keep a compatibility policy

Once agents, workflow engines, and customer scripts depend on a code, changing it is a breaking change. Publish a small error-code catalog with a lifecycle policy: active, deprecated, removal date, replacement code, and behavior notes.

This may sound heavyweight for an early-stage product. In reality, a short Markdown table and contract tests are usually enough. The cost of discipline is far lower than debugging why a production agent started retrying a permanently blocked action after an unannounced copy change.

Share enough context to act, but not enough to leak or bloat

The original post makes another important design choice: retain the full audit row server-side while sending the agent only the recovery class, limit, and reset time. That is a practical response to two constraints that agent-tool builders often ignore.

First, tool output can enter model context. Passing a detailed authorization envelope to many bots may leak information about other users, policies, internal operations, or security controls. Second, verbose payloads add tokens to every tool cycle. Across multi-step agents and high-volume workflows, unnecessary diagnostic text becomes a material cost and reliability issue.

The two-layer error model

A robust approach separates agent-safe context from operator-grade diagnostics.

The agent-safe response should include only what the agent needs to make the next permitted decision:

  • Stable code
  • Recovery class
  • Safe constraint name
  • Retry time, when applicable
  • Input fields that need correction, when applicable
  • A correlation or request ID
  • A concise human-facing explanation

The private audit record can include much more:

  • Authenticated principal and key ID
  • Policy evaluation path
  • Rule version
  • Resource identifiers
  • Request hashes and sanitized arguments
  • Upstream provider response
  • Timestamp and execution duration
  • Trace ID, service version, and deployment region
  • Security-risk annotations

The agent should never need a raw policy trace to decide whether to stop. An operator often needs it to determine whether the refusal was correct.

Error messages are a security boundary

Excessive specificity can help attackers enumerate resources or understand access controls. For instance, “User 9283 owns desk alpha and froze it at 09:14” is operationally useful but may be inappropriate for a tool invoked under a restricted credential. A safer message is simply that the requested action is unavailable because the desk is owner-frozen.

This does not mean errors should be opaque. It means disclose the actionable category while keeping sensitive evidence and detailed policy evaluation on the server. A request ID allows support and operations teams to bridge the two layers without making every agent response an audit export.

Observability is the missing half of refusal handling

The most revealing sentence in the original post is that the refusals were not logged. Without logs, the builder could not quantify which failures occurred, whether agents were looping, what permissions were most confusing, or whether a product policy was blocking legitimate work. That absence is not a reporting problem; it is a product-design blind spot. (reddit.com)

If an agent cannot complete a goal, that outcome belongs in product analytics just as much as a successful conversion or sent message.

What to measure

At a minimum, emit a structured event for every tool outcome—success, refusal, empty result, validation error, timeout, cancellation, and internal failure. Capture a controlled vocabulary rather than only a free-text message.

Useful metrics include:

  1. Refusal rate by tool and code. Which operations are most often blocked?
  2. Repeat-call rate after each refusal. Are clients and agents behaving as your recovery class intends?
  3. Time to successful recovery. For retryable conditions, how long does it take before the user’s goal is completed?
  4. Escalation rate. Which errors force humans into the loop most often?
  5. Empty-result rate. Is “no data” a valid answer, a discoverability failure, or evidence of a bad query experience?
  6. Token and latency cost per failed workflow. How costly are refusal loops compared with successful paths?
  7. Unknown-code frequency. If clients encounter undocumented errors, your contract is drifting.

Build a refusal dashboard

A simple dashboard can reveal high-leverage improvements. Imagine DESK_FROZEN_BY_OWNER is common but agents repeatedly attempt the action three times. The issue may not be the underlying policy; it may be that clients do not understand do_not_retry, the tool description overpromises what the action can do, or the agent’s system prompt does not prioritize structured fields.

Likewise, a high volume of INVALID_SYMBOL outcomes could suggest input validation needs to happen before a costly downstream call. A spike in RATE_LIMITED events after launching a new integration might call for better quota visibility, batching, or a more conservative orchestration policy.

The July 2026 MCP specification update also reflects the ecosystem’s growing emphasis on operational reliability: it introduced a stateless protocol core, cacheable list results, header-based routing, authorization hardening, and updated SDKs. Those changes do not replace product-level error design, but they reinforce that agent infrastructure is moving from demos toward systems that need predictable scaling, routing, and governance. (blog.modelcontextprotocol.io)

How to implement an agent-ready error contract

You do not need to redesign every endpoint in one release. Start with the highest-cost and highest-risk tools: those that spend money, change state, access sensitive records, or are likely to hit quotas and permissions.

Step 1: Inventory non-success outcomes

For each tool, list every meaningful result that is not a straightforward success. Include expected business refusals, empty states, validation failures, dependency errors, authorization failures, and cancellations.

Ask four questions for each one:

  • Can the same request succeed later without any change?
  • Can a changed argument make it succeed?
  • Can the agent resolve it itself?
  • What is safe to disclose to the agent?

The answers determine code, recovery class, and payload fields.

Step 2: Define a small shared schema

Use a stable top-level envelope across tools. Keep it boring. A shared ok, outcome, error, and request_id shape is easier for every client, test, and prompt to understand than a collection of per-tool conventions.

Avoid adding dozens of optional fields from the start. Create a core schema, then permit narrow extensions for particular cases, such as retry_at, field_errors, approval_url, or alternative_tools.

Step 3: Write behavior into tool descriptions

Tool descriptions should explain important constraints before the model invokes the tool. If a tool cannot execute when an account is frozen, say so. If it may return retry_after, say that the agent must not retry before reset_at. If user confirmation is required for high-impact actions, declare the condition explicitly.

This reduces failures, but it does not eliminate the need for structured responses. Tool descriptions tell the model what may happen; result objects tell it what did happen in this specific invocation.

Step 4: Add client-side guards

Do not place all responsibility on the model. The orchestration layer should enforce policies around retries and escalation.

For example:

if recovery == "do_not_retry":
    stop_this_action()
    summarize_constraint_to_user()

if recovery == "retry_after":
    schedule_one_retry_at(reset_at)
    continue_other_independent_steps()

if recovery == "request_approval":
    request_user_confirmation()
    do_not_execute_until_confirmed()

The model can choose a plan, but deterministic infrastructure should enforce limits that protect the user, the product, and the bill.

Step 5: Test failures as agent journeys

Unit-test the schema, but also run end-to-end tests that simulate an agent’s workflow. Assert that a daily limit produces one scheduled retry rather than five immediate calls. Assert that an owner freeze leads to a clear user-facing explanation. Assert that an empty search result is reported as completed rather than retried as a transport failure.

Failure-path tests are especially valuable when upgrading SDKs, changing prompts, or adding new models. The model may change how it interprets prose, but a well-implemented recovery controller can preserve the operational invariant.

What not to do

The following patterns are common because they are quick to ship, but they create hidden costs once tools are used by agents.

Do not return only a prose string

“Permission denied” may be fine for a terminal user. It is insufficient for a tool consumer that must choose the next action. At minimum, supply a stable code and recovery guidance.

Do not label every failure as transient

A generic retry policy creates loops, load amplification, and confusing user experiences. A permanent policy refusal should be explicitly terminal for the current agent authority.

Do not expose internal policy evidence by default

More JSON is not necessarily more useful. Return the minimum safe information needed to recover and preserve detailed traces for authorized operators.

Do not confuse empty data with an error

A completed search with zero results deserves a different result type from a timeout, permission refusal, or malformed request. The distinction improves both planning and user communication.

Do not rely on prompt text alone

A system prompt can say “do not retry denied actions,” but prompts are not a substitute for a structured contract and deterministic retry controls. Put the semantics in the payload and the enforcement in the orchestration layer.

The business case: better errors improve conversion and trust

Structured refusal handling is often framed as an engineering-quality improvement. It is also a product and marketing advantage.

For users, reliable recovery reduces the most damaging kind of AI failure: confident but unproductive activity. An agent that says, “I cannot place this order because the owner has frozen the desk; I will not retry. Ask the owner to unfreeze it,” is far more trustworthy than one that repeatedly claims to be working.

For support teams, request IDs and coded outcomes make tickets diagnosable. For product teams, refusal telemetry reveals feature gaps and permission-model friction. For founders, retry controls reduce avoidable model and infrastructure spend. For platform teams, a documented error contract lowers integration risk for customers building their own agents.

There is also a positioning opportunity. As MCP servers proliferate, tool quality will increasingly be judged by what happens when conditions are imperfect. A catalog with impressive actions but ambiguous failures is difficult to operationalize. A smaller tool set with clear authority boundaries, predictable outcomes, and safe recovery instructions may be more valuable in production.

A compact checklist for shipping better AI agent API errors

Before releasing or revising an agent-facing tool, review this checklist:

  • Does every non-success outcome have a machine-readable code?
  • Can the client tell refusal, empty completion, validation error, timeout, and internal failure apart?
  • Is there a recovery class that communicates the allowed next action?
  • For retryable outcomes, is a precise reset time or delay included?
  • For input problems, are corrective fields described without exposing sensitive internals?
  • Are permanent denials explicitly marked as non-retryable?
  • Does every result include a correlation ID?
  • Are complete audit details stored server-side?
  • Are response payloads safe for model context and token budgets?
  • Do integration tests verify that agents stop, wait, escalate, or alter inputs as intended?

If several answers are no, the tool may still work in happy-path demos. It is not yet ready for dependable autonomous operation.

Conclusion: refusal design is agent design

The r/SaaS discussion began with a simple admission: a bare error string caused an agent to retry blindly. The community response transformed that defect into a better engineering model—structured outcomes, explicit constraints, recovery classes, limited agent-safe context, and server-side auditability. (reddit.com)

The key takeaway is straightforward. AI agents do not need more verbose error messages; they need clearer boundaries and actionable state. When an API tells an agent whether to wait, change course, ask for approval, use an alternative, or stop, it turns failure from an ambiguous dead end into a controlled part of the workflow.

For builders of MCP servers and other agent-facing APIs, that is not edge-case polish. It is the difference between a tool that looks capable and one that can be trusted in production.

FAQ

What are AI agent API errors?

AI agent API errors are structured failure or non-success responses designed for software agents, not just human developers. They should explain the failure category and provide machine-readable guidance about whether the agent should retry, modify inputs, escalate, use another route, or stop.

Why do bare error strings cause retry loops?

A text-only refusal does not reliably tell an agent whether the condition is temporary or permanent. If the agent interprets the failure as transient, it may repeat the same tool call even when no retry can change the result.

What should an MCP tool return when access is denied?

Return a consistent result envelope containing a stable error code, a safe description of the relevant constraint, a recovery class such as do_not_retry or request_approval, and a request ID. Keep sensitive policy details and audit evidence on the server.

Should every API error be retryable?

No. Retry only makes sense when time, system state, or a documented event can change the result. Permanent permission denials, owner-controlled locks, and invalid actions should tell the agent to stop or escalate instead of trying again.

How do you handle an empty tool result?

Represent it as a successful completed outcome with an explicit empty-state reason, such as NO_MATCHES. Do not make the agent infer whether an empty list means no data, a permission issue, a timeout, or a failed tool call.