API Contract Spec
An API written without a contract is negotiated one endpoint at a time, in the moment, by whoever is typing. The agent picks a URL shape at 2pm and a different one at 4pm. One endpoint returns errors as strings, another as objects, and your frontend grows a small museum of error-handling styles to cope. None of this is the agent being bad at its job. It answered every question you left open, and it answered them inconsistently because you asked them separately.
The contract closes the questions before the code exists. It says, once, how URLs are shaped, how errors look, how auth works, and how lists paginate, and then every endpoint inherits those answers. The endpoint entries themselves get short, because they only carry what is actually specific to them: the path, the shapes in and out, and the ways they fail.
This is also the document that makes an agent genuinely fast. Hand it the contract and it can build the backend, the frontend client, and the tests against the same shapes, in any order, and the pieces meet in the middle. Skip it and you build the same API twice: once in code, and once in the debugging sessions where you find out what the code decided.
Write the conventions section first. It is the highest-value ten minutes in the file.
# API Contract: [project name]
Base URL: [e.g. /api/v1; version in the path from day one, renaming later breaks every client]
Format: JSON in, JSON out. `Content-Type: application/json` on every request with a body.
## Conventions (every endpoint inherits these)
- Naming: plural nouns for resources (`/notes`, `/notes/:id`). No verbs in URLs;
the HTTP method is the verb.
- IDs: [uuid / integer]. Never sequential integers if URLs are guessable by users.
- Timestamps: ISO 8601 UTC (`2026-08-10T14:00:00Z`) everywhere, no exceptions.
- Errors: every non-2xx response has the same body shape:
```json
{ "error": { "code": "not_found", "message": "Note 42 does not exist" } }
```
Codes are stable strings the frontend can switch on. Messages are for humans
and may change.
- Pagination: list endpoints take `?limit=` (default 20, max 100) and `?cursor=`,
and return `{ "items": [...], "next_cursor": "..." | null }`.
- Auth: [Bearer token in the Authorization header / session cookie]. Secrets and
tokens never appear in URLs; URLs end up in logs.
## Auth
- How a client gets a token: [login endpoint / API key issued where]
- 401 = not logged in. 403 = logged in but not allowed. Do not blur these;
they are debugged differently.
## Rate limits
- [e.g. 60 requests/minute per token. 429 with a `Retry-After` header when exceeded.]
---
## Endpoints
### GET /notes
- Purpose: list the current user's notes, newest first.
- Auth: required.
- Query params: `limit`, `cursor` (see conventions), `?tag=` optional filter.
- Response 200:
```json
{ "items": [ { "id": "…", "title": "…", "created_at": "…" } ], "next_cursor": null }
```
- Errors: 401.
- Notes: list items are summaries; full body only on GET by id. [Keeps list
responses small; adjust if your resource is tiny.]
### POST /notes
- Purpose: create a note.
- Auth: required.
- Request body:
```json
{ "title": "string, 1-200 chars, required", "body": "string, optional" }
```
- Response 201: the full created object, including server-set `id` and `created_at`.
- Errors: 400 `validation_failed` (with `message` naming the field), 401.
- Notes: validate on the server even though the frontend also validates.
The frontend is a convenience; the server is the law.
### GET /notes/:id
- Purpose: fetch one note, full body.
- Auth: required. Must belong to the current user, else 404 (not 403; do not
confirm the resource exists to people who don't own it).
- Response 200: the full object.
- Errors: 401, 404 `not_found`.
### PATCH /notes/:id
- Purpose: partial update. Only fields present in the body change.
- Request body: any subset of the writable fields from POST.
- Response 200: the full updated object.
- Errors: 400, 401, 404.
### DELETE /notes/:id
- Purpose: delete a note.
- Response 204, empty body. Deleting something already deleted returns 204
again, not 404: deletes are idempotent, and retries happen.
- Errors: 401.
[Repeat the block above for every resource. If an endpoint doesn't fit the
conventions, that is a signal to fix the endpoint, not to add an exception.]
---
## Out of scope (this version)
- [e.g. sharing between users, file attachments, admin endpoints]
## Instructions to the agent
- Implement exactly these endpoints. If a shape here is ambiguous or wrong,
stop and ask; do not invent a corrected version silently.
- Every endpoint gets at least one test for the success case and one for its
listed error cases before it counts as done.Adaptation notes:
- For a webhook receiver, add an entry per incoming event with one non-negotiable note: handle duplicate deliveries, because every webhook provider eventually sends the same event twice. Idempotency is the whole game there.
- If a frontend and backend are being built by different sessions (or different people), this file is the only thing both need to agree on. Freeze it before parallel work starts and treat changes as a versioned edit to the contract, not a quiet code change.
- Contracts drift. When the agent changes an endpoint mid-build for a good reason, the contract gets updated in the same commit or the document starts lying, and a contract that lies is worse than none.
- The common mistake is speccing only the happy path. The error rows are where frontends actually break; an endpoint entry without its error cases is half an entry.
- For a public API someone else will consume, this file becomes your docs page with light edits. Write it like a stranger will read it, because eventually one will.