API documentation best practices — OpenAPI spec (paths, components, examples, x-webhooks), docs-as-code CI/CD, platform selection (Mintlify/Redoc/Scalar), interactive playgrounds, changelog automation with conventional commits.
From clarcnpx claudepluginhub marvinrichter/clarc --plugin clarcThis skill uses the workspace's default tool permissions.
Designs and optimizes AI agent action spaces, tool definitions, observation formats, error recovery, and context for higher task completion rates.
Enables AI agents to execute x402 payments with per-task budgets, spending controls, and non-custodial wallets via MCP tools. Use when agents pay for APIs, services, or other agents.
Compares coding agents like Claude Code and Aider on custom YAML-defined codebase tasks using git worktrees, measuring pass rate, cost, time, and consistency.
Engineering great API documentation — from OpenAPI descriptions to interactive playgrounds, changelog automation, and prose quality checks. API documentation is a product, not an afterthought.
For API design conventions (resource naming, status codes, versioning): see skill
api-design. For contract-first workflow (spec linting, code generation, breaking-change CI): see skillapi-contract.
Treat documentation exactly like code: version-controlled in Git, reviewed via pull requests, built and deployed in CI.
Repository layout — docs next to code
├── api/
│ └── v1/
│ └── openapi.yaml ← spec is the source of truth
├── docs/
│ ├── guides/ ← tutorials and how-tos
│ ├── reference/ ← auto-generated from OpenAPI
│ └── changelog/ ← CHANGELOG.md or generated
├── mint.json ← Mintlify config (if using Mintlify)
└── .github/
└── workflows/
└── docs.yml ← build + lint + deploy
CI pipeline for docs (every PR):
# .github/workflows/docs.yml
name: Docs CI
on: [push, pull_request]
jobs:
docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Lint OpenAPI spec
run: npx @stoplight/spectral-cli lint api/v1/openapi.yaml
- name: Check prose (Vale)
run: vale docs/
- name: Check broken links
run: npx lychee --offline docs/**/*.md
- name: Build docs
run: npm run docs:build
- name: Deploy preview (PR only)
if: github.event_name == 'pull_request'
run: mintlify deploy --preview
Anti-patterns to avoid:
A well-documented OpenAPI spec is the foundation of every other documentation artifact. Renderers (Redoc, Scalar, Mintlify) consume your spec directly.
# openapi.yaml
paths:
/users/{id}/orders:
get:
summary: List orders for a user
description: |
Returns a paginated list of orders for the specified user.
Orders are sorted by `created_at` descending by default.
**Scopes required:** `orders:read`
**Rate limit:** 100 requests/minute per API key
operationId: listUserOrders
tags: [Orders]
parameters:
- name: id
in: path
required: true
description: The unique user identifier (UUID v4).
schema:
type: string
format: uuid
example: "550e8400-e29b-41d4-a716-446655440000"
- name: status
in: query
description: |
Filter by order status. Comma-separated list of values:
`pending`, `processing`, `shipped`, `delivered`, `cancelled`.
schema:
type: string
example: "pending,processing"
components:
schemas:
Order:
type: object
description: Represents a customer order.
properties:
id:
type: string
format: uuid
description: Unique order identifier.
example: "ord_2xNm0kqHrTwV"
status:
type: string
enum: [pending, processing, shipped, delivered, cancelled]
description: Current fulfillment status.
example: "shipped"
total_cents:
type: integer
description: Order total in the smallest currency unit (e.g., cents for USD).
example: 4999
examples:
pending_order:
summary: Newly created order
value:
id: "ord_2xNm0kqHrTwV"
status: "pending"
total_cents: 4999
shipped_order:
summary: Order that has been shipped
value:
id: "ord_9pLq7rJsKwXm"
status: "shipped"
total_cents: 12750
tags:
- name: Users
description: User accounts, profiles, and authentication.
- name: Orders
description: Order creation, management, and history.
- name: Billing
description: Subscriptions, invoices, and payment methods.
externalDocs:
description: Billing integration guide
url: https://docs.example.com/guides/billing
# Per-operation stability and rate-limit metadata
paths:
/users:
post:
x-stability: stable # stable | beta | experimental
x-rateLimit:
requests: 100
window: "1m"
scope: "per-api-key"
x-codeSamples:
- lang: TypeScript
label: SDK
source: |
const user = await client.users.create({ email, name });
- lang: Python
label: SDK
source: |
user = client.users.create(email=email, name=name)
- lang: Go
label: SDK
source: |
user, err := client.Users.Create(ctx, &CreateUserRequest{Email: email, Name: name})
- lang: Shell
label: curl
source: |
curl -X POST https://api.example.com/v1/users \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"email":"alice@example.com","name":"Alice"}'
# Webhook definitions (OpenAPI 3.1)
webhooks:
order.shipped:
post:
summary: Order shipped event
description: |
Fired when an order transitions to `shipped` status.
Retry policy: 3 attempts with exponential backoff (1s, 4s, 16s).
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/OrderShippedEvent'
| Platform | Best for | Key trait |
|---|---|---|
| Mintlify | SaaS / external developers | GitHub PR previews, MDX components, zero infra |
| Docusaurus | Open-source / complex sites | Algolia search free, blog/changelog, React customization |
| Redoc | Pure API reference only | Classic 3-panel layout, fast render, embeddable |
| Scalar | Interactive playground | Modern UI, OAuth PKCE, dark mode, ~200 kB bundle |
For detailed per-platform config examples, see skill
docs-strategy. Scalar embeds with one<script>tag; Mintlify usesmint.json; Redoc builds withnpx @redocly/cli build-docs api/v1/openapi.yaml.
A playground lets developers try API calls without leaving the docs browser.
Requirements for a production-quality playground:
localStorage so users don't re-enter# OpenAPI server block — enables environment switching
servers:
- url: https://api.example.com
description: Production
- url: https://sandbox.api.example.com
description: Sandbox — safe to experiment, no real charges
Every API operation needs working code examples in at least four languages.
| Language | Use case |
|---|---|
curl | Universal — any developer can run this |
TypeScript | Web/Node.js developers (largest audience) |
Python | Data scientists and backend developers |
Go | Systems and infrastructure developers |
Code examples must not drift from the actual API — test them against staging:
# .github/workflows/smoke-test-examples.yml
name: Smoke test code examples
on:
schedule:
- cron: '0 6 * * 1' # Every Monday at 06:00 UTC
workflow_dispatch:
jobs:
test-examples:
runs-on: ubuntu-latest
env:
API_BASE_URL: https://sandbox.api.example.com
API_KEY: ${{ secrets.SANDBOX_API_KEY }}
steps:
- uses: actions/checkout@v4
- name: Test TypeScript examples
run: |
cd docs/examples/typescript
npm ci
npx ts-node run-all.ts
- name: Test Python examples
run: |
cd docs/examples/python
pip install -r requirements.txt
python run_all.py
- name: Test curl examples
run: bash docs/examples/curl/run_all.sh
# Changelog
All notable changes to this project will be documented in this file.
Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
Versioning: [Semantic Versioning](https://semver.org/spec/v2.0.0.html)
## [Unreleased]
## [2.4.0] — 2025-11-01
### Added
- `GET /v1/orders/{id}/timeline` — returns fulfillment event history
### Changed
- `POST /v1/orders` now accepts optional `metadata` map (max 10 keys, 256 chars each)
### Deprecated
- `GET /v1/orders?format=legacy` — will be removed in v3.0. Use `format=standard`.
[Unreleased]: https://github.com/example/api/compare/v2.4.0...HEAD
[2.4.0]: https://github.com/example/api/compare/v2.3.0...v2.4.0
# .github/workflows/release-please.yml
name: Release Please
on:
push:
branches: [main]
jobs:
release-please:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- uses: googleapis/release-please-action@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
release-type: node
changelog-types: |
[
{"type":"feat","section":"Features","hidden":false},
{"type":"fix","section":"Bug Fixes","hidden":false},
{"type":"perf","section":"Performance","hidden":false},
{"type":"security","section":"Security","hidden":false},
{"type":"chore","section":"Miscellaneous","hidden":true}
]
release-please opens a "Release PR" that accumulates CHANGELOG.md entries from Conventional Commits. Merge the PR → creates a GitHub release automatically.
// .releaserc.json
{
"branches": ["main"],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
["@semantic-release/changelog", { "changelogFile": "CHANGELOG.md" }],
"@semantic-release/npm",
"@semantic-release/github",
["@semantic-release/git", {
"assets": ["CHANGELOG.md", "package.json"],
"message": "chore(release): ${nextRelease.version} [skip ci]"
}]
]
}
release-please vs semantic-release:
| Aspect | release-please | semantic-release |
|---|---|---|
| Release trigger | PR merge (human in the loop) | Push to main (fully automatic) |
| Changelog accumulation | Yes — batches multiple PRs | Per-push |
| Rollback | Revert PR before merge | Harder |
| Config complexity | Low | Medium |
| Best for | Teams with release cadence | Continuous delivery |
For breaking changes, always include a migration guide inline: show the before/after request shape and a one-paragraph migration note explaining what callers must change and why.
Structure every docs site into four distinct quadrants:
| Type | Orientation | Analogy | Key question answered |
|---|---|---|---|
| Tutorial | Learning | Teaching a child to cook | "How do I get started?" |
| How-to guide | Problem | A recipe | "How do I achieve X?" |
| Reference | Information | Encyclopedia | "What is the signature of X?" |
| Explanation | Understanding | Article | "Why does X work this way?" |
Map to directories: docs/tutorials/ (quickstart), docs/guides/ (webhooks, pagination, error-handling), docs/reference/ (auto-generated from OpenAPI), docs/explanation/ (auth rationale, rate-limiting internals).
# .vale.ini
StylesPath = .vale/styles
MinAlertLevel = warning
[*.md]
BasedOnStyles = Vale, Google, write-good
Vale.Avoid = [utilize, leverage, synergize, paradigm]
Vale.Prefer = [use, improve, combine, pattern]
brew install vale && vale sync
vale --output=line docs/ || exit 1 # CI step
Custom rules extend existence type — add tokens like 'is returned by' to enforce active voice.
# lychee — fast Rust-based link checker
brew install lychee
lychee --offline docs/**/*.md # local
lychee --accept 200,429 docs/**/*.md https://docs.example.com/sitemap.xml # CI weekly
Use lycheeverse/lychee-action@v1 in a scheduled workflow (cron: '0 4 * * 1') with fail: true.
Detect when source code changes but documentation has not been updated:
#!/bin/bash
# scripts/check-docs-staleness.sh
# Find docs files not updated in the last 90 days whose linked source files changed recently
git log --since="90 days ago" --name-only --pretty=format: src/ \
| sort -u \
| while read -r source_file; do
doc_file="docs/reference/$(basename "${source_file%.*}").md"
if [ -f "$doc_file" ]; then
last_doc=$(git log -1 --format="%ct" -- "$doc_file")
last_src=$(git log -1 --format="%ct" -- "$source_file")
if [ "$last_src" -gt "$last_doc" ]; then
echo "STALE: $doc_file (source: $source_file changed more recently)"
fi
fi
done
Before publishing or shipping API documentation:
description (not just a summary)description and exampledescription and at least one examplex-stability set on all operations (stable / beta / experimental)x-codeSamples present for all operations (curl + TypeScript + Python + Go)