How to Write API Documentation Developers Actually Read

Every endpoint page has to answer four questions: what the call does, what it takes, what it returns, and what happens when it fails. Structure the page around those four and developers can use your reference without reading it end to end.

A red scale ruler lying across a set of printed engineering drawings.

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.

bashcURL
curl https://api.example.com/v1/documents \
  -H "Authorization: Bearer $API_TOKEN" \
  -d "collection=handbook" \
  -d "limit=25"
javascriptJavaScript
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();
pythonPython
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.

Keep the language choice sticky

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.

json200 OK
{
  "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.

StatusWhen it happensWhat the client should do
400A parameter failed validationFix the request. The body names the offending field in error.field.
401Missing, malformed or expired tokenRefresh the token and retry once. Do not retry a second time.
404The collection does not exist, or the token cannot see itDo not retry. 404 and 403 are deliberately indistinguishable on this endpoint.
429Rate limit exceededBack off for the number of seconds given in Retry-After.
5xxServer-side faultRetry with exponential backoff, up to three attempts.
Say the ambiguous parts out loud

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.

ParameterTypeRequiredDefaultDescription
collectionstringYes—Slug of the collection to list documents from.
limitintegerNo25Documents per page. Values above 100 are clamped to 100.
cursorstringNo—Opaque cursor taken from a previous response’s next_cursor.
updated_sincestring (RFC 3339)No—Return only documents modified at or after this timestamp.
include_archivedbooleanNofalseInclude 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 questionWhere 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.

  1. 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.
  2. 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.
  3. 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.
Blocks for this page shape

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.