Good API documentation answers three questions for every operation: what it does, how to call it successfully, and what happens when the call fails. If your docs are rendered from an OpenAPI file, those answers live in the spec. Give every operation a short summary and a description that states the rules a caller would otherwise learn by trial and error. Attach at least one realistic request and response example. Declare authentication as a security scheme, not as a paragraph of prose. Document the 4xx responses with a real error schema. Then make CI fail when the spec and the code disagree, because a spec nobody checks drifts within a few releases.
This guide covers each of those, with a small invoices API scored before and after. For what the term covers in general, see the API documentation glossary entry.
Summaries scan, descriptions explain
The OpenAPI Specification gives every operation two text fields with different jobs. The summary is "a short summary of what the operation does". The description is the long form and supports CommonMark, so it can hold lists, links and code. Readers meet them in that order: they scan a list of summaries to find the operation they need, then read one description in full.
That gives you a rule for each. A summary is a verb phrase of two to six words that would make sense in a navigation sidebar: "List invoices", "Send an invoice", "Refund a payment". It does not repeat the path, and it does not end with a period. APIForge's endpoint list, for example, prints the summary under each path, so "GET /v1/invoices" followed by "Gets invoices" tells the reader nothing new. A description holds what the path and schema cannot say: default sort order, which records are excluded, side effects, idempotency, rate limits that apply to this operation only, and anything a caller has to do first.
| Field | Weak | Useful |
|---|---|---|
| summary | This endpoint is used to retrieve the list of invoice objects. | List invoices |
| description | Returns invoices. | Returns invoices newest first. Drafts are excluded unless status=draft is passed. |
| summary | POST /v1/invoices/{id}/send | Send an invoice |
| description | Sends the invoice. | Emails the invoice to the customer and moves it from draft to open. Calling it twice does not send a second email. |
Apply the same split to tags (a tag description says what the group of operations is for), to parameters (describe the allowed values and the default, not the type, which the schema already shows), and to schema properties. A property called amount with no description forces every client developer to guess the unit. "Amount in the smallest currency unit" settles it in five words.
A worked example: scoring an undocumented spec
Here is a spec that parses and validates without errors. Three of its four operations have no summary or description, request bodies are untyped objects, and only one operation documents an error.
openapi: 3.1.0
info:
title: Invoices API
version: 1.4.0
paths:
/v1/invoices:
get:
operationId: listInvoices
responses:
"200":
description: OK
post:
operationId: createInvoice
requestBody:
content:
application/json:
schema:
type: object
responses:
"201":
description: Created
/v1/invoices/{id}:
get:
operationId: getInvoice
summary: Get an invoice
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
"200":
description: OK
"404":
description: Not found
/v1/invoices/{id}/send:
post:
operationId: sendInvoice
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
"202":
description: AcceptedAPIForge scores this spec 84.2 out of 100. The documentation category gets 2.5 of its 10 points and error handling gets 2.5 of 10, because only GET /v1/invoices/{id} has a summary and only that operation documents a 4xx response. Structure loses 0.8 points for the four-segment send path. The issue list names the three operations with no summary or description and the three with no error responses.
The documented version below fixes both categories and adds the parts the score does not measure: a description on the API itself, a security scheme, a request example and an error body. It scores 100.
openapi: 3.1.0
info:
title: Invoices API
version: 1.4.0
description: |
Create, list, and send invoices. All requests need an API key
from Settings > Developers. Amounts are integers in the
smallest currency unit (cents for USD).
servers:
- url: https://api.example.com
security:
- ApiKeyAuth: []
paths:
/v1/invoices:
get:
operationId: listInvoices
summary: List invoices
description: Returns invoices newest first. Draft invoices are excluded unless status=draft is passed.
tags: [Invoices]
parameters:
- name: status
in: query
description: Filter by invoice status.
schema:
type: string
enum: [draft, open, paid, void]
responses:
"200":
description: A page of invoices.
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: "#/components/schemas/Invoice"
"401":
$ref: "#/components/responses/Unauthorized"
post:
operationId: createInvoice
summary: Create an invoice
description: Creates a draft invoice. Nothing is emailed until you call the send operation.
tags: [Invoices]
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/InvoiceInput"
examples:
oneLine:
summary: Single line item in USD
value:
customerId: cus_4821
currency: usd
lines:
- description: Design review, 3 hours
amount: 45000
responses:
"201":
description: The draft invoice.
content:
application/json:
schema:
$ref: "#/components/schemas/Invoice"
"401":
$ref: "#/components/responses/Unauthorized"
"422":
description: The body is valid JSON but breaks a business rule, for example an unknown customerId.
content:
application/problem+json:
schema:
$ref: "#/components/schemas/Problem"
example:
type: https://api.example.com/problems/unknown-customer
title: Unknown customer
status: 422
detail: No customer with id cus_4821 exists in this account.
components:
securitySchemes:
ApiKeyAuth:
type: apiKey
in: header
name: X-API-Key
description: Secret key from Settings > Developers. Test keys start with sk_test_.
responses:
Unauthorized:
description: The X-API-Key header is missing or the key was revoked.
content:
application/problem+json:
schema:
$ref: "#/components/schemas/Problem"
schemas:
InvoiceInput:
type: object
required: [customerId, currency, lines]
properties:
customerId:
type: string
currency:
type: string
description: ISO 4217 code, lowercase.
lines:
type: array
minItems: 1
items:
type: object
required: [description, amount]
properties:
description:
type: string
amount:
type: integer
description: Amount in the smallest currency unit.
Invoice:
allOf:
- $ref: "#/components/schemas/InvoiceInput"
- type: object
required: [id, status]
properties:
id:
type: string
examples: [inv_9f31]
status:
type: string
enum: [draft, open, paid, void]
Problem:
type: object
properties:
type:
type: string
format: uri
title:
type: string
status:
type: integer
detail:
type: stringExamples a reader can paste and run
An example is often the first thing a developer copies. OpenAPI lets you put one on a Media Type Object, a Parameter Object, or a schema. On media types and parameters, example (a single value) and examples (a named map) are mutually exclusive, so pick one per location. Use the named map when an operation has more than one real use case: an invoice with one line item, an invoice with tax, an invoice in a currency without minor units. Each named example carries its own summary, so the reader can tell the cases apart before reading the payload.
- Use values that look real and pass validation. "string" for an email and 0 for an amount teach nothing; "ana@example.com" and 45000 show the format and the unit.
- Reuse IDs across examples. If the create example uses cus_4821, the get example should return cus_4821, so a reader can follow one record through the API.
- Show an error example next to the success example, especially for 422, where the reader needs to know which field the server will complain about.
- In OpenAPI 3.1, schemas follow JSON Schema 2020-12, which uses examples (an array). The singular example keyword on a schema still works but is deprecated in 3.1.
- Validate examples against their schema in CI. An example that no longer matches the schema is worse than none, because readers trust it.
Document authentication in the spec
Authentication is where new users get stuck first, and prose alone does not help tools. Declare it in components.securitySchemes using one of the five scheme types the specification defines: apiKey, http (for bearer and basic), mutualTLS, oauth2 and openIdConnect. Apply it with a top-level security requirement so every operation inherits it. An operation can override the top-level requirement, and an empty array, security: [], removes it, which is how you mark a public health check or a login operation.
The scheme's description field is where the human answer goes: where to get a key, what a test key looks like, and how long a token lives. Then document what failure looks like. Give every protected operation a 401 response for a missing or invalid credential, and a 403 where a valid credential can still lack permission. Both should point at the same error schema. The OpenAPI authentication example shows a bearer scheme declared this way, and API security best practices covers what to put behind it.
Document errors as carefully as success
Client code spends more lines handling failures than successes, yet many specs list only 200. Document each error status an operation can really return, with a description that says what triggers it and what the caller should do. "400 Bad Request" repeats the status line. "The limit parameter is above 100" tells the reader how to fix the request.
Use one error schema across the API. RFC 9457 defines the application/problem+json media type with the members type, title, status, detail and instance, and allows extension members for problem-specific data. Its type member is a URI, and the RFC says dereferencing it should provide human-readable documentation for the problem type. That gives you a natural place for long-form error docs: one page per problem type, linked from every response that can return it. For the full pattern, see REST API error handling and the problem details response example.
Keep the docs in sync with CI
Docs drift because nothing fails when they do. A renamed field, a new required parameter, or an extra error status ships with the code, and the spec catches up later or never. Three checks in CI close most of that gap, and each catches a different kind of drift.
Three CI checks for API docs
Validate the spec on every pull request
Fail the build when the document does not parse, a $ref does not resolve, or a required field such as a response description is missing. Run the same check locally with the OpenAPI validator when you edit by hand.
Regenerate and diff if your spec comes from code
In a code-first setup, generate the spec in CI and compare it with the committed file. If they differ, someone changed an endpoint without updating the published contract. The workflow below does this with git diff.
YAMLname: openapi-sync on: [pull_request] jobs: spec: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: npm ci - run: npm run generate:openapi - run: git diff --exit-code openapi.jsonTest responses against the spec
Call a staging deploy and validate each response body against the documented schema for its status code. This catches drift that neither of the first two checks can see, such as a serializer that renames a field. Validating API responses against your OpenAPI schema walks through it with Ajv.
The generate:openapi script stands in for whatever your framework uses to export its spec. Treat the committed openapi.json as the reviewed contract: a pull request that changes it should get the same review as a database migration. Version the spec in info.version, which the specification defines as the version of the OpenAPI document, separate from both the OpenAPI version and your implementation version.
Review checklist before you publish
- Every operation has a verb-phrase summary and an operationId; operations with any rule a caller must know have a description.
- Every parameter and schema property whose meaning is not obvious from its name has a description that states units, formats and defaults.
- Every request body and every 2xx response has at least one example built from realistic values.
- A security scheme is declared and applied, public operations use security: [], and protected operations document 401.
- Every operation documents its error statuses with a shared error schema.
- CI validates the spec, checks it against the code, and tests live responses against it.
To check the first and fifth items quickly, paste your spec into the API quality score. It lists every operation missing a summary or description and every operation with no documented 4xx or 5xx response, alongside the naming, HTTP and versioning findings. For a longer pre-release list, use the OpenAPI production checklist.