Write API errors an AI agent can act on

How to shape API errors an AI agent can act on: status codes, Problem Details, Retry-After and request ids, with a baseline from live product APIs.

The numbers

From our drives. Each one links to its definition and evidence. A product can count in more than one row.

By AgentDrivesPublished 27 October 20267 min readTopic MCP and interfacesFor agents Markdown

8 of 35 products whose API answered a bad request with a web page instead of an API error, of those that answered our bad requests

On 25 October 2026, we sent four fixed bad requests to 40 product APIs, and 35 answered. 8 of 35 answered a bad request with a web page instead of an API error, 0 of 35 answered as Problem Details (RFC 9457), and 5 of 35 sent any rate-limit header.

What live APIs sent back to four bad requests

33 of 35 APIs named what went wrong in at least one error, but only 11 of 35 did so on every error. 21 of 35 answered every bad request in JSON, while 8 of 35 sent a web page for at least one, which an agent expecting JSON can’t parse.

The baseline, signal by signal

How the APIs answered four bad requests, and the standard that fixes each gap.

Signal Live APIs What to send
Success for a bad route or body 0 of 35 400 for malformed syntax, 404 for a missing route (RFC 9110[1])
Answered every bad request in JSON 21 of 35 JSON on every error, including the gateway and the default 404 handler
Sent a web page for at least one 8 of 35 The same JSON body as every other error
Named what was wrong in at least one error 33 of 35 A detail member for this occurrence (RFC 9457[2])
Named what was wrong in every error 11 of 35 The same detail on every error
Answered as Problem Details (RFC 9457) 0 of 35 Problem Details with type, status, title, detail and instance
Gave a request id with an error 16 of 35 An instance URI for this occurrence
Linked to docs in an error 7 of 35 A type URI that is a docs page
Any rate-limit header 5 of 35 A wait time in seconds (step 4)
Sent the IETF RateLimit header fields 1 of 35 The IETF RateLimit fields, as in step 4 (IETF draft[3])

Step 1: answer every error in JSON, never a web page

Send every error from the API host as JSON with the Content-Type application/problem+json (RFC 9457[2]). That includes errors raised before your code runs, so test your gateway, CDN and default 404 handler too. Keep stack dumps and implementation details out of the body: RFC 9457 encourages servers to avoid exposing them.

Bad, what the agent sees:

HTTP/1.1 404 Not Found
Content-Type: text/html

<html><body><h1>Page not found</h1>...</body></html>

Good:

HTTP/1.1 404 Not Found
Content-Type: application/problem+json

{
 "type": "https://docs.example.com/errors/unknown-route",
 "status": 404,
 "title": "Route not found",
 "detail": "The route /v1/widgetz does not exist. See the type URI for the list of routes.",
 "instance": "/errors/req_7f3a"
}

Step 2: pick the status code for each bad request

Return 401 for a missing key and for a made-up key, 404 for a route that doesn’t exist, and 400 for a body that won’t parse (RFC 9110[1]). Each code maps to one fix the agent can try.

  • No key: 401, which means the request lacks valid authentication credentials. Fix for the agent: get a key.
  • Made-up key: 401 as well, since a 401 on a request with credentials means they were refused. Fix: a new key.
  • Every 401 must carry a WWW-Authenticate header with at least one challenge for the resource.
  • Valid key, no permission: 403, which says the credentials are insufficient. The client should not retry with the same credentials. A made-up key is not this case.
  • Valid key, feature on a higher plan: 402, or 403 where clients depend on it, with the required plan and an upgrade link in the body. The steps: Tell an AI agent a feature needs a paid plan.
  • Route that can’t exist: 404. A 404 doesn’t say whether the absence is temporary or permanent, so the body has to. Fix: a different route.
  • Body that won’t parse: 400. Fix: repair the body.

Step 3: shape the body as Problem Details (RFC 9457)

Use the five members RFC 9457[2] defines:

  • type: a URI reference that identifies the problem type
  • status: the HTTP status code, as a JSON number
  • title: a short, human-readable summary of the problem type
  • detail: an explanation of this specific occurrence
  • instance: a URI reference for this specific occurrence

The default type about:blank means the problem carries nothing beyond its status code, so define your own type URIs. Put the field name and a valid value in detail.

Bad:

HTTP/1.1 400 Bad Request
Content-Type: application/json

{"error": "Bad request"}

Good:

HTTP/1.1 400 Bad Request
Content-Type: application/problem+json

{
 "type": "https://docs.example.com/errors/invalid-json",
 "status": 400,
 "title": "Request body is not valid JSON",
 "detail": "Could not parse the body: field 'email' is missing a closing quote.",
 "instance": "/errors/req_9c21"
}

Step 4: say how long to wait, in a header

On a 429, send Retry-After in whole seconds. RFC 6585[4] defines 429 Too Many Requests and says the response may include Retry-After to say how long to wait. Its value is either an HTTP-date or a non-negative whole number of seconds, as in Retry-After: 120 (RFC 9110[1]).

Add the IETF RateLimit fields if you want agents to pace themselves before they hit the limit. They are still an active Internet-Draft, not an RFC: version 11[5] is dated 23 May 2026 and expires 24 November 2026.

RateLimit-Policy:

RateLimit-Policy: "burst";q=100;w=60,"daily";q=1000;w=86400

RateLimit:

RateLimit: "default";r=50;t=30

In RateLimit-Policy, q (the quota) is required and w (the window) is optional. In RateLimit, r (the quota still available) is required. The draft uses 429 only as an example; the fields are not tied to that status code.

When you send both, Retry-After must take precedence, and its value should not point earlier than the end of the RateLimit window.

You want the agent to Send
Wait after being throttled Retry-After in seconds
Pace itself before the limit RateLimit-Policy and RateLimit
Both Both; the wait time in seconds wins

Put a request id in a response header and the same value in the body’s instance member, which identifies the specific occurrence. An agent can then hand a failure to a person or to support as one string.

Make the type URI a real docs page for that problem, since type is a URI reference for the problem type. RFC 9457 gives guidance for type URIs that can’t be dereferenced. For agents, avoid that case: one link that opens the right docs page doubles as the error kind.

Bad: "type": "about:blank" with no request id.

Good and bad answers to four bad requests

A bad answer leaves the agent guessing or retrying; a good one gives the right code, a problem+json body that names the fix and a request id. These pairs are written from the standards, not copied from any product.

Bad request Bad Good
No key 401 with no WWW-Authenticate and an empty body 401, WWW-Authenticate, detail naming the missing auth header
Made-up key 403, which tells the agent its key is valid but lacks access 401 with a type for a refused key, so the agent knows to get a new one
Route that can’t exist An HTML 404 page 404, problem+json, detail saying the route doesn’t exist, type linking the docs
Broken body A bare 400 with an empty body 400, detail naming the field or parse position

Checklist: test your own API with four bad requests

What it takes: four curl requests and your own API key. Replace the host and route with yours.

# 1. No key
curl -i https://api.example.com/v1/widgets

# 2. Made-up key
curl -i -H "Authorization: Bearer not-a-real-key" https://api.example.com/v1/widgets

# 3. Route that can't exist
curl -i -H "Authorization: Bearer $YOUR_KEY" https://api.example.com/v1/no-such-route

# 4. Body that won't parse
curl -i -X POST -H "Authorization: Bearer $YOUR_KEY" -H "Content-Type: application/json" \
 -d '{"email": "a@b' https://api.example.com/v1/widgets

Check each answer against these points:

  1. 401 for no key and for a made-up key, 404 for a route that can’t exist, 400 for a body that won’t parse.
  2. The body is JSON, not HTML.
  3. The Content-Type is application/problem+json.
  4. The body has type, status, title, detail and instance.
  5. detail names the header, field or route at fault.
  6. Every 401 carries WWW-Authenticate.
  7. A request id appears in a header and in instance.
  8. Throttled responses carry Retry-After in seconds.

In our test, 21 of 35 product APIs answered every bad request in JSON, 16 of 35 gave a request id and 5 of 35 sent a rate-limit header.

How we measured

On 25 October 2026 we sent four fixed bad requests to 40 product APIs and recorded the answers of the 35 that responded; all counts are out of those. The full findings are in API errors: do they tell an AI agent what to do next?, and the method is on how we drive.

What to change first

Start with the format: 0 of 35 answered as Problem Details and 8 of 35 sent a web page for at least one bad request. Make every error JSON with application/problem+json and the five members. Then point each type URI at its docs page and put a request id in instance. On throttled responses, send Retry-After in whole seconds. Run the checklist again after each change and compare your answers with the per-signal counts on our facts page.

See what an agent finds in your docs.

Free: what an agent finds, any wall in its way and how many things to fix. The Audit ($499) gives you every fix.

Pages this guide cites.

As linked when it was written. ↩ goes back to where the text cites it.

  1. 1rfc-editor.org · RFC 9110rfc/rfc9110.html↩
  2. 2rfc-editor.org · RFC 9457rfc/rfc9457↩
  3. 3datatracker.ietf.org · IETF draftdoc/draft-ietf-httpapi-ratelimit-headers↩
  4. 4rfc-editor.org · RFC 6585rfc/rfc6585.html↩
  5. 5ietf.org · version 11archive/id/draft-ietf-httpapi-ratelimit-headers-11.txt↩

Asked about this guide.

3 questions
Should I return 400 or 422 for a bad request body?

422 if the body parses but its instructions can't be processed: RFC 9110's own example is well-formed XML with semantically erroneous instructions. 400 if it doesn't parse.

Which rate-limit header should I send?

Retry-After, defined in RFC 9110. The IETF RateLimit fields are still an Internet-Draft; add them when you want agents to pace themselves before they hit the limit.

Should retry timing go in the header or the body?

The wait time goes in the Retry-After header. The body explains the condition, as RFC 6585 says a 429 response should.

Cite this

AgentDrives. “Write API errors an AI agent can act on.” 27 October 2026. agentdrives.com/guides/api-errors-agents-can-act-on

Drive data is CC BY 4.0. The text as Markdown. Every fact with its evidence.

More from the drives.