Design Agent-Readable API Errors That Actually Recover
Your API returns 400 Bad Request with {"error":"invalid input"}. A developer checks the docs, spots the missing field, and fixes it. An agent has no actionable detail, retries the same payload, then reports that the API is broken.
Error responses are an interface. They must tell an agent what failed, whether retrying helps, and what to change. Without that information, recoverable failures become failed automations.
This guide covers the API side of agent recovery. Pair it with agent error-recovery logic for retries, backoff, and circuit breakers. Use Apidog to define, mock, and test failure responses alongside successful ones.
The three questions every error must answer
An agent should be able to answer these questions without guessing:
Is this the caller's fault or the server's?
A4xxmeans repeating an unchanged request will fail again. A5xxmay succeed later.Should I retry, and when?
429is retryable after waiting.409may require re-reading state.422requires changing the payload.What exactly should I change?
“Validation failed” is not actionable. “customer.postal_codeis required whencountryisUS” is.
Answer all three consistently to prevent retry storms and unnecessary escalations.
Use a structured error format
Do not invent a new shape if you do not need one. RFC 9457 Problem Details for HTTP APIs provides a widely supported baseline:
{
"type": "https://api.example.com/errors/validation-failed",
"title": "Validation failed",
"status": 422,
"detail": "The field 'customer.postal_code' is required when 'country' is 'US'.",
"instance": "/v1/orders",
"errors": [
{
"field": "customer.postal_code",
"code": "required_conditional",
"message": "Required when country is US. Provide a 5-digit or 9-digit US postal code.",
"example": "94107"
}
],
"retryable": false,
"next_action": "Add customer.postal_code to the request body and send again."
}
The fields that matter most for agents are:
-
detail: Name the exact field and rule that failed for this request. -
errors: Return every validation problem at once, with field paths that map to the submitted payload. -
retryable: Make retry behavior explicit instead of requiring clients to infer it from status codes. -
next_action: Give one clear instruction the agent can follow.
Google’s API error design guide reaches the same conclusion: expose structured details instead of burying actionable information in prose.
Say when to retry
For transient failures, provide a concrete delay.
HTTP/1.1 429 Too Many Requests
Retry-After: 30
Content-Type: application/problem+json
{
"type": "https://api.example.com/errors/rate-limited",
"title": "Rate limit exceeded",
"status": 429,
"detail": "You have used 1000 of 1000 requests in the current minute window.",
"retryable": true,
"retry_after_seconds": 30,
"next_action": "Wait 30 seconds before sending this request again. Do not retry sooner."
}
The Retry-After header accepts seconds or an HTTP date. Seconds are usually easier for clients and agents to apply. Send the value in both the header and response body: conventional clients use the header, while models often rely on body content.
Apply the same pattern to 503 maintenance windows and 409 locked resources. If waiting is the right action, include the wait time.
For implementation details, see handling “rate limit exceeded” errors and implementing API rate limiting.
Never leak internals—and never return nothing
Avoid both extremes:
- Stack traces leak framework versions, file paths, query fragments, and sometimes secrets. They are a security problem and add noise agents cannot use. See testing APIs against untrusted input.
-
Empty errors such as a bodyless
500or{"error":true}provide no recovery path.
Return a stable public error with a correlation ID instead:
{
"type": "https://api.example.com/errors/internal",
"title": "Internal error",
"status": 500,
"detail": "The order could not be created due to an internal error. No order was created.",
"retryable": true,
"retry_after_seconds": 5,
"request_id": "req_01J8ZK3M2Q",
"next_action": "Retry once after 5 seconds. If it fails again, stop and report request_id req_01J8ZK3M2Q."
}
The key sentence is: “No order was created.” For failed writes, agents must know whether retrying could create a duplicate. If you cannot guarantee the outcome, support idempotency and document it. See idempotency keys for AI agents.
Make request_id resolvable in your logs using the practices in this API observability guide.
Put errors in your OpenAPI spec
If error responses are absent from OpenAPI, generated clients, mocks, and agent tools cannot reliably handle them.
responses:
'201':
description: Order created
content:
application/json:
schema: { $ref: '#/components/schemas/Order' }
'422':
description: >
Validation failed. Not retryable without changing the request body.
The errors array names each invalid field.
content:
application/problem+json:
schema: { $ref: '#/components/schemas/Problem' }
'429':
description: >
Rate limited. Retryable. Wait for retry_after_seconds before sending again.
content:
application/problem+json:
schema: { $ref: '#/components/schemas/Problem' }
These descriptions are operational instructions, not decoration. When generating tools from a specification, as described in turning an OpenAPI spec into agent tools, the model reads them to understand failure cases.
Test failures, not only successes
Error-path coverage often disappears because failures take effort to reproduce. Mocks remove that effort.
Define failure responses in Apidog, switch the mock between them, and test agents against repeatable 422, 429, and 500 scenarios without affecting production. For the broader practice, see running agents against mocks instead of production.
Test at least these cases:
- Several invalid fields: Return all validation errors together and verify the agent fixes them in one follow-up request.
-
Rate limit with a wait: Verify the agent waits at least
retry_after_seconds. - Server failure during a write: Verify retries do not silently create duplicates.
- Authentication failure: Verify the agent stops rather than retrying an invalid token. See least-privilege API keys for agents.
- Malformed error body: Return invalid JSON and verify graceful degradation; proxies eventually produce these failures.
Save these as CI scenarios. Serializer refactors commonly break error handling while happy-path tests continue to pass.
Why better errors matter
Clear errors reduce cost in three measurable ways:
-
Fewer retries:
{"error":"invalid input"}often triggers two or three duplicate attempts. A named missing field usually produces one corrected request. - Fewer escalations: An agent that knows the correction can complete the workflow instead of handing it to a person.
-
Faster debugging: Precise
detailplusrequest_idturns log hunting into a single lookup.
They also help human developers. Specific error messages improve every integration, not only agent-driven ones.
Design the escalation path
Not every failure is recoverable. Missing scopes, closed accounts, and decisions requiring human approval should tell the caller:
- what happened,
- what a person must do next, and
- which correlation ID to provide.
That message must surface where someone will read it. For example, Sharkly keeps an agent’s result and execution trace on its Task and routes items needing replies or review into an Inbox. A useful error message makes that handoff actionable; “invalid input” does not.
Do not make agents parse prose
Avoid inconsistent response wording such as:
{ "message": "Sorry, that didn't work. Please check your details and try again." }
An agent can only guess what to do next. The problem gets worse when APIs return this body with a 200 OK, making the failure invisible to retry policies, dashboards, and alerts.
Use two rules:
- Give each distinct failure a stable machine-readable code, such as
insufficient_funds. - Never return an error with a success status code.
Checklist: agent-readable errors
- Use one consistent structured format across the API.
- Make
detailname a specific field or condition. - Return all validation failures at once with field paths.
- Include
retryableon every error. - Include retry delays in seconds in both headers and bodies.
- State whether failed writes created or changed anything.
- Include a correlation ID that resolves in logs.
- Never expose stack traces, framework strings, SQL, or internal identifiers.
- Document error responses with agent-readable OpenAPI descriptions.
- Mock each failure mode and run saved tests in CI.
Errors are an interface. Design them for the caller you have—which is increasingly a model that will follow exactly what your response tells it to do. Download Apidog to define error shapes and mock them before agents encounter them in production.
Frequently asked questions
Should I use RFC 9457 or my own error format?
Use RFC 9457 unless you already have a consistent production format. Consistency matters more than switching only part of an API to a standard. Add extensions such as retryable and next_action to either format.
Is next_action safe in an API response?
Yes, if the service generates it from fixed templates. Never echo user-provided text into next_action: agents interpret it as instruction, which creates a prompt-injection path. See testing APIs against untrusted input.
Should validation errors be 400 or 422?
Use 400 for malformed requests, such as invalid JSON. Use 422 when the request parses but fails validation or business rules. If your API already uses one status for both, document that behavior rather than changing it inconsistently.
How much detail is too much?
Include the field, failed rule, and an example value when useful. Exclude internal identifiers, query text, stack frames, and implementation details.
Do error messages count against the context window?
Yes. Repeated verbose errors add up across retries. Keep errors below a few hundred tokens. The same principle applies in trimming API responses for agents.
How do I stop retries for non-retryable errors?
Set "retryable": false, state that clearly in next_action, and enforce it in the tool wrapper. Do not rely on model judgment alone.


Top comments (0)