CivicRecap for developers and agents
Last updated August 23, 2026
CivicRecap publishes a machine-readable record of California civic meetings — city councils, school boards, planning commissions and county boards. Everything on this page answers an anonymous caller: no key, no account, no sign-up.
The public API
The base URL is this origin. Responses are JSON, and a field with no value is omitted rather than sent as null.
| Endpoint | Answers |
|---|---|
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 /openapi.json — OpenAPI 3, generated from the routes themselves, so it cannot drift from what is mapped. Public reads are rate-limited per client address (300 requests a minute at the default setting) and answer 429 past it.
curl -s https://www.civicrecap.com/api/meetings/browse?page=1Errors are one shape, on every route:
{
"error": "not_found",
"message": "No meeting with that id or slug.",
"hint": "Find one with GET /api/meetings/browse, or read /sitemap.xml."
}What needs an account
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 plus the page's path — /md/browse, /md/developers (this page). 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}, the councils and boards that serve it at /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 here, 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
/auth.md— Auth.md agent-registration discovery: the credentials, how each is obtained, and why there is no OAuth to negotiate./.well-known/api-catalog— The API catalog (RFC 9727): a linkset pointing at the spec, these docs and the health endpoint./openapi.json— The full OpenAPI 3 description of the public read surface, generated from the routes themselves./llms.txt— What this site is, what is public and where everything machine-readable lives./sitemap.xml— Every public URL: places, councils and boards, meetings./robots.txt— Crawling rules. Everything public is allowed.
Using the record
The meetings are public records and the briefings and clips are made from them by machine. Every generated sentence is traced back to what it rests on before it is published, and anything that cannot be is dropped — but a briefing is a summary, not a transcript, and the terms say what may be relied on. Questions, corrections and anything that looks wrong: [email protected].