# 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.

Source: https://agentdrives.com/guides/api-errors-agents-can-act-on?via=md · Published 2026-10-27

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](https://www.rfc-editor.org/rfc/rfc9110.html)) |
| 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](https://www.rfc-editor.org/rfc/rfc9457)) |
| 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](https://datatracker.ietf.org/doc/draft-ietf-httpapi-ratelimit-headers/)) |

## 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](https://www.rfc-editor.org/rfc/rfc9457)). 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](https://www.rfc-editor.org/rfc/rfc9110.html)). 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](/guides/tell-ai-agents-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](https://www.rfc-editor.org/rfc/rfc9457) 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](https://www.rfc-editor.org/rfc/rfc6585.html) 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](https://www.rfc-editor.org/rfc/rfc9110.html)).

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](https://www.ietf.org/archive/id/draft-ietf-httpapi-ratelimit-headers-11.txt) 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 |

## Step 5: return a request id and a docs link

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?](/findings/api-errors), and the method is on [how we drive](/method).

## 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](/data/facts#api.probed).

## 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.
