API Documentation skill for Codex
Reference from the spec, guides from real calls. Codex finds where the OpenAPI spec really comes from, edits code annotations or YAML accordingly, and fills gaps in order of consumer pain: auth, error responses, summaries, field constraints, then examples. Examples are captured from a local or sandbox server rather than invented, and a bundled script scores coverage per operation, parameter and schema property.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- api-documentation Installs to ~/.agents/skills/api-documentation/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install api-documentation The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project api-documentation Installs to .agents/skills/api-documentation/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/api-documentation.zipunzip -o api-documentation.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) api-documentation PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $api-documentation, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Produces API reference and guides from an OpenAPI 3.x spec and the real implementation - operation summaries, parameter and schema descriptions, request and response examples, error catalogs, authentication and pagination guides - and renders them with Redocly, Scalar or a docs site. Includes a stdlib script that scores an OpenAPI file for missing descriptions and examples. Use when documenting a REST API, filling gaps in an OpenAPI spec, writing a getting-started or auth guide for an API, or when consumers keep asking questions the docs should answer.
Use it when
- A REST API has a spec but sparse descriptions, examples or error responses
- Consumers keep asking about auth, pagination or error codes
- You need rendered API reference plus getting-started guides
Use something else when
- You are designing or changing the API contract itself: use openapi-first
- You need typed client SDKs generated from the spec: use openapi-client-generation
- You are documenting functions inside a codebase rather than an HTTP API: use docstrings
How to check the result
The coverage script meets the agreed threshold, Redocly lint passes, and every example validates against its schema and was reproduced against a local or sandbox server.
The mistake it prevents
Documenting only the happy-path 200 response, leaving consumers to discover 401, 409, 422 and 429 behaviour in production.
API docs are done when a developer who has never seen the API can authenticate, make a successful call and handle the likely errors using only the docs, and every example in them returns what the docs say it returns.
Reference answers “what exactly does this accept and return”. Guides answer “how do I accomplish X”. Ship both; generate reference from the spec, hand-write guides.
Inspect first
ls openapi.* api/*.yaml **/openapi*.json 2>/dev/null # find the spec
cat AGENTS.md 2>/dev/null | head -60 # docs build, lint commands
rg -n "openapi|swagger|redocly|spectral|scalar" package.json pyproject.toml Makefile .github 2>/dev/null
Determine where the spec comes from, because that is where edits must go:
| Source of truth | Edit here | Never |
|---|---|---|
| Hand-written YAML spec | The YAML file | Edit generated HTML |
Generated from code (FastAPI, NestJS @nestjs/swagger, springdoc, utoipa, drf-spectacular) | Docstrings, decorators, schema field metadata in code | Hand-edit the emitted JSON; it is overwritten next build |
| No spec at all | Propose generating one from code, or write one; confirm with the user first | Write prose reference that duplicates the code by hand |
For designing or changing the API contract itself, hand off to $openapi-first; this
skill documents what exists.
Workflow
- [ ] 1. Locate the spec and its source of truth; read AGENTS.md
- [ ] 2. Score coverage: python3 scripts/openapi_doc_coverage.py <spec.json>
- [ ] 3. Lint: npx @redocly/cli lint <spec> (needs network the first time)
- [ ] 4. Fill gaps in priority order (table below)
- [ ] 5. Capture real examples from a local or sandbox server
- [ ] 6. Write or update the guides: quickstart, auth, errors, pagination, webhooks
- [ ] 7. Render preview and click through every operation
- [ ] 8. Re-run coverage and lint; report what remains
Gap priority
| Priority | Gap | Why first |
|---|---|---|
| 1 | Auth: securitySchemes and per-operation security | Nobody gets past step one without it |
| 2 | Error responses (4xx/5xx) with schema and example | Consumers spend most debugging time here |
| 3 | Operation summary + description | Navigation and search depend on summaries |
| 4 | Parameter and property description, formats, constraints | Units, ranges, enums, nullability |
| 5 | Request and response examples | Copy-paste starting points |
| 6 | tags with tag descriptions, operationId | Grouping in rendered docs, SDK method names |
Writing reference content
Summary: imperative verb phrase, under ~8 words, no trailing period: “Create a refund”, “List invoices for a customer”.
Description: what it does, side effects, idempotency, permissions needed, rate-limit class, and anything surprising. One short paragraph plus bullets.
Parameters and properties must state, where relevant:
- Units and format: “Amount in the smallest currency unit (cents for USD).”
- Constraints:
minimum,maximum,maxLength,pattern,enumin the schema, not only in prose. - Defaults and what omission means: “Defaults to 20; maximum 100.”
- Nullability and difference between absent and null.
- Which values are case-sensitive, and the time zone of timestamps (prefer RFC 3339 UTC).
paths:
/v1/refunds:
post:
operationId: createRefund
summary: Create a refund
description: |
Refunds all or part of a captured payment. Idempotent when you send an
`Idempotency-Key` header; repeating a request with the same key returns the
original refund instead of creating a second one.
Requires the `refunds:write` scope.
tags: [Refunds]
parameters:
- name: Idempotency-Key
in: header
required: false
description: Unique key per logical refund, up to 255 characters. Keys expire after 24 hours.
schema: { type: string, maxLength: 255 }
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/RefundCreate' }
examples:
partial:
summary: Refund 5.00 of a 20.00 payment
value: { payment_id: pay_9Qx2, amount: 500, reason: requested_by_customer }
responses:
'201':
description: Refund created. `status` is `pending` until the processor confirms.
content:
application/json:
schema: { $ref: '#/components/schemas/Refund' }
'409':
description: The payment is already fully refunded.
content:
application/problem+json:
schema: { $ref: '#/components/schemas/Problem' }
OpenAPI 3.1 note: inside a Schema Object prefer examples (a JSON Schema array); the
singular example is deprecated there. On Media Type, Parameter and Header objects, the
examples map of named Example objects is the richer choice and renders as a dropdown.
Examples that are true
- Capture examples from a running local or sandbox server, then trim. Never invent response fields; a made-up field becomes a support ticket.
- Use realistic but fake IDs with the real prefix format (
cus_,pay_), test API keys only, and no real customer data. - Show at least one curl request per operation in guides:
curl -sS https://api.example.com/v1/refunds \
-H "Authorization: Bearer $EXAMPLE_API_KEY" \
-H "Idempotency-Key: 8c1e0d9a-refund-001" \
-H "Content-Type: application/json" \
-d '{"payment_id":"pay_9Qx2","amount":500}'
- If network is off in the Codex sandbox, run the server locally (read AGENTS.md for the command) or mark examples as unverified in the report. Never call production APIs or use live keys to capture examples without the user’s explicit go-ahead.
Guides every public API needs
| Guide | Must contain |
|---|---|
| Quickstart | Get a key, first request, expected response, next steps; under 10 minutes |
| Authentication | Scheme, where to get credentials, scopes table, token lifetime and refresh, common 401/403 causes |
| Errors | Error format (RFC 9457 problem details if used), full code catalog with cause and fix, retryable vs not |
| Pagination | Cursor or offset model, page size limits, a loop that fetches all pages |
| Rate limits | Limits per plan, headers returned, 429 handling with backoff |
| Webhooks (if any) | Event list, payload examples, signature verification code, retry schedule |
| Versioning and changelog | How versions are selected, deprecation policy, link to changelog |
Rendering and linting
npx @redocly/cli lint openapi.yaml # structural + rule-based lint
npx @redocly/cli build-docs openapi.yaml -o dist/api.html
npx @redocly/cli bundle openapi.yaml --ext json -o dist/openapi.json # single JSON file for tooling
npx @stoplight/spectral-cli lint openapi.yaml # if the repo uses Spectral rulesets
npx downloads packages, so it needs network or a pre-installed devDependency. Prefer
the version pinned in the repo’s package.json. For Scalar or a docs-site integration
(Docusaurus, Starlight, Mintlify), follow the platform’s OpenAPI plugin; see $docs-as-code.
Coverage script
python3 scripts/openapi_doc_coverage.py dist/openapi.json
python3 scripts/openapi_doc_coverage.py openapi.yaml --min 90 # YAML needs PyYAML installed
Reports per-category coverage (operation summaries and descriptions, parameter and
property descriptions, examples, documented error responses, security) and lists each
gap with its JSON path. Exits 1 when overall coverage is below --min (default 80).
Without PyYAML, bundle to JSON first with the Redocly command above.
Gotchas
descriptionon a$refsibling is ignored in OpenAPI 3.0 (allowed in 3.1). In 3.0, put the description on the referenced schema or wrap withallOf.- Documenting only the 200 response. Every operation needs its realistic 4xx set: 400/422 validation, 401, 403, 404, 409, 429.
- Enum values listed in prose but not in the schema: SDKs and validators never see them.
- Generated docs from code where the framework hides validation constraints; confirm the
schema shows
maxLength,minimumand required fields the code enforces. - Pasting examples captured with a real token or customer email. Scrub before committing.
- Example drift: add a contract test or schema validation of examples (Redocly lint checks
examples against schemas with its
no-invalid-media-type-examplesrule).
Verification
- Coverage script at or above the agreed threshold; lint clean or remaining warnings listed with reasons.
- Every example validated against its schema and, where possible, reproduced against a local server in this session.
- Rendered preview opened; auth, errors and pagination guides linked from the reference.
- A cold read of the quickstart reaches a 2xx response without outside knowledge.
Report
**Spec:** openapi.yaml (OpenAPI 3.1, 42 operations), source of truth: hand-written
**Coverage:** 58% -> 94% (remaining: 3 webhook payload examples, see list)
**Lint:** redocly lint 0 errors, 2 warnings (operation-4xx-response on /health, intentional)
**Examples:** 38 captured from local server; 4 marked unverified (needs sandbox Stripe)
**Guides added:** quickstart.md, authentication.md, errors.md, pagination.md
**Questions:** is `amount` allowed to exceed the original payment for FX adjustments? What the zip contains
Codex reads bundled files only when a step points to them. Every one is shown here, so you can review it before installing.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "API Documentation"
short_description: "Reference from the spec, guides from real calls"
default_prompt: "$api-documentation Our OpenAPI spec has no descriptions or examples; fill them in from the code" scripts/openapi_doc_coverage.py
#!/usr/bin/env python3
"""Score an OpenAPI 3.x document for documentation completeness.
Usage: python3 openapi_doc_coverage.py <spec.json|spec.yaml> [--min PERCENT] [--max-gaps N]
Reads JSON with the standard library. YAML works only if PyYAML is importable; otherwise
bundle to JSON first (npx @redocly/cli bundle spec.yaml --ext json -o spec.json).
Exit codes: 0 coverage >= --min (default 80), 1 below threshold, 2 usage/parse error.
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
METHODS = ("get", "put", "post", "delete", "options", "head", "patch", "trace")
def load(path: Path) -> dict:
text = path.read_text(encoding="utf-8")
if path.suffix.lower() == ".json" or text.lstrip().startswith("{"):
return json.loads(text)
try:
import yaml # type: ignore
except ImportError:
raise SystemExit("error: YAML input needs PyYAML; convert to JSON first (see --help)")
return yaml.safe_load(text)
class Score:
def __init__(self) -> None:
self.cats: dict[str, list[int]] = {}
self.gaps: list[str] = []
def check(self, cat: str, ok: bool, where: str, what: str) -> None:
hit = self.cats.setdefault(cat, [0, 0])
hit[1] += 1
if ok:
hit[0] += 1
else:
self.gaps.append(f"[{cat}] {where}: {what}")
def resolve(spec: dict, node):
seen = 0
while isinstance(node, dict) and "$ref" in node and seen < 20:
ref = node["$ref"]
if not ref.startswith("#/"):
return {}
cur = spec
for part in ref[2:].split("/"):
cur = cur.get(part.replace("~1", "/").replace("~0", "~"), {}) if isinstance(cur, dict) else {}
node, seen = cur, seen + 1
return node if isinstance(node, dict) else {}
def has_example(spec: dict, media: dict) -> bool:
if media.get("example") is not None or media.get("examples"):
return True
schema = resolve(spec, media.get("schema", {}))
return schema.get("example") is not None or bool(schema.get("examples"))
def main(argv: list[str]) -> int:
if not argv or argv[0] in ("-h", "--help"):
print(__doc__.strip())
return 2
minimum, max_gaps = 80.0, 60
if "--min" in argv:
minimum = float(argv[argv.index("--min") + 1])
if "--max-gaps" in argv:
max_gaps = int(argv[argv.index("--max-gaps") + 1])
path = Path(argv[0])
try:
spec = load(path)
except (OSError, ValueError) as exc:
print(f"error: cannot read {path}: {exc}", file=sys.stderr)
return 2
if not isinstance(spec, dict) or "openapi" not in spec:
print("error: not an OpenAPI 3.x document (missing 'openapi' key)", file=sys.stderr)
return 2
s = Score()
info = spec.get("info", {})
s.check("info", bool(info.get("description")), "info", "missing description")
has_global_security = bool(spec.get("security"))
s.check("security", bool(spec.get("components", {}).get("securitySchemes")), "components", "no securitySchemes defined")
tag_docs = {t.get("name"): t.get("description") for t in spec.get("tags", []) if isinstance(t, dict)}
for route, item in spec.get("paths", {}).items():
item = resolve(spec, item)
shared_params = item.get("parameters", [])
for method in METHODS:
op = item.get(method)
if not isinstance(op, dict):
continue
where = f"{method.upper()} {route}"
s.check("operations", bool(op.get("summary")), where, "missing summary")
s.check("operations", bool(op.get("description")), where, "missing description")
s.check("operations", bool(op.get("operationId")), where, "missing operationId")
s.check("operations", bool(op.get("tags")), where, "no tags")
for tag in op.get("tags", []):
tag_docs.setdefault(tag, None)
s.check("security", "security" in op or has_global_security, where, "no security requirement (add security: [] if intentionally public)")
for p in shared_params + op.get("parameters", []):
p = resolve(spec, p)
s.check("parameters", bool(p.get("description")), f"{where} param '{p.get('name')}'", "missing description")
body = resolve(spec, op.get("requestBody", {}))
for ctype, media in body.get("content", {}).items():
s.check("examples", has_example(spec, media), f"{where} request {ctype}", "no example")
responses = op.get("responses", {})
codes = [str(c) for c in responses]
s.check("errors", any(c.startswith("4") or c == "default" for c in codes), where, "no 4xx or default response documented")
for code, resp in responses.items():
resp = resolve(spec, resp)
for ctype, media in resp.get("content", {}).items():
if str(code).startswith("2"):
s.check("examples", has_example(spec, media), f"{where} {code} {ctype}", "no example")
for name, schema in spec.get("components", {}).get("schemas", {}).items():
schema = resolve(spec, schema)
for prop, pdef in (schema.get("properties") or {}).items():
pdef_resolved = pdef if "$ref" not in pdef else resolve(spec, pdef)
ok = bool(pdef.get("description") or pdef_resolved.get("description"))
s.check("properties", ok, f"schemas.{name}.{prop}", "missing description")
for tag, desc in tag_docs.items():
s.check("tags", bool(desc), f"tag '{tag}'", "no tag description")
hit = sum(v[0] for v in s.cats.values())
total = sum(v[1] for v in s.cats.values()) or 1
pct = 100.0 * hit / total
print(f"OpenAPI {spec.get('openapi')} - {path}")
for cat, (h, t) in sorted(s.cats.items()):
print(f" {cat:<11} {h:>4}/{t:<4} {100.0 * h / t:5.1f}%")
print(f" {'overall':<11} {hit:>4}/{total:<4} {pct:5.1f}% (threshold {minimum:.0f}%)")
if s.gaps:
print(f"\nGaps ({len(s.gaps)}):")
for g in s.gaps[:max_gaps]:
print(f" - {g}")
if len(s.gaps) > max_gaps:
print(f" ... {len(s.gaps) - max_gaps} more (raise --max-gaps)")
return 0 if pct >= minimum else 1
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))
About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.