Good API documentation is not prose about an API. It is a reference organised so that a developer can land on one page, copy a working request, and understand what comes back without opening a second tab. Every endpoint page should answer four questions, in this order: what does this call do, what does it take, what does it return, and what goes wrong. Answer all four and the page works. Answer three and somebody files a support ticket about the fourth.
That sounds obvious, and most references still fail on the fourth. Here is the structure that holds up, what belongs in each section, and the material that gets written but never read.
What makes API documentation good?
API documentation is good when a developer who has never seen your API can make a successful call in under five minutes, and can predict what happens on the unhappy path without having to trigger it. Three properties get you there.
- Copyable. The first thing on the page is a complete request with every required header, not a fragment that assumes six lines of setup from somewhere else.
- Exhaustive on inputs and outputs. Every parameter, every field in the response, every error code, with types and defaults stated rather than implied.
- Honest about failure. Rate limits, validation errors, pagination cut-offs and ambiguous status codes documented as first-class behaviour, not as footnotes.
Everything else (tone, narrative, personality) is secondary. Nobody reads a reference for pleasure.
Start every endpoint page with a working request
The highest-value block on an endpoint page is a request the reader can paste into a terminal and run. Put it above the parameter table, above the prose, immediately under the one-sentence description of what the endpoint does.
curl https://api.example.com/v1/documents \
-H "Authorization: Bearer $API_TOKEN" \
-d "collection=handbook" \
-d "limit=25"const res = await fetch(
'https://api.example.com/v1/documents?collection=handbook&limit=25',
{ headers: { Authorization: `Bearer ${ process.env.API_TOKEN }` } }
);
const { data, next_cursor } = await res.json();import os
import requests
res = requests.get(
"https://api.example.com/v1/documents",
headers={"Authorization": f"Bearer {os.environ['API_TOKEN']}"},
params={"collection": "handbook", "limit": 25},
)
data = res.json()["data"]Three things make a snippet useful rather than decorative. It includes the authentication header, so nobody has to go and find it. It uses realistic values rather than <YOUR_VALUE_HERE>, because a placeholder that fails on paste teaches nothing. And it appears in the languages your readers actually use: if you ship an SDK, the SDK call belongs here beside the raw HTTP, not on a separate page.
A reader who picks Python at the top of a reference should still be reading Python at the bottom of it. Tabbed samples that stay in sync across every block on the page remove a small, constant irritation, and on a long reference that irritation compounds.
Document the response, including the shape of failure
Show the successful response in full, with values that look like real data, and annotate any field whose meaning is not obvious from its name.
{
"data": [
{
"id": "doc_8f2c",
"title": "Deploying to staging",
"collection": "handbook",
"updated_at": "2026-08-14T09:31:02Z"
}
],
"next_cursor": "eyJvIjoyNX0",
"has_more": true
}Then document what happens when the call does not succeed. This is where most references stop, and it is where most integration time is actually spent. At minimum: which status codes the endpoint can return, what an error body looks like, whether the error is retryable, and what the rate limit is.
| Status | When it happens | What the client should do |
|---|---|---|
400 | A parameter failed validation | Fix the request. The body names the offending field in error.field. |
401 | Missing, malformed or expired token | Refresh the token and retry once. Do not retry a second time. |
404 | The collection does not exist, or the token cannot see it | Do not retry. 404 and 403 are deliberately indistinguishable on this endpoint. |
429 | Rate limit exceeded | Back off for the number of seconds given in Retry-After. |
5xx | Server-side fault | Retry with exponential backoff, up to three attempts. |
If your API returns 404 rather than 403 for a resource the caller is not allowed to see, write that down. It is a reasonable security decision and a completely unreasonable thing to leave a developer to infer from behaviour at two in the morning.
Put parameters in a table, not in prose
A paragraph describing five parameters is unreadable. A table of the same five is scannable, diffable, and, if you generate it from your schema, much harder to leave stale.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| collection | string | Yes | — | Slug of the collection to list documents from. |
| limit | integer | No | 25 | Documents per page. Values above 100 are clamped to 100. |
| cursor | string | No | — | Opaque cursor taken from a previous response’s next_cursor. |
| updated_since | string (RFC 3339) | No | — | Return only documents modified at or after this timestamp. |
| include_archived | boolean | No | false | Include documents that belong to archived collections. |
Two columns earn their keep and are routinely missing. Default answers the question the reader actually has, which is what happens if they leave the parameter out. Required saves them a round trip through a 400.
Write for the questions a reader arrives with
Nobody arrives at a reference page to learn. They arrive with one specific question, and the page either answers it in the first screen or costs them a search. Map the sections to the questions rather than to your own mental model of the API.
| The reader’s question | Where the page answers it |
|---|---|
| What does this endpoint do? | One sentence under the heading. Not a paragraph. |
| How do I call it? | The copyable request, immediately below that sentence. |
| What can I send it? | The parameter table. |
| What comes back? | An annotated success response. |
| What if it breaks? | The status-code table and the error body shape. |
| How does this fit with everything else? | A short “see also” at the foot, pointing at the guide, not at another reference page. |
What to leave out
Three things take up room in references without earning it.
- A narrative introduction to the API. That belongs in a guide and should be linked from the reference. Somebody on an endpoint page has already decided to use the thing.
- Every field of every shared object, repeated inline. Define shared objects once and link to them. Repeated definitions drift, and the drift is invisible until a reader hits it.
- Changelog entries mixed into the reference. Keep a separate changelog and put a dated note on the endpoint only when its own behaviour changed.
Generate what you can, write what you cannot
Parameter tables, response schemas and status codes should come out of your OpenAPI description or your type definitions. Anything hand-maintained drifts within two releases, and a reference that lies is worse than one that is thin: a thin page sends the reader to support, a wrong page sends them into a bug hunt.
What cannot be generated is the part that matters most: why an endpoint exists, which of two similar endpoints to reach for, what limit is sensible in practice rather than merely permitted, and what the genuinely surprising behaviour is. Write those by hand, keep them to a few sentences, and put them where the generated material cannot overwrite them.
A generated reference tells you what the API accepts. A written one tells you what to do with it. You need both, and only one of them can be automated.
A checklist before you publish an endpoint page
- A one-sentence description directly under the heading.
- A complete, runnable request, authentication included.
- Every parameter in a table, with type, required and default.
- A full success response using values that look like real data.
- Every status code the endpoint returns, and whether the client should retry.
- Pagination behaviour, if the endpoint paginates.
- Rate-limit behaviour, if the endpoint is rate limited.
- A link to the guide that explains when to use this endpoint.
Doku ships an API Parameter Table block and language-synced tabbed code samples for exactly this page shape. For where a reference sits relative to guides and tutorials, see How to Structure a Documentation Site.