API documentation tells developers what an API does and how to call it. It has two halves: a reference, with an entry for every endpoint (method, path, parameters, a sample request, a sample response and the errors it can return), and guides, such as a quickstart, authentication and a changelog. Below, three well-known API docs, Stripe's, GitHub's and Twilio's, are described section by section, followed by six original samples of the parts most teams write by hand, each with notes on why it works.
| Example | What it shows |
|---|---|
| ExampleStripe's API reference | What it showsOne pattern for every resource, and errors that link back to the docs |
| ExampleGitHub's REST API docs | What it showsGuides kept apart from the reference, and permissions per endpoint |
| ExampleTwilio's Messages resource | What it showsProperties labeled for personal data, and samples named after tasks |
| ExampleEndpoint reference sample | What it showsDescription, parameters, request, response, errors |
| ExampleAuthentication sample | What it showsKeys, scopes, test and live, what to do after a leak |
| ExampleError reference sample | What it showsA table of codes and an error body that helps |
| ExamplePagination and rate limits sample | What it showsDefaults, maximums and the last page |
| ExampleWebhook event sample | What it showsWhen an event fires, signatures, retries |
| ExampleOpenAPI description sample | What it showsThe reference written as a spec file |
What API documentation includes
Every set of API docs answers the same questions in roughly the same order. A missing page tends to show up as support tickets.
| Part | The developer's question | Usually written by |
|---|---|---|
| PartOverview | The developer's questionWhat does this API do, and is it the right one? | Usually written byA person |
| PartQuickstart | The developer's questionHow do I make one successful call? | Usually written byA person |
| PartAuthentication | The developer's questionHow do I prove who I am, and with what permissions? | Usually written byA person |
| PartEndpoint reference | The developer's questionWhat exactly does this call take and return? | Usually written byA person, often inside the spec file |
| PartErrors | The developer's questionWhat went wrong, and what do I do now? | Usually written byA person, with codes from the code |
| PartConventions | The developer's questionHow do pagination, rate limits and versions work? | Usually written byA person |
| PartCode samples and SDKs | The developer's questionWhat does a call look like in my language? | Usually written byEngineers, tested like code |
| PartChangelog | The developer's questionWhat changed, and will it break me? | Usually written byA person, at every release |
Stripe's API reference
Stripe's reference opens with an introduction before any resource. It says the API is organized around REST, with “predictable resource-oriented URLs”, form-encoded request bodies, JSON responses and standard HTTP response codes. The same page gives the base URL, https://api.stripe.com, and links the official client libraries for Ruby, Python, PHP, Java, Node.js, Go and .NET. It explains that the API key you send decides whether a request runs in live mode or in a sandbox, and readers who log in see the docs with their own test key and data.
Every resource follows one pattern. The Customers section has a sentence on what the object represents, its endpoints named by task (create, update, retrieve, list all, delete and search customers) and the events it emits, such as customer.created. Each endpoint page has a one-line description, a curl request that pins the API version in a Stripe-Version header, the full JSON response, every parameter with its type and whether it is optional, and a Returns paragraph that says what comes back and when the call raises an error.
Errors get their own page: codes in the 2xx range mean success, 4xx a problem with the request, 5xx a problem on Stripe's side, followed by a table of status codes and four error types. The error object carries a doc_url pointing to more information about the error code, and a param field that names the parameter at fault, so an app can show the message next to the right form field.
What to borrow. Task names for endpoints, a complete response instead of an excerpt, and error messages that lead back to the docs.
GitHub's REST API docs
GitHub keeps articles and reference apart. The landing page offers a quickstart, getting started, authentication, an overview of the REST API, API versions, breaking changes, best practices, and an article on the OpenAPI description that GitHub says fully describes the API. The reference is grouped by product area (Actions, Activity, Apps, Billing, Branches and so on), with endpoint groups inside each.
Each endpoint has the same parts. Create an issue shows its method and path, POST /repos/{owner}/{repo}/issues, and a description that starts with who may call it: “Any user with pull access to a repository can create an issue.” It also warns that the endpoint returns 410 Gone when a repository has issues turned off, and that creating content too quickly can trigger secondary rate limits. Four sections follow, each heading repeating the endpoint's name: the fine-grained access tokens that can call it, its parameters (grouped as headers, path parameters and body parameters), its HTTP response status codes, and code samples in cURL, JavaScript and GitHub CLI. The response appears twice, as an example and as a schema, and the sample request sends a dated X-GitHub-Api-Version header.
What to borrow. Permissions before parameters, and a status code list written for this endpoint, 410 Gone included, rather than a generic one.
Twilio's Messages resource
Twilio's reference page for messages opens with one sentence: “A Message resource represents an inbound or outbound message.” Then come the resource's properties, a table of status values, a section on how a long SMS is counted in segments, and one section per action: create, fetch, read multiple, update and delete. Each property is listed with its type, whether it is required, a PII classification and a description.
The create request goes to POST https://api.twilio.com/2010-04-01/Accounts/{AccountSid}/Messages.json, so the API version is a date inside the path. Code samples come in Node.js, Python, C#, Java, Go, PHP, Ruby, the twilio-cli and curl, and each is titled with the task it does, such as sending an SMS message, with variants for SMS, RCS, WhatsApp and Messaging Services.
What to borrow. A personal-data label on every property answers a security reviewer's first question. Status values belong in a table, because a message moves through them. And samples named after a job are easier to find than samples named after an endpoint.
What to borrow from Stripe, GitHub and Twilio
Each of the three is at its best for a different task. Give each one the job it does best, and copy the feature that makes it work.
| The reader's task | Study | The feature to copy |
|---|---|---|
| The reader's taskReproduce a request | StudyStripe | The feature to copyA curl sample on every endpoint page that pins its API version, set beside the complete JSON response; logged-in readers see their own test key and data |
| The reader's taskFind out what access a call needs | StudyGitHub | The feature to copyA section on each endpoint listing the fine-grained token permissions it needs, placed before the parameters, after a description that says who may call it |
| The reader's taskFind the code sample for a job | StudyTwilio | The feature to copySamples titled by task, such as sending an SMS message, offered in nine languages and tools |
All three also name the API version a request uses, each in its own place: Stripe in a Stripe-Version header, GitHub in a dated X-GitHub-Api-Version header and Twilio in a date inside the URL path.
Endpoint reference sample
The six samples below document one invented API, Pressline, which publishes articles to a news site and a newsletter. For the first page a developer reads, the quick start guide template has an API quickstart example, and the technical writing examples include a second reference entry, for an invoicing API. This sample is one reference entry, with the scope it needs and a response that shows how values come back.
POST /v2/drafts
Creates a draft article in a publication. The draft stays private until you publish it with Publish a draft, or until publish_at if you set it. Requires a key with the drafts:write scope, plus publish if you set publish_at.
Body parameters
publication_id(string, required). The publication the draft belongs to.title(string, required). Shown as the headline. Up to 200 characters.body(string, optional). The article text in Markdown. Defaults to empty.author_ids(array of strings, optional). Up to five authors. Defaults to the owner of the key.publish_at(string, optional). An ISO 8601 date and time with a time zone offset. When set, the draft is scheduled.
Returns the Draft object with status draft or scheduled. Errors: 400 missing_title, 403 insufficient_scope, 404 publication_not_found, 422 publish_at_in_past.
Sample request
curl https://api.pressline.example/v2/drafts \
-H "Authorization: Bearer $PRESSLINE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"publication_id": "pub_8Kd2",
"title": "Council approves the harbor budget",
"publish_at": "2026-11-12T06:00:00-05:00"
}'Sample response (201 Created)
{
"id": "drf_3Qp9",
"object": "draft",
"publication_id": "pub_8Kd2",
"title": "Council approves the harbor budget",
"status": "scheduled",
"publish_at": "2026-11-12T11:00:00Z",
"author_ids": ["aut_51Lx"],
"created_at": "2026-11-10T14:22:07Z"
}Why it works. The description says what happens next (the draft stays private) and which scope the key needs. Every parameter has a type, whether it is required, its limit and its default. The request runs as pasted, with the key in an environment variable instead of the page. And the response shows publish_at converted to UTC, which answers the time zone question before anyone files it as a bug.
Authentication section sample
Pressline uses API keys. Send your key in the Authorization header of every request: Authorization: Bearer YOUR_API_KEY.
Keys that start with pl_test_ work only in your test workspace, which has sample publications and never sends email. Keys that start with pl_live_ act on real publications. Create and revoke keys in Settings › API keys.
Each key has scopes: drafts:read, drafts:write, publish and subscribers:read. Give a key only the scopes its integration needs.
Keep keys out of browser code and out of version control. If a key leaks, revoke it at once. A request with no key or a revoked key returns 401 invalid_api_key; a key without the right scope returns 403 insufficient_scope, and the message names the scope it needs.
Why it works. One header, shown exactly. The key's prefix tells a developer which workspace a request will touch, and the test workspace promises it can't email real subscribers. Scopes are listed with the advice to use as few as possible, and the section says what to do after a leak, which is the moment people actually read it. Separating 401 from 403 tells the developer whether to fix the key or its permissions.
Error reference sample
| Status | Code | What it means | What to do |
|---|---|---|---|
| Status400 | Codeinvalid_json | What it meansThe body isn't valid JSON | What to doCheck quotes and commas; the message gives the line and column |
| Status400 | Codemissing_title | What it meansThe draft has no title | What to doSend a title of 1 to 200 characters |
| Status401 | Codeinvalid_api_key | What it meansThe key is missing, mistyped or revoked | What to doSend a current key in the Authorization header |
| Status403 | Codeinsufficient_scope | What it meansThe key lacks a scope this call needs | What to doUse a key with the scope named in the message |
| Status404 | Codepublication_not_found | What it meansNo publication with that ID in this workspace | What to doCheck the ID, and whether a test key is calling live data |
| Status409 | Codealready_published | What it meansAnother request published the draft first | What to doFetch the draft; don't retry the publish |
| Status422 | Codepublish_at_in_past | What it meansThe scheduled time has passed | What to doSend a future time with a time zone offset |
| Status429 | Coderate_limited | What it meansToo many requests | What to doWait the number of seconds in the Retry-After header |
| Status500 | Codeserver_error | What it meansSomething failed on Pressline's side | What to doRetry with backoff; give support the request_id |
Error response body
{
"error": {
"status": 422,
"code": "publish_at_in_past",
"message": "publish_at must be in the future. You sent 2026-11-09T06:00:00-05:00.",
"param": "publish_at",
"request_id": "req_7Hc1",
"doc_url": "https://docs.pressline.example/errors#publish_at_in_past"
}
}Why it works. Codes are stable strings a program can branch on, while the message is for people and repeats the value that was wrong. The param field names the input at fault, the request_id lets support find the call in seconds, and the doc_url leads back to this table, the same idea as the doc_url in Stripe's error object. The What to do column is what turns a list of codes into help.
Pagination and rate limits sample
Pagination. List endpoints return 25 items by default and 100 at most; set the number with limit. Items are sorted newest first by created_at. Each response includes next_cursor: send it back as cursor to get the next page. When next_cursor is null, you have the last page. Cursors expire after 24 hours.
Rate limits. Each key can make 50 requests a second, and each workspace 200. Over the limit you get 429 rate_limited with a Retry-After header in seconds. Wait that long before you retry; retrying at once only extends the wait.
A list request and its first page
GET /v2/drafts?publication_id=pub_8Kd2&limit=2
{
"data": [
{ "id": "drf_3Qp9", "title": "Council approves the harbor budget" },
{ "id": "drf_3Qn4", "title": "Ferry timetable changes in December" }
],
"next_cursor": "c_9Fk2"
}Why it works. It states the default, the maximum and the sort order, then says exactly how to recognize the last page, which is the question every loop needs answered. The limits are given per key and per workspace, so a team running several integrations can plan for both. This is the reference version; a concept page that explains how a limit behaves under bursts is a separate document, and the technical writing examples include one.
Webhook event reference sample
Sent when a draft goes live, either because you called Publish a draft or because its publish_at time arrived. Not sent when you schedule a draft: scheduling sends draft.scheduled.
Pressline signs every delivery. The Pressline-Signature header holds an HMAC-SHA256 of the raw request body, made with your endpoint's signing secret. Check it before you trust the payload.
Respond with any 2xx status within 10 seconds. Failed deliveries are retried up to 8 times over 24 hours, with longer gaps each time. The same event can arrive more than once, so use the event id to skip duplicates.
Payload
{
"id": "evt_5Rt8",
"type": "draft.published",
"created_at": "2026-11-12T11:00:02Z",
"data": {
"object": {
"id": "art_7Lm2",
"object": "article",
"draft_id": "drf_3Qp9",
"url": "https://harborherald.example/council-approves-harbor-budget"
}
}
}Why it works. It says when the event fires and when it doesn't, which is where webhook bugs come from. Security, timeouts, retries and duplicates each get one sentence, and the payload shows the object the developer will actually receive.
OpenAPI description sample
The OpenAPI Specification “defines a standard, programming language-agnostic interface description for HTTP APIs”, written in JSON or YAML. It grew out of the Swagger 2.0 specification, which SmartBear Software donated in 2015, and the OpenAPI Initiative, a Linux Foundation project, now maintains it. Documentation tools such as Swagger UI render the file as interactive reference pages. Here is the Create a draft entry again, as it would sit in the file. The excerpt uses OpenAPI 3.1; the latest published version, as of November 2026, is 3.2.1.
openapi.yaml (excerpt)
openapi: 3.1.0
info:
title: Pressline API
version: "2026-11-01"
servers:
- url: https://api.pressline.example/v2
paths:
/drafts:
post:
operationId: createDraft
summary: Create a draft
description: >
Creates a draft article in a publication. The draft stays
private until you publish it, or until publish_at if you set it.
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/DraftCreate"
responses:
"201":
description: The draft was created.
"422":
description: publish_at is in the past.
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
schemas:
DraftCreate:
type: object
required: [publication_id, title]
properties:
publication_id:
type: string
examples: [pub_8Kd2]
title:
type: string
maxLength: 200
description: Shown as the headline.
publish_at:
type: string
format: date-time
description: When set, the draft is scheduled for this time.Why it works. The reference text lives next to the contract, so a renamed field changes in one place. The summary becomes the page title, the description is written by a person, and limits such as maxLength appear wherever the file is shown. What the file can't hold is the rest of the docs: the quickstart, the authentication guide and the reasons behind each design choice. Teams that keep the file and those guides in one repository, reviewed like code, are practicing docs as code.
If you write the guides in Strokes, attach the OpenAPI file to a chat with the Strokes Assistant and ask which parameters, defaults or limits the hand-written pages get wrong.
Habits of good API docs
Says what the reader needs
- GET /v2/drafts: Lists a publication's drafts, newest first. Scheduled drafts are included; published ones are not.
- title (string, required): Shown as the headline. Up to 200 characters.
- Returns: the Draft object, with status draft or scheduled.
- Errors: 403 insufficient_scope if the key lacks drafts:read.
Restates the name
- GET /v2/drafts: Gets drafts.
- title (string): The title.
- Returns: the response.
- Errors: see error codes.
- Samples that run as pasted. Test keys, realistic IDs, environment variables for secrets, and a full response to compare against. Run every sample before each release.
- One name for each thing. The parameter in the table, the sample and the SDK must match. A short style guide settles naming, capitalization and how code appears in text; the style guide examples include Google's developer documentation guide.
- Versions you can see. Say which version each page describes and keep a dated changelog; the changelog examples include an API changelog with deprecations.
- The unhappy path. Errors, limits, timeouts and retries are where integrations fail, and where docs often stop short.
Frequently asked questions
What is the least documentation a small API can ship with?
An overview, a quickstart that ends in one successful call, an authentication page, a reference entry for every endpoint with a sample request and response, a list of errors with what to do about each, and a dated changelog. Webhooks, SDK guides, tutorials and a page on versioning can wait until the API has them or readers ask.
Is OpenAPI the same as Swagger?
Not exactly. OpenAPI is the specification, a standard way to describe an HTTP API in JSON or YAML. It was based on the Swagger 2.0 specification, which SmartBear Software donated in 2015. Swagger is now the name of a set of tools built around the OpenAPI Specification, such as Swagger UI and Swagger Editor.
Who writes API documentation?
Usually engineers and technical writers together. Engineers know the behavior and often write the reference descriptions inside the spec file; technical writers shape the guides, the quickstart and the wording across the set. Whoever writes it, someone who didn't build the API should try every sample before it ships.


