Development

Choosing HTTP Status Codes for an Agent-Facing API

Antonio Evans10 min read

A flowchart with three branches: one labeled 'Retry' (5xx codes), one 'Change tool' (4xx codes like 400, 404, 422), and one 'Human intervention' (401, 403, 429 after retries).
Key points

Key points

  1. 1Use 5xx codes only for genuinely transient server-side failures that a retry can fix, and 4xx codes for problems the agent must solve by changing its request or plan.
  2. 2The status code is the branch signal an agent reads first, so an ambiguous or invented code turns into either a retry loop or a premature give-up.
  3. 3Every error should carry a machine-readable body with a stable problem type, since a prompt cannot parse a prose message reliably.
  4. 4Rate limiting and auth failures should route to a bounded retry and then a human, not into another agent attempt.

Why the status code matters more to an agent than to a human

A human developer reads the error body, checks the docs, and fixes the call. An agent does something different: it reads the status code and the body, weighs them against whatever branching rules its prompt or orchestration layer contains, and picks the next action. That next action is chosen from a small menu, usually "retry," "call a different tool," "rewrite the arguments," or "stop and ask." If your API returns a code that does not map cleanly onto that menu, the model guesses, and guessing is where loops come from.

The HTTP specification defines five classes of status codes by first digit: informational, success, redirection, client error, and server error (RFC 9110 as summarized by Olostep). The important split for an agent-facing API is between the fourth and fifth classes, because that is the boundary between "I did something wrong" and "the server did something wrong." A retry only helps in the second case.

The failure modes are easy to describe. When a 4xx code is returned for a temporary condition, the agent may treat its own request as broken, rewrite the payload over and over, and never recover. When a 5xx code is returned for a permanent condition, the agent retries a request that can never succeed until its step budget runs out. Practitioners building agents against real APIs have documented exactly this: a 422 with no field detail is invisible to the model, while a 422 carrying the missing field lets it self-correct (lessons from the field on agent HTTP errors).

The design goal is therefore simple to state: every status code your API can return should imply exactly one agent action.

The decision table

The table below is the core of this article. Read it as a contract: the "agent action" column is what a well-written prompt or orchestration framework should do when it sees the code.

StatusMeaningRetry safe?Agent actionHuman needed?
400 Bad RequestMalformed syntax or invalid JSONNoRewrite request once, then stopOnly if it repeats
401 UnauthorizedMissing or invalid credentialsNoFetch a fresh token, retry onceYes if still failing
403 ForbiddenAuthenticated but not permittedNoChoose a different tool or stopUsually yes
404 Not FoundResource does not existNoChange the identifier or planNo
408 Request TimeoutServer gave up waitingYesRetry with backoffNo
409 ConflictState conflict, duplicate, or lockSometimesRe-read state, then retryNo
422 Unprocessable ContentValid syntax, failed validationNoFix the fields named in the bodyNo
429 Too Many RequestsRate limit exceededYes, after delayWait for Retry-After, retryOnly after the cap
500 Internal Server ErrorUnhandled server exceptionYesRetry with backoffOnly after the cap
502 Bad GatewayBad response from upstreamYesRetry with backoffOnly after the cap
503 Service UnavailableDown or overloadedYesWait for Retry-After, retryOnly after the cap
504 Gateway TimeoutUpstream did not respondYesRetry with backoffOnly after the cap

The retryable and non-retryable split in this table follows a widely used convention: 408, 409, 429, 500, 502, 503, and 504 are treated as retryable, while 400, 401, 403, 404, and 422 are not (Temporal's HTTP retry recipe). You will notice 409 sitting on the fence. Treat it as retryable only when your API documents that the conflict is a temporary lock rather than a duplicate key, and have the agent re-read state before trying again.

A second reference point comes from payment and mailing APIs that publish their retry policy. One such provider tells clients to retry only 429 and 503, using exponential backoff with jitter and an idempotency key so that a retried create does not produce duplicates (PostGrid retry logic). Narrowing the retry set this aggressively is defensible for create operations, because each retry risks a double side effect.

The decision rule I recommend: retry only codes that a retry can fix, and only on requests you can safely repeat.

Writing the branch logic into the prompt

Most agent failures are prompt failures. The model does not inherently know that a 503 means "wait" and a 422 means "fix your arguments," so you have to tell it. The most robust pattern is to put the decision table into the system prompt in the same shape as the table above, plus a hard cap on attempts.

You call an external HTTP API. After every call, look at the status code.

If 200-299: continue with the result.
If 400: your request body was malformed. Fix it once and retry. If 400 happens
  again, stop and report the problem detail field.
If 401: call the refresh_token tool, then retry the request exactly once.
  If 401 happens again, stop and ask a human.
If 403: you do not have permission for this operation. Do not retry.
  Choose a different tool or stop and ask a human.
If 404: the resource does not exist. Check the identifier, then change your
  plan. Never retry 404 with the same identifier.
If 409: re-read the resource, then retry once.
If 422: read the "errors" array in the body. Only change the fields it names.
  Retry once. If it fails again, stop.
If 429 or 503: read the Retry-After header. Wait that many seconds, then retry.
  Do not retry more than 3 times in total.
If 500, 502, or 504: wait, then retry. Do not retry more than 3 times.
If any code is not listed above: stop and report it. Do not guess.

Never retry the same request more than 3 times without changing something.

Two things make this work in practice. First, the list of explicit codes gives the model an anchor for the common cases. Second, the fallback rule ("if any code is not listed, stop") prevents the model from inventing behavior for a code you did not anticipate. Without that rule, an agent that receives a 418 or a custom 4xx code will usually retry, because retrying is the safest available default from the model's point of view.

The retry cap matters as much as the mapping. Bounded retry is standard in production agents, where layered failures (model API, tool API, business logic) are handled differently and exhausted retries escalate rather than repeat (retry patterns for production agents). A prompt that says "retry up to 3 times" is a budget; one that says "retry until it works" is a loop with extra steps.

Making the body parseable: RFC 9457

The status code says which class of action to take. The response body says which specific action. Status codes alone "cannot always convey enough information about errors to be helpful," which is exactly why the IETF published a standard machine-readable error format (RFC 9457 problem details). The format defines five fields: type (a URI identifying the problem kind), title, status, detail, and instance.

The Swagger team's write-up gives the reason this matters for agent clients directly: the new wave of API consumers, AI bots and models, demand precision that ad hoc {"error": "..."} shapes cannot provide, and a standard format lets a client act on the problem rather than string-match on prose (Problem Details, doing API errors well). Here is a response shaped for an agent:

{
  "type": "https://api.example.com/errors/validation",
  "title": "Request validation failed",
  "status": 422,
  "detail": "Two fields are invalid.",
  "instance": "/v1/orders/8f2c",
  "errors": [
    { "detail": "must be a positive integer", "pointer": "#/quantity" },
    { "detail": "must be a valid email address", "pointer": "#/contact_email" }
  ]
}

The type URI is the most valuable field for an agent, because it is stable and short. A prompt can say "if type ends in /validation, fix the pointers" without depending on the title or detail wording, which humans may rewrite. The extension errors array with pointers shows the model exactly which fields to change, which is the difference between one corrective attempt and a rewrite spiral. Frameworks already render this format out of the box: Spring MVC returns ProblemDetail objects with application/problem+json, and the status property on that object sets the HTTP status (Spring's error response docs).

One caution. The standard was previously published as RFC 7807 and later revised as RFC 9457, which obsoletes it (RFC 7807). If your tooling emits or parses the older shape, it is backward compatible, but new APIs should target the current standard.

Codes that cause loops, and what to return instead

A handful of status codes are technically valid but actively harmful to an agent client. They cause loops because the model cannot map them to a known action.

CodeWhy it hurts an agentUse instead
200 with "error" in the bodyAgent sees success and never reads the bodyThe correct 4xx or 5xx
418 I'm a teapotNo documented semantic; model guessesThe real code
Custom 6xx or numeric codesOutside the model's training and your promptA standard code plus type URI
400 for validation failuresToo generic; agent cannot find the bad field422 with errors array
401 for permission problemsAgent loops on token refresh403 when credentials are valid but insufficient
500 for bad inputAgent retries forever a request that cannot work400 or 422
404 for a temporary outageAgent edits a correct identifier503
429 with no Retry-AfterAgent retries immediately, deepening the limit429 with the header

Two anti-patterns deserve special mention. Hiding errors inside a 200 response is called out as a serious mistake because it misleads consumers into believing a request succeeded (Zuplo on API error handling). And using a single generic code for every failure strips the response of the information the client needs to act. The rule quoted there is worth adopting verbatim: use the most specific status code that applies.

The 200-with-error pattern is especially dangerous in agent workflows, because the agent's branch logic usually triggers on the status code before it inspects the body. If the request "succeeded" from the transport's point of view, the malformed result gets passed downstream and the real error only surfaces three steps later, in a place where it is much harder to attribute.

Retries, idempotency, and who pages a human

Retries only make sense if repeating the request is safe. For reads, this is free. For creates, the agent must send an idempotency key so that a retry after a timeout does not create a second record. One provider documents exactly this contract: an Idempotency-Key header, typically a V4 UUID, guarantees that repeated requests within a defined window return the same response, including error responses, without duplicating the side effect (PostGrid on response codes and retry logic).

Timing matters as much as safety. Exponential backoff with jitter spreads retries so that a fleet of agents hitting a recovering service does not stampede it. The formula is a base delay times a multiplier raised to the attempt number, with randomization applied to the result (PostGrid on backoff and jitter). A worked example with a 30 second base, a factor of 2, and full jitter:

  • Attempt 1: 30 seconds calculated, random delay between 0 and 30 seconds.
  • Attempt 2: 60 seconds calculated, random delay between 0 and 60.
  • Attempt 3: 120 seconds calculated, random delay between 0 and 120.
  • Attempt 4: 300 seconds, capped per your policy.

Prefer the server's Retry-After header when it is present, since the server knows more about its recovery time than your backoff formula does. In the Temporal recipe, the Retry-After header is parsed in three forms: retry-after-ms in milliseconds, retry-after in seconds, and retry-after as an HTTP date, with the header overriding the default delay (Temporal's retry recipe).

When does a human get involved? Three conditions should trigger it, and the API should make all three easy to detect:

  1. Auth is broken after refresh. A 401 that survives one token refresh is an operator problem, not an agent problem.
  2. Permission is genuinely absent. A 403 should page a human or route to a different tool, never to a retry.
  3. The retry budget is exhausted. After the third 429 or 503, the agent should stop and report the problem instance URI so an operator can pull the log.

The last point is a design decision the API can support. If the body always carries the instance field with a URI for the specific occurrence, the human who gets paged has a direct handle on the failing request instead of a timestamp and a shrug (RFC 9457).

A checklist before you ship

Before you hand an API to an agent client, walk this list. Each item is something a reviewer can verify in a test.

  • Every error path returns a 4xx or 5xx code, never a 200 with an error field.
  • Validation failures return 422 with an errors array naming each field.
  • Missing or invalid credentials return 401, and insufficient permission returns 403.
  • Temporary unavailability returns 503, not 404 or 500.
  • 429 and 503 responses include Retry-After in a form the client can parse.
  • Create endpoints accept an Idempotency-Key header.
  • Error bodies use application/problem+json with a stable type URI per error kind.
  • No custom code or joke code is reachable by an agent.
  • Your prompt or orchestration code lists every reachable code and has a "stop and report" fallback for anything else.
  • The retry budget is capped and recorded per task.

Versioning belongs on this list too. Once an agent's prompt depends on your error type URIs and status semantics, changing them is a breaking change for that client, so document what you change and where you changed it (Speakeasy on versioning and evolution). If you serve many agent clients, a documented, versioned error contract is worth as much as the endpoint itself.

If you are building the agent side and want one endpoint that speaks to text, vision, and embedding models behind a single key, the natural place to start is Quanta Inference, where the same status-code discipline applies to every model call.

FAQ

FAQ

Should an agent ever retry a 400?

No, not with the same payload. A 400 means the request was malformed, so the agent should repair the payload once using the problem detail body, then stop if it happens again. Retrying an unchanged 400 produces an identical error every time.

What about a 404 on a resource the agent just created?

Treat it as a plan problem, not a transient one. The agent should verify the identifier it used and check whether the create call actually succeeded, then stop or report. Retrying a 404 with the same identifier only burns steps.

How many times should an agent retry a 503?

Three attempts is a reasonable default, with exponential backoff and jitter, honoring any Retry-After header the server sends. After the cap it should stop and report the problem instance URI so a human can investigate. Unlimited retries on a 503 are the most common cause of agent loops.

Do I need both a status code and a problem type URI?

Yes. The status code gives the agent its coarse action class (retry, fix, stop), and the type URI gives it the specific cause that a prompt can branch on. A 422 alone says "fix something"; a 422 with type ending in /validation and an errors array says exactly what to fix.

How do I keep the prompt in sync with the API?

Generate the list of status codes and type URIs from the same source that generates your OpenAPI spec, and include it in the system prompt as a static block. When the API adds a new error kind, regenerate the block and run your agent's test suite against the new responses. Treat an unlisted code as a bug in the prompt, not a reason to retry.

Sources

Sources

  1. What are HTTP Status Codes?
  2. Handling HTTP Errors in AI Agents: Lessons from the Field
  3. Retry policy from HTTP responses | AI Cookbook
  4. Response Codes and Retry Logic - Getting Started - ReadMe
  5. Building Retries in Agents
  6. RFC 9457: Problem Details for HTTP APIs
  7. Problem Details (RFC 9457): Doing API Errors Well - Swagger
  8. Error Responses :: Spring Framework
  9. RFC 7807: Problem Details for HTTP APIs
  10. Best Practices for API Error Handling - Zuplo
  11. Versioning Best Practices in REST API Design - Speakeasy
About the author

About the author

Antonio Evans
Antonio Evans

Antonio Evans is the founder of Quanta Intelligence and a fractional CTO who builds HIPAA-ready AI systems for health and life-science companies. Before that he was technical co-founder of FriendsEAT for twenty years, CEO of Socialdraft, and a data engineer at IBM. Antonio lives with his dog Santino in Lisbon, Portugal.

All articles