Why This Checklist Exists
Every API has two kinds of consumer. One is a person, who reads your documentation, infers what you meant, tries something, gets confused, and eventually asks. The other is an autonomous agent, which receives your specification flattened into a set of tool definitions and then acts on it. The difference that matters is not intelligence. Only one of them can ask a follow-up question.
That makes an agent the strictest consumer your API will ever have, and a useful one to design for. A human reads endpoint one, meets an inconsistency at endpoint fifty, and adapts. An agent reads endpoint one and generalises, so an inconsistency is no longer an aesthetic complaint. It produces a confidently wrong call. A description that is vague becomes a wrong parameter. An error returned as 200 OK becomes a success. A retry with no idempotency guarantee becomes a duplicate charge.
The useful consequence is that almost nothing here is new. These are the same practices that make an API pleasant for people; agents simply remove the tolerance that let you skip them. Work through the checklist against an API you already have. Each row states what to check, why it matters to a caller that cannot ask, and where the full treatment lives.
1. The Specification Is the Interface
An agent never sees your controllers, your documentation site, or your README. It sees a machine-readable specification, converted into tool definitions. Whatever is missing from that document does not exist.
| Check | Why it matters |
|---|---|
| A specification exists and is generated from, or verified against, the running API | A specification that drifts is worse than none. It is confidently wrong, and it is the only thing the caller has |
Every operation has an operationId that is unique, stable and under 64 characters |
The operationId typically becomes the tool’s function name. OpenAI caps names at 64 characters and Anthropic at 128, so 64 is the safe intersection; longer names are rejected outright rather than truncated |
| Every operation has a description that states what it does and when to use it | The description is the entire prompt the caller reasons over. “Gets a user” does not distinguish this operation from the three beside it |
| Every parameter and response property is typed and described | An untyped data object is an instruction to guess |
| Request and response examples are complete payloads, not fragments | A caller assembling a body from leaf-level examples will omit whatever had none |
| Schema nesting stays shallow, five levels being a practical ceiling | Deeply nested schemas exceed provider limits and are assembled incorrectly long before that |
A quick way to see what you are actually shipping: take one operation, generate the tool definition from it, and read that definition without the specification beside you. If you cannot tell what the operation does or how to call it, neither can the caller.
How an API becomes tool definitions walks through that conversion in full, including which parts of the contract do not survive it.
See REST principles and OpenAPI.
2. Resources Are Use-Cases, Not Tables
The fastest way to make an API hard to call is to expose the schema. Table-shaped endpoints push the domain onto the caller: it has to know which three writes constitute “refund an order”, in what sequence, and what to do when the second fails.
| Check | Why it matters |
|---|---|
| Resources are named for things the product does, not tables it stores | A caller reasons about the domain it was given. Give it the wrong one and it reasons well about the wrong thing |
| One canonical path per resource, with aliases removed or documented as aliases | Two paths with identical descriptions force an arbitrary choice, and half of those choices will be wrong |
| Nesting stops at one level | /orders/{id}/refunds is navigable; three levels deep is a guess about hierarchy |
| Identifiers are opaque and stable | An identifier a caller can construct is one it will construct incorrectly |
| Naming, casing and pluralisation are uniform across every endpoint | This is the whole of it: consistency is what lets a caller generalise from the first endpoint to the fiftieth. It is also why a slightly imperfect convention applied everywhere beats a perfect one applied in places |
| A multi-step operation is exposed as one operation where it is really one intention | Otherwise the caller orchestrates a transaction it cannot roll back |
See resource naming.
3. Errors Are Part of the Contract
An error is not the absence of a response. It is a documented outcome, and for a caller with no human to escalate to, it is the only signal available to choose what to do next.
| Check | Why it matters |
|---|---|
Errors use application/problem+json per RFC 9457, across the whole API |
One shape, learned once. A caller that can parse one error can parse all of them |
| Framework and validation errors go through the same shape | The default error page is the one a caller meets first and the one nobody styles |
No error is ever returned with a 200 |
A 200 carrying "error": true is not an error to a caller that branches on status. It is a success with strange contents |
Callers branch on the stable type URI, never on the human-readable prose |
Prose is for people and gets rewritten. The type is the machine-readable part and must not change |
Every type URI resolves to a page that documents that error |
This is the only part of your contract that ships with a URL inside it, and therefore the only part a confused caller can look up unaided |
| The response says what to do next, not only what went wrong | “Retry after 30 seconds” and “this will never succeed” are different instructions |
| Internals never leak: no stack traces, no SQL, no internal hostnames | A verified caller is still not a trusted one |
The resolvable type is the item most often skipped, and the cheapest to fix. Either point at a published catalogue, where every error in the error reference has a stable URI such as idempotency key conflict or validation failed, or host your own under a path like /problems/{type} and keep a page per type. What matters is that the URI resolves to something that explains the error, rather than being a namespaced string that 404s.
See error handling and input validation.
4. Writes Survive Being Retried
A caller that does not get a response does not know whether the write happened. It will try again, and an agent will do so immediately, without the pause a human takes to check.
| Check | Why it matters |
|---|---|
Unsafe writes accept an Idempotency-Key |
Without one, “retry” and “duplicate” are the same request |
| The stored key is fingerprinted against the request body | Otherwise the same key with different contents silently returns the first result, which is a harder bug than a duplicate |
| A reused key with a different body returns a conflict, not a success | See idempotency key conflict |
| The retention window for keys is documented | A caller cannot reason about a window it does not know |
GET, PUT and DELETE are genuinely safe or idempotent as HTTP specifies |
Free interoperability, and callers assume it whether or not you honoured it |
| Conditional requests are available for read-modify-write | ETag and If-Match beat inventing an optimistic-concurrency scheme |
One caveat worth stating, because the rule is often applied mechanically: an endpoint whose POST is a pure function (validate this document, convert this payload, look up this identifier) has nothing to make idempotent, because calling it twice already produces the same result and changes no state. Adding a key there is ceremony. The check is not “does every write have a key” but “do you know which of your writes have effects”.
See idempotency and conditional requests.
5. Collections Do Not Trap the Caller
| Check | Why it matters |
|---|---|
| Large or feed-like collections use cursor pagination | Offsets skip and repeat rows under concurrent writes, and a caller paging an unstable list cannot tell |
| The response states whether more pages exist, unambiguously | An empty final page is a cheaper contract than inferring completion from a short one |
| Filterable and sortable fields are allow-listed and documented | An unlisted filter is a guess, and a guess that silently returns everything is a bad one |
| Default and maximum page sizes are documented and enforced | A caller asked to “fetch all” will ask for all |
| An invalid cursor returns a documented error | See invalid pagination cursor |
See pagination and filtering, sorting and searching.
6. Change Is Announced in the Response
A caller integrated six months ago and is not reading your changelog. The only channel guaranteed to reach it is the response it is already parsing.
| Check | Why it matters |
|---|---|
| Most change is additive, so most change needs no new version | Tolerant readers and additive responses are what let an API evolve without a v2 |
Deprecated operations send a Deprecation header, RFC 9745 |
Two separate RFCs, frequently conflated |
Retiring operations send a Sunset header with a real date, RFC 8594 |
“Soon” is not a date, and a caller cannot schedule against it |
A Link relation points at the migration guide |
The header says something is ending; the link says what to do |
| Usage is instrumented per version | You cannot retire what you cannot see, and “who is still calling this” is the only question that matters at the end |
| Nothing is removed without having been announced in a response first | A surprise 404 is indistinguishable from an outage |
See deprecation and sunsetting and versioning.
7. The Webhooks You Send Are an API Too
Everything above applies to the events you push, and this is the half most teams never review. A webhook is an API you published without writing down.
| Check | Why it matters |
|---|---|
| The payload is signed, and the signature covers the raw body | Signing a re-serialized payload is the most common implementation bug in the whole subject |
| The signature scheme is documented well enough to implement without asking | Receivers get this wrong in ways that fail intermittently |
| Every delivery carries a unique, stable event identifier | It is the key a receiver dedupes on, and dedupe is the only defence that neutralises retries, replays and at-least-once delivery at once |
Every payload carries an explicit event type |
Two payloads that are otherwise just fields over the same object must not be interchangeable, because a caller must never be able to present one as the other |
| A change to a payload is a new event type, not a quiet mutation | Receivers parse these; a silently added meaning is a silently broken receiver |
| The retry schedule is published: how many attempts, what backoff, when you give up | Without it a receiver cannot distinguish “still coming” from “lost”, and cannot size its own replay window |
| The tolerance window on a signed timestamp is wider than your last retry | Otherwise your own final retry fails verification |
| Delivery is dispatched after the transaction commits | Otherwise the notification can arrive before the row it describes is readable, and the receiver fetches a resource that does not exist yet. The outbox pattern exists for this |
| A replay or re-delivery mechanism exists | Receivers have outages, and the alternative is a support ticket |
Consider whether a shared secret is the right root of trust. HMAC is the common case and is fine, but it gives every receiver a key capable of forging your signatures, and the signature stops meaning anything once the body is copied out of the request. Asymmetric signing, where you hold a private key and publish a public one, keeps verifying after a receiver has stored the payload, which is exactly what a receiver handling anything auditable should do with it.
See webhooks and webhook signature verification.
8. The Webhooks You Receive Assume Nothing
The same rules, inverted. A webhook endpoint is a public URL that accepts unsolicited requests from the internet.
| Check | Why it matters |
|---|---|
| Verify before anything else touches the database | Verification is a gate, not a step. Anything before it runs on unauthenticated input |
| The signature is computed over raw bytes captured before body parsing | Re-serializing changes key order, whitespace and escaping |
| Comparison is constant-time | Ordinary string equality leaks how many leading bytes were correct |
| The timestamp is checked against a tolerance window | A valid captured request replays perfectly otherwise |
| The raw payload is stored before it is processed | If processing fails you still have the evidence, and you can reprocess without asking for a replay |
| Processing happens asynchronously; the endpoint acknowledges quickly | A slow receiver looks like a failed one and earns retries you did not need |
| The handler is idempotent, keyed on the sender’s event identifier | Assume at-least-once delivery, because that is what you are being given |
| Out-of-order arrival is handled | Retries reorder events by construction |
| Failures land somewhere a person will see them | An endpoint that quietly returns 200 and drops the event is the worst available outcome |
9. Delivery History, Not Delivery State
This is the item that is almost universally missing, and the one that decides whether an integration problem takes ten minutes or a week.
Recording the current state of a delivery is not the same as recording its history. A row carrying delivered_at, an attempt counter and a last-error field clears the error on success. A delivery that failed five times and then succeeded therefore reads, forever afterwards, as though it had always worked. The information you need to fix anything is the information that got overwritten.
| Check | Why it matters |
|---|---|
| One record per attempt, not per delivery | The history is the point; the current state is derivable from it |
| Each attempt records status code, attempt number, total attempts, duration and error | Enough to diagnose without reproducing |
A response that never arrived stores a null status, not a 0 |
“We never heard back” and “it returned 500” need different fixes, and rendering the second when you mean the first sends someone hunting an access log for a request that was never made |
| The record says what happens next, not only what happened | retrying, with a predicted next attempt, tells the reader to wait. failed tells them to act. Predicting it means matching your queue’s backoff exactly, including how it behaves once the schedule is exhausted |
| Response bodies are not stored | A receiver’s error page can contain anything; the status code answers the question |
| Every outbound notification goes through one path that sends and records | Two jobs each doing their own HTTP call and their own bookkeeping is how one webhook ends up well-instrumented and the other invisible |
| The history is visible to the customer, not only to you | Otherwise someone can configure an endpoint that has never once worked and have no way to discover it |
| Delivery history is retained on its own schedule, separately from domain data | Retry noise is your behavior, not the customer’s history |
See observability and tracing.
10. Enforce It, Because Review Does Not Scale
Every item above is a rule someone can agree with and then not apply. A convention that depends on remembering holds until the next deadline.
| Check | Why it matters |
|---|---|
| The specification is linted in CI, and the build fails on regression | The specification is the artefact the caller consumes, so it is the artefact to gate |
| The lint covers descriptions, naming, examples and error shapes, not only schema validity | A document can be valid OpenAPI and still be unusable. Validators already exist; this is a different check |
| Each rule reports why it fired, not only that it did | A report that does not explain itself is noise a team learns to skip |
| New endpoints are checked against this list before they ship | Retrofitting consistency across fifty endpoints costs far more than applying it to one |
Specification linting is a solved problem with several tools available; see API tooling. General linters such as Spectral check whether a document is well formed; legible scores it against the checks in this list specifically, and both are worth running because they disagree about what counts as a problem. Webhook contracts have no equivalent, so for now section 7 through section 9 remain a review rather than a gate. That is a good reason to do the review deliberately, and to write down what you decided.
Using This
Run it against one API rather than all of them, and in order. Sections 1 to 6 are the request/response contract, 7 to 9 are the event contract, and section 10 is what stops the result decaying. Most teams find the first four sections mostly satisfied and the last three mostly absent, because the outbound event surface is the one that never had a design review.
None of this is specific to agents. It is what an API owes any consumer that has to integrate without being able to ask you a question: a partner team in another timezone, an SDK generated from your specification, a customer’s CI job, or a language model. Agents are just the consumer that made the cost visible.