What Makes API Documentation Actually Useful

2026-05-12 | Tags: [screenshot-api, developer-experience, documentation, dx, api-design, devrel]

Most API documentation is written by people who already know how the API works. That's the problem. The author knows which parameters matter for which use cases, which error codes actually happen in practice versus which are theoretical, and which combination of options produces the result the developer wants. None of that implicit knowledge makes it into the docs.

The result is reference documentation that's technically complete and practically useless. Every parameter is listed. No use case is covered. The developer reads it, learns that width accepts integers, and still doesn't know how to take a screenshot of a page that requires authentication.

Good documentation is structured around what developers are trying to do, not around what the API can do.

Progressive Disclosure

The first thing a developer sees in your docs should answer one question: "Can this API do what I need?" That's a binary decision, and it should take 30 seconds to make.

Structure documentation in layers:

Layer 1: The quick-start — One working example, copy-pasteable, no sign-up required (use a demo key or public endpoint). The quick-start should work the first time. If it requires configuring anything beyond pasting it into a terminal, it's not a quick-start.

Layer 2: Common use cases — Five to eight scenarios that cover 80% of what developers actually use the API for. Not parameter-by-parameter coverage, but task-oriented: "How do I screenshot a page behind a login?", "How do I batch process a list of URLs?", "How do I capture only the visible viewport?". Each use case is a complete, working example.

Layer 3: The full reference — Every endpoint, every parameter, every response field, every error code. Alphabetical or logical grouping. This is what developers use once they're already building, not what converts them from evaluation to implementation.

Most APIs have only layer 3. Some have layer 1. The ones developers love have all three.

# Layer 1: Quick-start (30 seconds to working)
import requests

response = requests.get(
    "https://api.example.com/screenshot",
    params={"url": "https://example.com", "width": 1280},
    headers={"X-API-Key": "demo_key_readonly"}
)
with open("screenshot.png", "wb") as f:
    f.write(response.content)
# Layer 2: Use case — screenshot behind authentication
response = requests.get(
    "https://api.example.com/screenshot",
    params={
        "url": "https://app.example.com/dashboard",
        "width": 1280,
        "js": "true",
        "delay": 2000,
        "cookies": "session=abc123; csrf_token=def456",
    },
    headers={"X-API-Key": "your_api_key"}
)

The quick-start uses demo_key_readonly — a real, working key with rate limits — not a placeholder. Placeholders break copy-paste. Developers who can't get the first example working don't evaluate the API further.

Interactive Examples

The jump from reading documentation to writing code has friction. Interactive examples reduce that friction to near zero.

The minimum viable interactive example: a form with the endpoint's parameters, a "Run" button, and the actual response displayed inline. Not a mock response — the real API, called from the docs page, returning real data.

// Docs page: inline "Try it" widget
async function runExample(params) {
    const response = await fetch('/api/screenshot?' + new URLSearchParams(params), {
        headers: { 'X-API-Key': getDemoKey() }
    });

    if (response.headers.get('content-type').startsWith('image/')) {
        const blob = await response.blob();
        document.getElementById('result-image').src = URL.createObjectURL(blob);
    } else {
        const json = await response.json();
        document.getElementById('result-json').textContent = JSON.stringify(json, null, 2);
    }
}

The interactive example doubles as a debugging tool. When a developer's integration isn't working, they can reproduce it in the docs widget to isolate whether the problem is in their code or in the API. Stripe's API explorer is the gold standard — every endpoint is callable inline, with the developer's real credentials, showing the real request and response.

Error Reference Completeness

Error documentation is the most neglected part of API docs and the most important for developers in the implementation phase. The standard approach: list the error codes in a table with one-line descriptions. That's necessary but not sufficient.

A complete error reference has three parts per error code:

What it means — the specific condition that triggers this error, not just the name. auth.key_not_found means the key doesn't exist in the database, not just that authentication failed.

Why it happens — the common causes in practice. rate_limit.hourly_exceeded happens when burst traffic hits the limit, when a loop runs without backoff, or when multiple application instances share a key without coordinating.

How to fix it — the specific remediation. Not "check your API key" — "verify the key is being sent in the X-API-Key header, not the Authorization header; the API does not accept Bearer tokens."

## auth.key_not_found

**HTTP Status**: 401
**Meaning**: The API key provided does not exist in the system.
**Common causes**:
- The key was deleted after being generated
- The key contains a typo (check for copied whitespace)
- Using a test key against the production endpoint or vice versa
**Fix**: Generate a new key at /dashboard/keys. If the key was just created, allow 30 seconds for propagation.

**Example response**:
{
  "error": "auth.key_not_found",
  "message": "The API key provided was not found. Verify the key at /dashboard/keys.",
  "request_id": "req_01HXYZ"
}

This level of error documentation eliminates a category of support requests entirely. The developer reads it, recognizes their situation, and fixes it without contacting support.

What to Omit

Documentation that includes everything is documentation that communicates nothing. Omission is a documentation decision, not a failure.

Omit parameters that developers should never set directly. If a parameter is for internal use, debugging by the API team, or a legacy compatibility flag, don't document it. Documenting it invites misuse and generates support requests when it behaves unexpectedly.

Omit error codes that never occur in normal usage. A server.database_deadlock error code that triggers once per 10 million requests doesn't need developer-facing documentation. Log it internally, but don't add it to the public reference where it creates confusion about what developers need to handle.

Omit historical context. Developers don't need to know that width used to default to 800 before version 2.3. They need to know what it defaults to now. Historical context belongs in a changelog, not in the parameter description.

The test: if removing a section would make a developer more confused, keep it. If it would leave them equally informed, remove it.

Code Examples in Every Language You Support

Language-specific code examples are a forcing function. Writing the Python example is easy because you know Python. Writing the PHP example exposes every assumption you made about how developers handle binary responses, authentication headers, and error checking.

# Python
import requests
response = requests.get(
    "https://api.example.com/screenshot",
    params={"url": "https://example.com"},
    headers={"X-API-Key": "your_api_key"},
    timeout=30
)
response.raise_for_status()
with open("screenshot.png", "wb") as f:
    f.write(response.content)
// Node.js
const https = require('https');
const fs = require('fs');

https.get({
    hostname: 'api.example.com',
    path: '/screenshot?url=https%3A%2F%2Fexample.com',
    headers: { 'X-API-Key': 'your_api_key' }
}, (res) => {
    const file = fs.createWriteStream('screenshot.png');
    res.pipe(file);
    file.on('finish', () => file.close());
});
# Ruby
require 'net/http'
require 'uri'

uri = URI('https://api.example.com/screenshot')
uri.query = URI.encode_www_form(url: 'https://example.com')
response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
    http.get(uri.request_uri, 'X-API-Key' => 'your_api_key')
end
File.binwrite('screenshot.png', response.body)

Each example handles the binary response correctly for its language idiom. The Python example uses response.content not response.text. The Node example streams to file. The Ruby example uses binwrite. These details matter — a developer who copies the example and gets corrupted PNG data loses an hour debugging what should have been obvious from the docs.

The Documentation Test

Before publishing, run the documentation through this test: give it to a developer who has never used the API and ask them to build a working integration using only the docs. Not someone on your team — someone completely unfamiliar with your system.

Measure three things: how long it takes to get the first successful response, how many errors they encounter that aren't covered in the error reference, and which parts of the docs they skip or skim without reading. The skipped sections are either redundant or misplaced. The undocumented errors are documentation debt that will generate support tickets at scale.

The documentation is done when the test developer builds their integration faster than they would have by asking someone on your team.


Part of the developer experience series. Previous: SDK Design Decisions. Next: Onboarding Flow Design.