API strategy

Agent infrastructure

API error responses for AI agents: a practical guide to structured, recoverable errors

API error responses for AI agents: how to design structured, recoverable errors with RFC 7807, code taxonomies, retry hints, and remediation fields in 7 steps.

8 minute read
Decorative imagery showcasing Pontil's brand

By the end of this guide you'll have an error contract your agents can actually recover from. Not a status code and a stack trace — a structured payload the model can parse, a taxonomy that maps to specific recovery actions, and hints that tell the caller whether to retry, back off, fix the input, or stop. Prerequisites: a REST or RPC API you own, a tool-calling agent that hits it, and the ability to change response bodies. Time required: half a day to design, one to two days to roll out behind a version flag.

The short version of the problem: HTTP status codes were designed for browsers and human developers. They tell you something went wrong. They don't tell an agent what to do next. An agent that receives 400 Bad Request with a stack trace will retry the same call, hallucinate a fix, or give up. None of those are the right answer. Structured error responses give the model enough signal to self-correct — or to stop trying.

Step 1 — Adopt a structured error format

Pick one shape and use it everywhere. The obvious candidate is RFC 9457 Problem Details for HTTP APIs, which the IETF published in July 2023 to replace RFC 7807. RFC 9457 is backward-compatible with 7807, adds a shared registry of problem types, and tightens support for representing multiple problems in a single response. If you already ship 7807, you're 95% of the way there; new implementations should target 9457 directly. It's boring, it's stable, and it's already understood by most HTTP tooling.

The minimum useful shape:

{
 "type": "https://api.example.com/errors/rate-limited",
 "title": "Rate limit exceeded",
 "status": 429,
 "detail": "You have exceeded 100 requests per minute for user_id=u_123.",
 "instance": "/orders/o_456",
 "code": "rate_limited",
 "retryable": true,
 "retry_after_seconds": 12,
 "trace_id": "trc_9f2b1c"
}

The standard fields (type, title, status, detail, instance) cover human debugging. The extra fields (code, retryable, retry_after_seconds, trace_id) cover the agent. Both audiences read the same payload; neither one has to guess.

Return application/problem+json as the content type so clients can dispatch on it.

Step 2 — Define an error code taxonomy

code is the field the model will actually branch on. Keep it a stable, lowercase, snake_case string — never a free-text message, never a localised phrase, never the HTTP status number restated. A small, closed vocabulary the agent can learn beats a long list of ad-hoc strings.

Aim for one code per recovery action. If two errors need the same response from the agent, they're the same code.

Code
HTTP status
Agent should

`invalid_argument`

400

Fix the input and retry once

`unauthenticated`

401

Refresh the token, then retry

`permission_denied`

403

Stop — surface to the user

`not_found`

404

Stop — the resource doesn't exist

`conflict`

409

Re-read state, decide whether to retry

`rate_limited`

429

Back off for `retry_after_seconds`

`unavailable`

503

Retry with exponential backoff

`internal`

500

Retry once, then stop


Document the full taxonomy in your OpenAPI spec. Version it. Don't add codes silently — that's the same drift problem covered in OpenAPI spec drift, and agents will silently mis-handle unknown codes.

Step 3 — Attach explicit recovery hints

Structured errors are only half the job. The other half is telling the caller what to do. Two hints matter more than the rest:

  • retryable — a boolean. Not a status class, not a heuristic. If it's true, the agent may retry. If it's false, retrying is guaranteed to fail again and the agent should stop.
  • retry_after_seconds — an integer. Populate it on 429 and 503 responses. Honour it in the runtime so retries don't hammer the same limit.

Add remediation when the fix is deterministic:

{
 "code": "invalid_argument",
 "status": 400,
 "detail": "currency must be a three-letter ISO 4217 code.",
 "retryable": true,
 "remediation": {
   "field": "currency",
   "expected": "ISO 4217 alpha-3",
   "received": "US Dollars",
   "example": "USD"
 }
}

The agent doesn't have to infer what "invalid currency" means. The remediation block tells it exactly which field to fix and what shape the fix should take. This is where LLM agent error recovery actually happens — in the delta between "something failed" and "here's the specific thing to change."

Step 4 — Distinguish validation errors from business rule violations

Agents conflate these when APIs conflate them. They shouldn't.

  • Validation errors (invalid_argument) mean the request was malformed. The agent can fix the payload and retry.
  • Business rule violations (precondition_failed, conflict, quota_exceeded) mean the request was well-formed but the system state doesn't allow it. The agent probably can't fix this by re-crafting the payload — it needs to re-read state, ask the user, or stop.

Return them with different codes and different retryable values. A well-designed tool schema prevents most validation errors before they happen; business rule errors are the ones you have to handle at runtime.

Example of a business rule error:

{
 "code": "precondition_failed",
 "status": 409,
 "title": "Order already shipped",
 "detail": "Cannot cancel order o_456 — it shipped at 2026-05-04T09:12:00Z.",
 "retryable": false,
 "trace_id": "trc_2a71ff"
}

The agent shouldn't retry. It should surface the fact to the user.

Step 5 — Include the trace ID and keep messages agent-safe

Every error carries a trace_id. When something breaks in production, that's the join key between the agent's transcript, your logs, and your APM. Without it, debugging an agent that made forty tool calls is guesswork.

While you're at it, keep the detail field agent-safe:

  • No stack traces. Stack traces leak internals and confuse the model.
  • No PII. The agent's transcript may be logged in the orchestrator, the observability stack, and the model provider's storage.
  • No inconsistent phrasing. If the same failure produces "user not found" one day and "could not locate user" the next, agents that pattern-match on strings will break.

Write detail as a single, specific sentence that names the resource and the constraint. Save the internals for the trace.

Step 6 — Return partial results when a batch call partially fails

Agents batch. A POST /orders/bulk call with 50 orders shouldn't fail atomically when one order is invalid — the agent then has to figure out which one, retry the other 49, and reconstruct the response. That's the loop where things go wrong.

Return a 207 Multi-Status or a structured partial-success body:

{
 "succeeded": [
   {"index": 0, "id": "o_101"},
   {"index": 1, "id": "o_102"}
 ],
 "failed": [
   {
     "index": 2,
     "code": "invalid_argument",
     "detail": "currency must be a three-letter ISO 4217 code.",
     "remediation": {"field": "currency", "expected": "ISO 4217 alpha-3"}
   }
 ]
}

The agent now knows exactly which items failed, why, and how to fix them. It doesn't retry the whole batch. This composes cleanly with parallel tool calls — the same pattern applies to any fan-out where partial failure is likely.

Step 7 — Test the error contract, not just the happy path

Contract tests for errors are the thing most teams skip. Then a refactor changes the shape of an error payload, no human-driven test catches it, and every agent using the API silently loses its recovery signal.

At minimum, test:

  1. Every documented error code is producible by at least one test scenario.
  2. The shape of every error response matches the OpenAPI schema — including the extra fields (code, retryable, remediation).
  3. retryable: true errors are actually recoverable when the caller does the documented thing.
  4. retryable: false errors don't become recoverable in a future change without a version bump.

This is where a proper testing setup earns its keep — see the mock vs live third party APIs guide for how to combine both without the usual gaps.

Common pitfalls

  • Overloading 400 for everything. If validation, auth, and business rule violations all return 400, the agent can't tell them apart. Use the right status code, and lean on code for the fine-grained distinction.
  • Free-text error messages as the primary signal. The model will pattern-match on strings until you change one. Then it breaks. Make code the primary signal; detail is for humans.
  • retry_after without honouring it in the runtime. If the tool runtime ignores retry_after_seconds, agents will burn through their retry budget and hit the rate limit again. Wire the hint into the runtime, not just the response.
  • Silent additions to the taxonomy. New error codes are breaking changes for agents. Version them. Announce them. Don't ship a new code the same week someone deploys an agent that doesn't know it exists.
  • Localised error messages. If your API returns different detail strings by Accept-Language, agents that were tuned on English strings will fail in production for non-English users. Keep code locale-independent; localise title and detail only if you must.

Done properly, structured error responses are the difference between agents that recover and agents that spiral. The work is small. The payoff shows up the first time a rate limit hits at 2 a.m. and the agent handles it without a page.

Join our weekly newsletter

Stay up to date on the ever changing agentic landscape.

POSTS

Related content

Agent infrastructure

Agents in production

AI agent error handling: a practical guide to retries, circuit breakers, and recovery

8 minute read

Agent infrastructure

API strategy

API idempotency for AI agents: a practical guide to safe tool retries

7 minute read

API strategy

Agent infrastructure

REST API design best practices for the agent era

10 minute read