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.

  • Writing & Docs
  • $api-documentation
  • 208 lines
  • 3 files
  • ~2,400 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- api-documentation

Installs to ~/.agents/skills/api-documentation/ for every project on this machine.

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.

~/.agents/skills/api-documentation/SKILL.md

--- name: api-documentation description: Produces API reference and guides from an OpenAPI 3.x spec and the real implementation - operation summaries, parameter and schema descriptions,… license: MIT ---

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 truthEdit hereNever
Hand-written YAML specThe YAML fileEdit generated HTML
Generated from code (FastAPI, NestJS @nestjs/swagger, springdoc, utoipa, drf-spectacular)Docstrings, decorators, schema field metadata in codeHand-edit the emitted JSON; it is overwritten next build
No spec at allPropose generating one from code, or write one; confirm with the user firstWrite 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

PriorityGapWhy first
1Auth: securitySchemes and per-operation securityNobody gets past step one without it
2Error responses (4xx/5xx) with schema and exampleConsumers spend most debugging time here
3Operation summary + descriptionNavigation and search depend on summaries
4Parameter and property description, formats, constraintsUnits, ranges, enums, nullability
5Request and response examplesCopy-paste starting points
6tags with tag descriptions, operationIdGrouping 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, enum in 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

GuideMust contain
QuickstartGet a key, first request, expected response, next steps; under 10 minutes
AuthenticationScheme, where to get credentials, scopes table, token lifetime and refresh, common 401/403 causes
ErrorsError format (RFC 9457 problem details if used), full code catalog with cause and fix, retryable vs not
PaginationCursor or offset model, page size limits, a loop that fetches all pages
Rate limitsLimits per plan, headers returned, 429 handling with backoff
Webhooks (if any)Event list, payload examples, signature verification code, retry schedule
Versioning and changelogHow 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

  • description on a $ref sibling is ignored in OpenAPI 3.0 (allowed in 3.1). In 3.0, put the description on the referenced schema or wrap with allOf.
  • 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, minimum and 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-examples rule).

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.yamlgenerated from this page

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.py150 lines, 6.2 KB
#!/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.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem