Most API specs were written for human developers — people who read docs carefully, infer intent from context, and pause when something feels dangerous.
Agentic clients do none of that. If your spec assumes a thoughtful human reader, an LLM-driven agent will find the ambiguity and run straight at it. Four hundred times. In a loop. With zero hesitation.
I learned this the hard way last quarter, when an agent integration hit our DELETE /projects/{id} endpoint 400 times in a runaway loop. Here's what I changed in the spec — not the code, the spec — to make the API survivable for machine callers.
The caller is no longer a person
I spent a decade writing API docs for humans. A field called id was fine, because any developer would trace one curl example and figure out the format.
Put the same field in front of an agent and you get three different hallucinated shapes in the first ten calls: UUIDs, integers, email addresses the model inferred from the resource name. The caller is a statistical pattern matcher with no shame about guessing.
So I now assume every parameter will be read by something that will confidently invent a value if the description is thin. That assumption reshapes what goes on the page.
Descriptions carry the whole contract
The biggest single improvement I shipped: one full sentence of semantic description for every field, parameter, and operation.
Useless:
customer_id: string
Usable:
customer_id: >
Stripe customer ID, format cus_XXXX, case-sensitive,
14-18 chars after the prefix; obtain via /customers.list,
never fabricate.
The "never fabricate" part is not theater. Agents read those instructions and comply more often than you'd expect — especially when the description names the upstream source of the value.
Same principle for enums: don't list ["active", "pending", "cancelled"] and stop. Describe what each state means and which transitions are legal. The model routes to the right value if you say it out loud.
Idempotency is not optional anymore
Human clients retry once, maybe twice, then escalate to a person. Agents retry aggressively — often on ambiguous responses they should have treated as success.
I now require an Idempotency-Key header on every non-GET endpoint and reject requests without one. The server stores the first response for 24 hours and replays it on duplicates.
This is not an agent feature. It's self-defense. Without it, a single misread 502 becomes three duplicate charges and a refund ticket.
Dry-run by default on anything destructive
Every destructive operation accepts ?dry_run=true. The response is identical in shape to the real call, but with "dry_run": true and no side effect.
One crucial detail: the spec has to say dry-run is free and safe. If the agent thinks a dry run costs a billable request or counts against a rate limit, it will skip it under pressure.
Two-phase confirmation for irreversible actions
This is the pattern that turned our 400-call runaway loop into a log entry instead of a data-loss incident:
- The first
DELETEreturns HTTP 202 with aconfirmation_token, asummaryof exactly what will be destroyed, andexpires_in_seconds: 30. - The agent must resubmit with the token to actually execute.
- The token is single-use and scoped to the exact resource.
An agent in a tight loop burns its token on the first call of each pair and never reaches execution.
Given an agent calling DELETE /projects/prj_123
When the first request arrives without a confirmation_token
Then return 202 with { confirmation_token, summary, expires_in_seconds: 30 }
And record no destructive side effect
Given a second request arrives with an expired or mismatched token
Then return 409 with a human-readable explanation
And require a fresh confirmation
Error responses that drive the retry decision
I rewrote every 4xx/5xx body to a fixed shape:
{
"error_code": "payment.card_declined.insufficient_funds",
"category": "permanent",
"message": "...",
"suggested_fix": "Call /customers.search with the email to resolve the ID.",
"docs_url": "..."
}
The suggested_fix tells the agent what went wrong. But category is the field that actually saves money — it's what a client branches on when it doesn't recognize the specific code. HTTP status is no help: a 400 might be a malformed payload or a declined card; a 500 might clear on retry or fail identically for the next forty-seven attempts.
Eight closed categories cover everything:
| category | What the agent should do |
|---|---|
auth_required |
Refresh credentials, retry once |
forbidden |
Do not retry |
not_found |
Do not retry the same ID |
validation |
Do not retry without changing the payload |
rate_limit |
Retry after the hint |
conflict |
Fetch fresh state, then retry |
transient |
Retry with backoff |
permanent |
Surface to a human |
Two rules make this hold up: error_code strings are frozen once published (deprecate, never rename), and machine-actionable detail lives in a structured details object — never in prose.
Make capabilities discoverable
Agents don't browse your documentation site. They load whatever the system prompt points them at.
- I expose a
/_capabilitiesendpoint returning available operations, current rate limits for the calling identity, and cost per call. Agents that check it first make dramatically fewer wrong calls. -
operationIdvalues are verb_noun (list_projects,archive_project) — that's the name the model actually sees in its tool list. - Every response carries
X-RateLimit-RemainingandX-RateLimit-Resetas documented contract, not incidental headers. When the agent can see it's at 4 remaining calls, it backs off. When it can't, it doesn't.
Watch three numbers per agent identity
Every request carries a required X-Agent-ID header, and I log per identity: dry-run ratio, confirmation-token burn rate, and retry count.
A healthy agent integration runs 30–60% dry-run during exploration, drops under 10% in steady state, and almost never fails the second phase of confirmation. Anything outside that envelope gets a closer look before it becomes an incident.
The uncomfortable summary
None of this is exotic engineering. Idempotency keys, dry runs, structured errors — we've known about them for years. What changed is that the forgiving, context-aware human reader who papered over our spec gaps is gone.
The spec is now the entire interface. Write it like the reader will do exactly what it says — because this reader will.
I write about spec-first delivery, API contracts, and AI-assisted engineering at spec-coding.dev. The full version of this guide (with versioning and deprecation patterns for agentic clients) is at spec-coding.dev/blog/designing-api-specs-for-agentic-clients.
Top comments (0)