What an API Contract Actually Is
An API is a behavioral contract, not a list of URLs. The contract includes shapes, errors, retry behavior, ordering, consistency and rate limits — everything a client is forced to assume, whether you documented it or not.
Frame the contract
API design starts with a consumer, a design question and a guarantee — never with a URL.
The contract is everything the client is forced to assume
Ask a team to describe their API and they will show you a table of endpoints. Ask their consumers what they *depend on* and you get a much longer list: which fields are always present, whether a create is safe to retry, whether a just-written resource is immediately readable, in what order webhook events arrive, what a 409 means here, how long a cursor stays valid.
Every one of those is part of the contract, because a client somewhere has written code that assumes an answer. If you never decided the answer, the client reverse-engineered one from observed behavior — and now your *implementation detail* is load-bearing. This is Hyrum's Law applied to APIs: with enough consumers, every observable behavior of your system will be depended on by somebody.
The practical consequence: you do not get to choose whether these guarantees exist. You only get to choose whether they are designed and written down or accidental and discovered during an outage.
- Operations — what can be asked of the system, and with which method semantics.
- Shapes — request and response fields, types, optionality, and what
nullmeans where it is allowed. - Errors — the taxonomy of failures, which are the caller's fault, and which are safe to retry.
- Ordering and consistency — whether a write is visible to the next read, and whether events arrive in business order.
- Idempotency and retries — what happens when the same request arrives twice, because eventually it will.
- Limits and pagination — how much may be asked at once, and how the rest is reached.
- Change — what the client may rely on staying true across releases.
One request, read as a set of promises
Take the most ordinary exchange an API has and read it the way a consumer must: every line is a clause. The status code is a promise about what happened. The Idempotency-Key echo is a promise about retries. The request_id is a promise that failures can be correlated. The absence of a field is a promise too — one you may not have meant to make.
This is why "we just return the model as JSON" is a contract decision, not a shortcut. It promises consumers every column of the table, forever, including the ones you add next quarter for internal bookkeeping.
POST /payments HTTP/1.1
Authorization: Bearer <token>
Idempotency-Key: 7f9c…
Content-Type: application/json
{
"amount": 1999,
"currency": "EUR",
"source": "card_abc"
}HTTP/1.1 201 Created
Request-Id: req_01H…
Idempotency-Replayed: false
{
"id": "pay_9d2…",
"status": "processing",
"amount": 1999,
"currency": "EUR"
}Where API design sits among the other disciplines
API design is not a synonym for REST, and it is not a subset of system design. Software design decides how code inside one process is structured; API design decides how software exposes stable contracts to other software; software architecture decides how services interact; system design decides how those contracts participate in a complete scalable system. The Layer Model: TCP/IP First, OSI as a Map-level transport questions and authentication mechanics each have their own domain — this domain decides where they appear *in the contract*.
That separation matters because the failure modes differ. A bad internal interface costs a refactor. A bad published contract costs a migration program: every consumer must change, on their schedule, not yours. The cheapest moment to get an API right is before the first consumer ships — which is why this domain spends so much time on evolution, compatibility and the cost of change.
| Domain | Question it answers | This domain instead asks |
|---|---|---|
| Software design | How should internal code and modules be structured? | What stable surface do we expose across the process boundary? |
| Software architecture | How do services and components interact? | What does each interaction actually promise? See API Architecture: REST, GraphQL, RPC, gRPC, WebSockets, Webhooks. |
| Networking | How are calls transported? | Which transport properties become contract clauses (timeouts, streaming)? |
| Security engineering | How are authn, authz and abuse handled? | Where do those controls appear in the contract? See API Security as a Boundary. |
| Databases | How is data stored and reached? | Which storage costs does each contract promise impose? See Why Is This Query Slow? Indexes. |
Key points
- An API is a behavioral contract: operations, shapes, errors, retries, ordering, consistency, limits and change policy — not just endpoints.
- With enough consumers, every observable behavior becomes part of the contract whether you documented it or not (Hyrum's Law).
- Guarantees you never designed still exist; consumers inferred them from behavior, and they will break loudly when the behavior shifts.
- A published contract is expensive to change because the cost lands on consumers, on their schedule.
- API design connects the domains around it: it decides where transport, security, storage and architecture facts surface as promises.
Follow a Request
Change the contract and observe which guarantee moves.
Follow the failure
How the contract fails or gets misused, hop by hop — and what it costs when it completes.
- 1Team → contract: ships "the model as JSON" with whatever fields the ORM produces, documented as a list of URLs.
- 2Consumers → contract: integrate against the observed behavior — field presence, error strings, implicit ordering — because nothing else was stated.
- 3Team → implementation: refactors internals; a field disappears, an error message changes, list order shifts with a new index.
- 4Consumers → production: integrations break in ways the provider cannot predict, because the provider never knew which behaviors were load-bearing.
- 5Team → consumers: the provider is now afraid to change anything, and the API freezes in its accidental shape.
- Consumer integrations fail on changes the provider believed were invisible refactors.
- The provider loses the ability to change its own system: every internal detail leaked into the contract is now pinned by someone else's code.
- Support burden grows: each consumer holds a slightly different mental model of the API, and each model is partially wrong.
Design, observe, evolve
A contract decision is incomplete until you know how you would notice it failing and how it changes later.
- • Write the contract as promises, not URLs: for every operation state the shape, the errors, the retry rule and the consistency the caller may assume.
- • Decide the answer to "what happens on retry, on partial failure, on concurrent update" before the first consumer ships — silence is also an answer, just an accidental one.
- • Keep a deliberate response model between storage and consumers so internal change stays internal (see [[response-contracts]]).
- • State what is *not* promised: field order, error message text, timing — an explicit non-guarantee is the only defense against Hyrum's Law.
- • Consumer bug reports that quote undocumented behavior ("it always used to return…") reveal which accidental promises exist.
- • Contract tests failing on a refactor that "shouldn't change anything" catch a leaked implementation detail before consumers do.
- • Support tickets clustering on one endpoint usually mean its contract is ambiguous, not that its users are careless.
- • A contract stated as explicit promises can grow additively: new fields, new operations and new error codes extend it without invalidating old clauses.
- • Behaviors that were never promised can change freely — but only if the non-guarantee was stated before consumers integrated.
- • When a promise must change, the change is a project with a timeline (see [[deprecation]]), not a deploy.
- • Writing the contract down costs design time before any code runs, and it makes disagreements visible early — which is the point, but it feels slower.
- • Explicit guarantees are commitments: promising read-after-write or ordering constrains future architecture choices.
- • Some flexibility is genuinely lost: a documented non-guarantee cannot quietly become a guarantee later without consumers noticing the inconsistency.