# CivicRecap for developers and agents

CivicRecap publishes a machine-readable record of California civic
meetings: city councils, school boards, planning commissions and county
boards. Everything below answers an anonymous caller — no key, no
account, no sign-up — and the base URL is this origin,
`https://www.civicrecap.com`.

## The API

- `GET /api` — This API, described: what it is and where the spec is.
- `GET /api/places` — Every city and county with meetings on the record.
- `GET /api/places/{slug}` — One place, and the councils and boards that meet about it.
- `GET /api/agencies` — Every council, board and commission on the record.
- `GET /api/agencies/{slug}` — One council or board.
- `GET /api/in/{place}/{body}` — The same profile, addressed the way the site's URLs are.
- `GET /api/meetings/browse` — Meetings on the record, paged and filterable.
- `GET /api/meetings/{id}` — One meeting. Signed out this is the teaser — title, council or board, date, agenda items, published clip headlines — marked "gated": true.
- `GET /api/upcoming` — The next meetings of a followed place or body, inside 30 days.
- `GET /api/search/locations` — One field: an address, a city, a county, a ZIP or the name of a body.
- `GET /api/version` — The build this API is running.
- `GET /health` — Liveness, including the database.

The complete description is https://www.civicrecap.com/openapi.json (OpenAPI 3,
generated from the routes themselves, so it cannot drift from what is
mapped). Responses are JSON; absent fields are omitted rather than sent as
`null`. Errors are one shape on every route:

```json
{
  "error": "not_found",
  "message": "No meeting with that id or slug.",
  "hint": "Find one with GET /api/meetings/browse, or read /sitemap.xml."
}
```

Public reads are rate-limited per client address (300 requests a minute at
the default setting), and answer `429` past it.

## What needs an account, and what is never handed over

Finding a meeting is public; reading one is not. `GET /api/meetings/{id}`
answers an anonymous caller with a redacted teaser — the title, the
council or board, the date, the agenda items and the published clip
headlines — marked `"gated": true`. The recording, the written briefing
and the transcript need either a session cookie (`cr_session`, minted by
signing in) or a share grant: `?st=` on a link a signed-in reader passed
on, good for that one meeting for 14 days.

The spec declares this where a machine can read it: every gated
operation carries `security` against the named schemes in
`components.securitySchemes` — `session`, `share_grant`,
`media_signature` and `email_graphic` (what a figure carries when it was
mailed rather than rendered on a page). A fifth, `agent_token`, is not
part of this surface at all: it is a bearer token an operator mints by
hand for the read-only operational tier at `/api/agent/*`, and nothing
here issues one. There is no OAuth authorization server and no token
endpoint.
An operation declaring no `security` answers an anonymous caller in full.

There are no downloads, for anyone. Posters are open, so a share card can
be unfurled; a rendered clip, the web rendition, our copy of the agenda
and a briefing figure are signed per visit and expire, and the signature
is minted by `GET /api/meetings/{id}/clips`, `/video`, `/agenda` and
`/graphics` rather than assembled from a key. The recording stays with
the council or board that made it.

## Reading the site as markdown

Every public page answers `Accept: text/markdown` with a prose version of
itself, and the same document has an address of its own at `/md` + the
page's path — `/md/browse`, `/md/in/{place}`, `/md/developers` (this
document). Ask for the `/md` address when a cache sits between us: the
markdown answer carries `Vary: Accept`, but a shared cache that ignores
`Vary` cannot tell the two representations of a page URL apart.

Pages are addressed place-first: `/in/{place}`, `/in/{place}/{body}`, and
a meeting at `/in/{place}/{body}/{date}-{id}`. The older flat spellings
(`/places/{slug}`, `/agencies/{slug}`, `/meetings/{slug}`) are answered
forever and redirect, so a link of either shape resolves. A URL naming
nothing answers a real `404`, never a `200` carrying an app shell.

## Machine-readable files

- https://www.civicrecap.com/auth.md — Auth.md agent-registration discovery: the credentials, how each is obtained, and why there is no OAuth to negotiate.
- https://www.civicrecap.com/.well-known/api-catalog — The API catalog (RFC 9727): a linkset pointing at the spec, these docs and the health endpoint.
- https://www.civicrecap.com/openapi.json — The full OpenAPI 3 description of the public read surface, generated from the routes themselves.
- https://www.civicrecap.com/llms.txt — What this site is, what is public and where everything machine-readable lives.
- https://www.civicrecap.com/sitemap.xml — Every public URL: places, councils and boards, meetings.
- https://www.civicrecap.com/robots.txt — Crawling rules. Everything public is allowed.

## Elsewhere

- Browse every public body: https://www.civicrecap.com/browse
- Site index: https://www.civicrecap.com/sitemap.xml
- For agents: https://www.civicrecap.com/llms.txt
- API description: https://www.civicrecap.com/openapi.json
- Developer and agent documentation: https://www.civicrecap.com/developers
