# info.description
Public, read-only JSON API exposing the structured biographical content powering thecandidate.com. Sprint 22 ships the historical-president surface (46 rows); Sprint 23+ extends with senators + representatives. Every row mirrors 1:1 the JSON-LD `Person` block on the corresponding `/federal/{office}/historical/[slug]` page so downstream LLM crawlers + agents can ingest the structured contract directly instead of scraping HTML.

Key contracts:
- Read-only. No POST / PUT / DELETE. No auth required.
- Fail-OPEN, UA-aware rate-limiting. Default lane = 60 req/min/IP; named-AI-bot lane (GPTBot, ClaudeBot, PerplexityBot, Meta-ExternalAgent, Applebot-Extended, Bytespider, CCBot, Amazonbot, Google-Extended, plus Googlebot / Bingbot / cohere-ai / MistralAI-User / YouBot / Diffbot) = 600 req/min/IP. Every response (200 included) carries `X-RateLimit-Lane`, `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` (ISO-8601), and `X-RateLimit-Reset-Epoch` (Unix seconds, GitHub / RFC 9331-draft convention) — dual emission for parser compatibility. 429s additionally carry `Retry-After`. When rate-limit infrastructure is degraded, requests pass through with `X-RateLimit-FailedOpen: 1` (opposite of the forms write surface, which fails CLOSED). Machine-readable policy: `info.x-rate-limit-policy`. Human-readable policy: docs/architecture/api-rate-limits.md.
- ETag + Last-Modified emitted on every response. Conditional `If-None-Match` / `If-Modified-Since` requests return 304.
- `dataset_version` is the primary cache-bust signal. The response envelope carries it in `meta.dataset_version`; the ETag value is derived from it; the `X-Dataset-Version` header surfaces it ergonomically.
- Cache-Control: `public, max-age=300, stale-while-revalidate=3600` (5-minute fresh + 1-hour stale revalidate).

License posture is provisional pending the Sprint 24 operator decision. Until then, the API is free to ingest with attribution to https://thecandidate.com + a link back to the per-row canonical URL. See https://thecandidate.com/legal/api-license.

# info.x-rate-limit-policy.lanes[].selected_when
User-Agent header contains any of the named-AI-bot substrings (case-insensitive)

# info.x-rate-limit-policy.lanes[].selected_when
User-Agent does NOT match the bot lane substring list (browsers, anonymous, unknown, missing UA)

# info.x-rate-limit-policy.headers.on_success_200[]
X-RateLimit-FailedOpen (only when backing store unreachable)

# info.x-rate-limit-policy.fail_open_semantics
When the rate-limit backing store is unreachable, errors, or returns malformed payload, the request is ALLOWED through with `X-RateLimit-FailedOpen: 1`. A backing-store outage MUST NOT 429 the public read API. Opposite of the forms write surface at /api/forms/submit, which fails CLOSED.

# info.x-rate-limit-policy.policy_note
Quota numbers (60, 600) are policy, not contract. Future operator picks may tune them based on observed traffic without a spec-version bump. The contract is: fail-OPEN UA-aware dual-lane.

# info.license.name
Sprint 24 will finalize (provisional — free to ingest with attribution)

# tags[].description
Former U.S. Presidents — biographical profiles with sourced facts.

# tags[].description
API metadata + machine-readable contract.

# paths./api/historical/presidents.get.description
Returns every individual who has served as President of the United States, in chronological order (first-term start year ascending). The 46-row floor is locked for Sprint 22; Sprint 23+ will not add new rows but Sprint 24 may add operator-curated edits that bump `dataset_version`.

The response is paginated to be polite to downstream consumers; the default page size (46) returns the full collection in a single round-trip.

# paths./api/historical/presidents.get.parameters[].description
Page size. Maximum 100. Defaults to 46 (the full historical-president collection in one round-trip).

# paths./api/historical/presidents.get.responses.200.headers.ETag.description
Strong validator derived from MAX `dataset_version` across rows + row count + ordering hash. Use with `If-None-Match` for conditional refetch.

# paths./api/historical/presidents.get.responses.200.headers.Last-Modified.description
HTTP-date string derived from MAX `updated_at` across rows.

# paths./api/historical/presidents.get.responses.200.headers.X-Dataset-Version.description
MAX `dataset_version` across rows. Mirrors `meta.dataset_version` in the response body for header-only consumers.

# paths./api/historical/presidents.get.responses.200.headers.Vary.description
`Accept-Encoding, User-Agent` — the UA varies the rate-limit lane.

# paths./api/historical/presidents.get.responses.200.headers.X-RateLimit-Lane.description
Which rate-limit lane the request was scored against. `bot` = named AI crawler (10× budget); `default` = everything else.

# paths./api/historical/presidents.get.responses.200.headers.X-RateLimit-Limit.description
Per-minute budget for the chosen lane. `60` (default) or `600` (bot).

# paths./api/historical/presidents.get.responses.200.headers.X-RateLimit-Remaining.description
Best-effort hits remaining in the current window AFTER this call. Capped at zero. `0` does NOT guarantee the next request will 429 — the window may have reset.

# paths./api/historical/presidents.get.responses.200.headers.X-RateLimit-FailedOpen.description
Present (`1`) only when the rate-limit backing store was unreachable and we synthesized an `allowed` result per the fail-OPEN policy. Audit-only — operators can spot fail-open periods in CDN logs.

# paths./api/historical/presidents.get.responses.200.headers.X-RateLimit-Reset.description
ISO-8601 timestamp at which the current lane window closes and the budget resets. Sprint 22 production format; preserved for any consumer parsing it directly. Sprint 23 Task 06 added emission on 200 (not just 429) so LLM consumers can self-throttle without waiting for a 429 wall.

# paths./api/historical/presidents.get.responses.200.headers.X-RateLimit-Reset-Epoch.description
Unix epoch seconds form of `X-RateLimit-Reset` (matches the GitHub / IETF RFC 9331-draft convention). Identical instant, alternative parser path. Both formats emit on every 200 + 429.

# paths./api/historical/presidents.get.responses.304.description
Not Modified. Returned on a conditional `If-None-Match` or `If-Modified-Since` request when the underlying data is unchanged.

# paths./api/historical/presidents.get.responses.400.description
Malformed query parameter (e.g. `per_page` > 100).

# paths./api/historical/presidents.get.responses.429.description
Rate limited. The chosen lane's per-minute budget was exhausted. Includes `Retry-After`, `X-RateLimit-Lane`, `X-RateLimit-Limit`, `X-RateLimit-Remaining: 0`, and `X-RateLimit-Reset` headers so clients can backoff cleanly. Backing-store outages do NOT 429 — they pass through with `X-RateLimit-FailedOpen: 1` on a 200.

# paths./api/historical/presidents.get.responses.429.headers.Retry-After.description
Seconds until the chosen lane's window resets.

# paths./api/historical/presidents.get.responses.429.headers.X-RateLimit-Limit.description
Per-minute budget for the saturated lane.

# paths./api/historical/presidents.get.responses.429.headers.X-RateLimit-Reset.description
ISO-8601 timestamp at which the current window closes and the budget resets.

# paths./api/historical/presidents.get.responses.429.headers.X-RateLimit-Reset-Epoch.description
Unix epoch seconds form of `X-RateLimit-Reset`. Identical instant, GitHub / RFC 9331-draft convention.

# paths./api/historical/presidents/{slug}.get.description
Returns a single row matching the kebab-case slug (e.g. `abraham-lincoln`, `theodore-roosevelt`). Slugs are stable; once published they never change. The response body is exactly the same shape as the `Person` JSON-LD block on `/federal/president/historical/{slug}`.

# paths./api/historical/presidents/{slug}.get.responses.200.headers.ETag.description
Strong validator derived from `dataset_version` + a SHA-256 of the row's material columns.

# paths./api/historical/presidents/{slug}.get.responses.200.headers.Last-Modified.description
HTTP-date string from the row's `updated_at`.

# paths./api/historical/presidents/{slug}.get.responses.200.headers.X-RateLimit-Lane.description
Which rate-limit lane the request was scored against. `bot` = named AI crawler (10× budget); `default` = everything else.

# paths./api/historical/presidents/{slug}.get.responses.200.headers.X-RateLimit-Remaining.description
Best-effort hits remaining in the current window AFTER this call.

# paths./api/historical/presidents/{slug}.get.responses.200.headers.X-RateLimit-FailedOpen.description
Present (`1`) only when the backing store was unreachable and we synthesized an `allowed` result per the fail-OPEN policy.

# paths./api/historical/presidents/{slug}.get.responses.200.headers.X-RateLimit-Reset.description
ISO-8601 timestamp at which the current window closes (Sprint 22 production format).

# paths./api/historical/presidents/{slug}.get.responses.200.headers.X-RateLimit-Reset-Epoch.description
Unix epoch seconds form of `X-RateLimit-Reset` (GitHub / RFC 9331-draft convention).

# paths./api/historical/presidents/{slug}.get.responses.429.headers.Retry-After.description
Seconds until the chosen lane's window resets.

# paths./api/historical/presidents/{slug}.get.responses.429.headers.X-RateLimit-Reset.description
ISO-8601 timestamp at which the current window closes.

# paths./api/historical/presidents/{slug}.get.responses.429.headers.X-RateLimit-Reset-Epoch.description
Unix epoch seconds form of `X-RateLimit-Reset`.

# paths./openapi.json.get.description
Returns this OpenAPI 3.0.3 specification as JSON. Versioned via `info.version`; bumps on spec changes.

# paths./openapi.json.get.responses.200.headers.ETag.description
Derived from the spec content hash + last deploy timestamp.

# paths./openapi.json.get.responses.200.headers.Last-Modified.description
Last deploy timestamp in HTTP-date format.

# components.schemas.Term.description
One presidential term entry. Cleveland-style non-consecutive terms appear as two rows on the same `slug` (Cleveland), or as two separate `slug`s (Trump 1 + Trump 2) — see `dataset_version` lock notes.

# components.schemas.Term.properties.congress.description
Congress number (senators / representatives only; null for presidents).

# components.schemas.Source.description
One provenance record. Mirrors the per-section Citation JSON-LD chain on the rendered detail page.

# components.schemas.Source.properties.scope.description
Which editorial section the citation backs.

# components.schemas.Quote.description
Notable quote (notable_quotes JSONB array entry).

# components.schemas.Position.description
One policy position (key_positions JSONB array entry).

# components.schemas.ExternalIds.description
Foreign authority identifiers. All optional; ingest fills what it finds.

# components.schemas.Person.description
One historical-president row. Mirrors the `Person` JSON-LD block on the detail page, with the operator-only admin fields (`reviewed_at`, `reviewer_notes`, `ingest_anomaly_notes`) excluded.

# components.schemas.Meta.properties.dataset_version.description
MAX `dataset_version` across the rows in this response.

# components.schemas.Meta.properties.generated_at.description
Response generation timestamp. Audit metadata only — downstream consumers should rely on `dataset_version` + row `updated_at` for freshness.

# components.schemas.ErrorResponse.properties.error.properties.message.example
No historical president matches the requested slug.

# x-license-disclosure
Provisional. Sprint 24 will finalize the public-read-API license posture (CC-BY-SA, CC-BY, or custom commercial). Until then, ingest is free with attribution to https://thecandidate.com + per-row canonical URL backlink. See https://thecandidate.com/legal/api-license.