When Correct APIs Break Agents: Six Design Choices That Pass Every Test
tl;dr — for human consumption
An API can satisfy its spec completely and still break the agents that use it, because the failure surface isn't execution — it's inference. Deeply nested responses, denormalized fields, drifted documentation, hidden call ordering, dead-end errors, misleading names, and near-duplicate endpoints all force the agent to infer what the interface could have stated. Every inference costs tokens, and every inference is a chance to confabulate. Audit for the six anti-patterns and move each from inferred to stated.
machine_summary: claim: "an API can pass every contract test and still break agents, because agents fail at the places where the interface makes them infer what it could have stated" takeaway: "audit for six spec-compliant anti-patterns — deep nesting and denormalized fields, drifted docs, hidden call dependencies, dead-end errors, misleading names, near-duplicate endpoints — and restate each in the shape, the docs, the contract, the error, or the name" evidence_type: argument cross_links: [ax-vs-ux, computational-kindness, first-contact-experiences, deterministic-ax-metrics]
The failure surface is inference, not execution
When an agent fails against your API, the instinct is to go looking for the bug. Usually there isn’t one. The endpoint does what the spec says, the test suite is green — and the agent still called the wrong tool, read the wrong field, or built ten steps of plan on a response it misread. That’s because “correct” is a claim about your spec, and the agent never meets your spec. It meets tokens, names, and implied promises. An API breaks agents wherever it makes them infer something it could have stated — and every one of the six design choices below forces exactly that inference, while passing every test you’d normally write. Each is a specific, named form of the guesswork tax; this piece is the catalogue of where the tax hides in an interface that is, by its own definition, working.
Deep nesting is a tax; denormalized fields are a trap
The agent reads every response as context, so every layer of wrapping is a fee charged per call, billed in the tokens the task costs. If the answer to “did it work?” lives at .run.result.latest.metadata.status, four levels of envelope get shipped, parsed, and paid for every time anyone asks. There’s a second cost that’s worse than the fee: all those envelopes look alike. When every response repeats the same boilerplate wrapper, the model is handed a strong repeating pattern, and a language model completes strong patterns — so the risk of reading the expected value instead of the actual one goes up precisely where the payload is most padded. Treat that as mechanism plus field observation rather than settled fact; it gets a full piece of its own soon.
The placement rule is almost embarrassingly simple: a field’s depth should match its importance. An operation’s status belongs at .status. And its dual: data belongs inside the object it describes. A person’s age goes at .persons[0].age; the moment it’s denormalized into .firstPersonAge, the agent must notice the convention, trust it, and reassemble your entity in its head — a join performed in inference, per session, forever.
// − what the agent gets
{ "run": { "result": { "latest": { "metadata": { "status": "failed" } } } },
"firstPersonAge": 41 }
// + what the agent needed
{ "status": "failed",
"persons": [{ "name": "…", "age": 41 }] }
Documentation that has drifted is worse than none
No documentation forces the agent to guess — bad, but at least the agent knows it’s guessing, and a well-run one hedges, probes, and verifies. Auto-generated documentation that has drifted from actual behavior removes even that: it reads as authoritative, the agent extends it full trust, and there is no signal that trust is misplaced until a call misbehaves — sometimes not even then, because when observation and documentation disagree, a model will often side with the document it just read. Auto-generation makes this failure mode cheaper to produce, not rarer: the pipeline that generated the docs once is precisely the pipeline nobody is watching now.
// − the generated docs (last touched three refactors ago):
// GET /v1/orders/{id} → { "state": "pending" | "shipped" | "delivered" }
// − what the endpoint returns today:
{ "state": "SHIPPED", "substate": "handed_to_carrier" }
Every consumer of that doc now writes if state == "shipped" — a branch that can never fire, in code that looks correct, justified by documentation that used to be. A confidently wrong document outperforms a missing one at destroying a session.
Secretly dependent calls, and the silent grade of the failure
Some endpoints only work after another call has run first — a session warmed, a resource activated, a cache populated — and nothing in the contract says so. There are two grades of this. With an explicit error, the dependency costs one wasted call and is instantly learned. Without one, the cold call returns a 200 with something empty or subtly stale, and the agent builds the rest of its plan on it:
// − cold call, hidden dependency, silent failure — HTTP 200:
{ "recommendations": [] } // indistinguishable from "there are none"
// + cold call, dependency stated — the failure is the documentation:
{ "error": "no_active_session",
"fix": "call POST /v1/sessions first, then retry this endpoint" }
A human developer discovers the ordering once and remembers; the agent is session-stateless and re-pays the discovery every time. Sequencing that lives only in your team’s folklore is, from the agent’s side, indistinguishable from nondeterminism.
Errors that end instead of redirect
A bare 400 or "something went wrong" gives the agent nothing to plan with, so it does the only things it can: retry blind variations — each one a paid guess — or invent a workaround and act on it. The craft fix is already written up: errors should be recovery instructions that name the failure, the fix, and the tool. The theory point worth adding here: a dead-end error is the same defect as a hidden dependency, surfacing at recovery time instead of planning time. In both cases the server knows the next legal move and declines to say it, converting information it holds into inference the agent must perform.
Names are contracts the agent executes literally
An endpoint with query in its name must not modify a resource. A get must be side-effect-free; a delete must delete, not archive. The agent arrives already fluent in these conventions — it has read more API surfaces than your whole team combined, and query-is-read-only is baked deep into its priors. That fluency is exactly what makes the lie expensive: your team absorbs the local folklore (“oh, queryOrders also refreshes the cache, everyone knows that”), but the agent has no access to your company’s folklore — only to the industry’s. So it executes the public meaning of your private exception, first session, possibly holding write access. A misleading name isn’t a cosmetic flaw; it’s an incorrect instruction you shipped inside your own interface, and the agent’s obedience to it will read, in the incident review, like the agent’s fault.
Almost-duplicate endpoints force a coin flip
When search, query, list, and find coexist with overlapping behavior, a human asks a teammate which one is real. The agent can’t; tool selection is a probability question, so it picks the likeliest-looking one — and picks differently across sessions, which makes your integration nondeterministic at the interface layer with every endpoint behaving correctly. Either consolidate, or make the boundary part of the interface — the description field is the teammate’s answer, stated where the agent can actually read it:
// − four descriptions, zero boundaries
"searchOrders": "Search orders",
"queryOrders": "Query the orders index",
"listOrders": "List orders"
// + the boundary is part of the contract
"listOrders": "Enumerate orders, newest first. For text matching use searchOrders.",
"searchOrders": "Full-text search over orders. For a known id use getOrder."
The takeaway: state what you’re making them infer
One thread runs through all six: the interface holds the information — where the value is, what the endpoint does, what must run first, what to do next, which tool is which — and makes the agent derive it instead. Inference is the most expensive and least reliable operation in the whole stack, and each of these patterns spends it on questions the API could answer in a string. The audit is six questions: Is the important field at the depth its importance deserves? Do the docs match the behavior, today? Does every endpoint fail loudly when called cold? Does every error name the next move? Does every name tell the truth? Between any two similar endpoints, is the boundary written down? Your test suite checks the implementation against the spec. Nothing checks the spec against its reader — and the reader has changed.
this page as your agent reads it → /posts/correct-apis-break-agents.md