Designing API Specs for Agentic Clients

Most API specs were written for human developers 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, 400 times in a loop, with zero hesitation.

statusreviewedpublished2026-03-10last-review2026-11-06read10 minauthorEditorial Teampolicyeditorial

The Caller Is No Longer a Person

For decades, API docs were written for humans. A field called id was fine because any developer reading it would trace one curl example and figure out the format. Put the same field in front of an agent and it can come back in several hallucinated shapes within the first handful of calls: UUIDs, integers, email addresses the model inferred from the resource name. The caller is a statistical pattern matcher with no shame about guessing. That single change invalidates most of the affordances we rely on.

The safe assumption now is that every parameter will be read by something that will confidently invent a value if the description is thin. That assumption reshapes what belongs on the page.

Descriptions Carry the Whole Contract

The single highest-leverage change is writing one full sentence of semantic description for every field, parameter, and operation. customer_id: string is useless. customer_id: Stripe customer ID, format cus_XXXX, case-sensitive, 14 to 18 chars after the prefix; obtain via /customers.list, never fabricate is usable. The "never fabricate" part is not theater. Agents read those instructions and comply more often than you would expect, especially when the description names the upstream source.

The same principle applies to enums. Do not list ["active", "pending", "cancelled"] and stop. Describe what each state means, which transitions are legal, and which one the agent is most likely to need. The model will route to the right value if you say so 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. 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. Deciding what counts as a duplicate, and how long the key stays valid, is the part teams get wrong — the retry matrix for payment workflows settles those questions in the domain that punishes mistakes hardest. This is not an agent feature, it is 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 should accept ?dry_run=true. The response is identical in shape to the real call but with "dry_run": true and no side effect. Agents can be instructed, via system prompts and tool descriptions, to dry-run first on unfamiliar calls. This catches the "I think this deletes a row but let me find out" class of mistake before it becomes an incident.

The spec has to say this is free and safe. If the agent thinks dry-run costs a billable request or counts against a rate limit, it will skip it under pressure.

Two-Phase Confirmation For Irreversible Actions

Picture the failure this pattern exists for: an agent's planner misreads "archived" as "deletable" and hits DELETE /projects/{id} hundreds of times in a runaway loop, until the rate limiter stops it. Without a confirmation step, nothing stands between that misreading and permanent data loss. With one, no data is lost and the only trace is a burst of unused tokens in the logs.

The pattern: the first POST returns HTTP 202 with a confirmation_token, a summary field describing exactly what will be destroyed, and an expires_in_seconds value (30 seconds is a reasonable default). The agent must resubmit the same call 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 tokens on the first call of each pair and never reaches execution. That one pattern turns a near-disaster into a log entry.

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 a valid, unexpired token
  When the token matches the exact resource and agent identity
  Then execute the deletion and invalidate the token

Given a second request arrives with an expired or mismatched token
  When the server validates the token
  Then return 409 with a human-readable explanation and require a fresh confirmation

Capability Negotiation And Discoverability

Agents do not browse your documentation site. They load whatever the system prompt points them at and work from there. Expose a /_capabilities endpoint that returns the list of operations, the current rate limits for the calling identity, the cost per call in tokens or cents, and the feature flags in effect. An agent that checks this first has far less to guess, and makes fewer wrong calls as a result.

In parallel, the OpenAPI spec should use explicit operationId values written in verb_noun form (list_projects, archive_project, not projectsGet2). The operationId is what the agent sees in its tool list, so it is the real name of the endpoint as far as the model is concerned.

Cost And Rate Visibility In Every Response

Humans learn rate limits by getting throttled once. Agents need the number in the response. Every response should include X-RateLimit-Remaining, X-RateLimit-Reset, and for paid endpoints X-Cost-Units. The spec documents these as part of the contract, not as incidental headers. When the agent can see it is at 4 remaining calls with a 60-second reset, it backs off. When it cannot, it does not.

Error Messages That Suggest The Fix

Give every 4xx response body the same fixed shape: { error_code, message, suggested_fix, docs_url }. The suggested_fix is a sentence the agent can act on. "Invalid customer_id format" becomes "Invalid customer_id; expected format cus_XXXX, received [email protected]; call /customers.search with the email to resolve it." An agent can usually recover from an error like that in one turn instead of flailing through several, and it never leaks internal stack traces back to end users because there are none to leak.

The Category Field Is What Drives Retry

A suggested_fix sentence tells an agent what went wrong. It does not tell the agent whether to try again, and that is the decision that actually burns money and rate limit. HTTP status is no help here: 400 might be a malformed payload or a declined card, and 500 might clear on retry or fail identically on the next forty-seven attempts. So every error should also carry a category drawn from a closed set of eight machine-readable buckets. It is the field a client branches on when it does not recognize the specific error_code, and it is small enough that an agent given a one-page error reference can get the retry semantics right on codes it has never seen.

categoryWhat it meansWhat the agent should do
auth_requiredCredentials missing or expired.Refresh and retry once.
forbiddenValid credentials, action not allowed.Do not retry.
not_foundResource does not exist.Do not retry the same ID.
validationRequest is malformed.Do not retry without changing the payload.
rate_limitToo many calls.Retry after the hint.
conflictOptimistic lock or state mismatch.Fetch fresh state, then retry.
transientSomething upstream is briefly broken.Retry with backoff.
permanentWill never succeed as written.Surface to a human.

Two rules make this hold up. First, error_code is a stable, hierarchical string like payment.card_declined.insufficient_funds; once published it is frozen, so a client that does not know the leaf can still match the payment. prefix and route sensibly. Codes get added and deprecated, but never renamed. Second, the machine-actionable detail lives in a structured details object, never in prose: field_errors as an array of {field, code, message} entries so a generated client can raise a typed exception per field, a retry_after_seconds value duplicated from the Retry-After header because SDKs drop headers more often than bodies, and a trace_id that matches the server logs so a pasted error lands support on the exact request.

Observability For Agent Traffic

Tag every request with the agent identity (via a required X-Agent-ID header) and log the dry-run ratio, confirmation-token burn rate, and retry count per identity. Those are the three numbers to watch when a new integration goes live. The healthy pattern is a high dry-run ratio while the integration explores, a sharp drop once it reaches steady state, and almost no failures in the second phase of confirmation. Write the bands you expect into the spec. A number outside them usually points to a spec problem rather than an agent problem, and it tells you which description to rewrite.

Versioning And Deprecation For Clients You Cannot Email

A growing share of the integrations hitting a public API are written by someone who typed a prompt into Cursor and shipped whatever the model suggested. Those clients never join your Slack and never read your email. Worse, every code assistant is a cache of your docs frozen at some training cutoff, so when you ship a change the humans get a notice and the models get nothing — their users can keep generating code against a ghost version of the API for months. You cannot email a machine client. You can only arrange for the current truth to live where machines look: an X-API-Version response header, a machine-readable changelog at a URL that never moves, and RFC 8594 Deprecation and Sunset headers that carry a real removal date — 90 days out for a public API — plus a Link header pointing at a before/after migration guide. Middleware that sees a Sunset header can warn, alert, or block a deploy long before the endpoint disappears. Whether a given change even needs that treatment is a review question, and the API contract checklist is where we keep the criteria.

The changelog is JSON, not Markdown, because a docs crawler cannot prompt its users from prose:

{
  "version": "2026.03.10",
  "changes": [
    {
      "category": "breaking",
      "severity": "high",
      "endpoint": "POST /v1/invoices",
      "affected_fields": ["line_items[].tax_rate"],
      "summary": "tax_rate is now required; previously defaulted to 0",
      "migration": "https://docs.example.com/migrations/2026-03-tax-rate"
    }
  ]
}

The hard part is deciding what counts as breaking. The obvious cases — removing a field, renaming one, changing a type — are easy. The ones that burn you look additive on paper: a new enum value a strict client crashes on, a field that switches from null to "", or pure semantic drift where the shape never moves but status: "complete" quietly starts firing webhooks two seconds late. Every contract test still passes; every stale client is now subtly wrong. The defense is a CI gate: openapi-diff runs on every PR, and any breaking diff fails the build unless the description carries the literal BREAKING-CHANGE-APPROVED: plus a changelog entry. You can still ship the break when you must, but never by accident and never without a trail a machine can read.

If You Remember One Thing

Specs for agentic clients are not "specs plus examples." They have different defaults: idempotency required, dry-run free, destructive ops two-phase, every field in a full sentence, capabilities discoverable, costs visible, errors actionable, identity tagged. Once you accept that the API is now the prompt, every other decision gets simpler.

Contract Review Packet to Copy

Use this when an API will be called by agents, scripts, or generated clients that cannot infer intent. The packet makes retry, pagination, and failure semantics explicit.

API contract review packet: Designing API Specs for Agentic Clients

Decision to make:
- How API specs need to change for LLM-powered agentic clients: discoverability, idempotency by default, dry-run support, semantic descriptions, and the 'are you sure?' pattern for destructive ops.

Owner check:
- Product owner:
- Engineering owner:
- QA or operations reviewer:

Scope boundary:
- In scope:
- Out of scope:
- Assumption that still needs approval:

Acceptance evidence:
- Test or fixture:
- Log, metric, or screenshot:
- Manual review step:

Contract boundary: no release without compatibility classification, consumer impact, retry behavior, and rollback notes.

Reviewer prompt:
- What would still be ambiguous to someone who missed the planning meeting?
- What evidence would make this safe enough to ship?

Agent-safe endpoint block

When an API may be called by an agent, the spec needs more than request and response shape. It must tell the caller how to avoid destructive guessing.

POST /refunds:prepare
- dry_run: true by default
- returns estimated impact, required permissions, and confirmation_token
- never moves money

POST /refunds:commit
- requires confirmation_token from prepare
- requires idempotency_key
- rejects if token is older than 10 minutes
- returns audit_log_id and rollback_support_contact

Agent instruction:
- Do not call commit unless the user has explicitly approved the prepare result.

References

The standards this article leans on, for readers who want the primary sources:

Agent-safe API review gate

Agentic clients need explicit friction before destructive calls. This gate keeps the API from rewarding confident guessing.

API behaviorRequired guardEvidence
Creates or moves moneyprepare endpoint plus confirmation token.Contract test proves commit rejects missing token.
Deletes or disables datadry_run default and human-readable impact summary.Fixture shows impacted records before commit.
Retries after failureidempotency key and replay-safe error code.Replay test proves one side effect per key.