Publishers
CivicCatch API
Authenticate with a tenant API key and pull upcoming meetings from /api/v1/meetings and the documents an agency publishes between them from /api/v1/documents — official packet links plus a CivicCatch page for the detail. Built for a newsroom home page, not a scrape.
1. Create a key
- Sign in and finish onboarding so you own a preview or active tenant.
- Open your account and create an API key.
- Copy the secret once. CivicCatch stores only a hash.
2. Request meetings
GET https://www.civcatch.com/api/v1/meetings with Authorization: Bearer <key>. Only GET and OPTIONS are served. Responses carry Access-Control-Allow-Origin: * and Cache-Control: no-store: the key is the only thing gating the data, so never put a live key in browser JavaScript — call from your server. Optional ?window=month limits the list to the current Pacific calendar month (upcoming + already held). ?window=week is the same Pacific Sunday–Saturday week as the tenant this-week page. ?window=catalog is every upcoming meeting plus the last 90 days of past meetings — the same window as the tenant government page. Default window=upcoming returns every future meeting.
curl -sS "https://www.civcatch.com/api/v1/meetings?window=month" \ -H "Authorization: Bearer cc_live_YOUR_KEY"
Query parameters
window is the only parameter. Every window is evaluated against the America/Los_Angeles calendar, matching the tenant site. An unrecognised value is not an error — it silently falls back to upcoming, so check your spelling rather than your error handler.
window | What comes back |
|---|---|
upcoming (default) | Every future meeting, through the end of its Pacific calendar day. recent is always []. |
week | The Pacific Sunday–Saturday week containing “now” — the same week as the tenant home page. Meetings already held this week land in recent. |
month | The current Pacific calendar month, split into upcoming and already held. |
catalog | Every upcoming meeting plus the last 90 days of past meetings — the same window as the tenant government page. |
There is no pagination and no cursor: each call returns the whole result set for the window. A tenant catalog is tens of meetings, not thousands.
3. Response
upcoming is sorted soonest first; recent most recent first. Both hold the same meeting object.
{
"ok": true,
"tenant": {
"slug": "bigbear",
"display_name": "Big Bear Valley",
"site_url": "https://bigbear.civcatch.com"
},
"as_of": "2026-08-24T17:04:11.523Z",
"window": "month",
"upcoming": [
{
"id": "0f8f8a1c-1f2c-4a7d-9a1e-2c3b4d5e6f70",
"starts_at": "2026-08-25T01:00:00.000Z",
"has_meeting_time": true,
"body": "BBLDWP",
"body_label": "BBLDWP · Big Bear Lake Department of Water & Power",
"agenda_url": "https://example.gov/agenda.pdf",
"minutes_url": null,
"highlights": ["Vote on water rates"],
"highlights_es": ["Votación sobre las tarifas de agua"],
"decisions": [],
"decisions_es": [],
"agenda_unparsed": false,
"location": "City Hall, 39707 Big Bear Blvd",
"href": "https://bigbear.civcatch.com/meetings/2026-08-24-bbldwp"
}
],
"recent": []
}Top-level fields
ok— alwaystrueon 200. Error bodies carryok: falseand anerrorstring instead.tenant—slug,display_name, andsite_url(the tenant’s canonical subdomain origin) for the key’s tenant.as_of— server time the payload was built, ISO 8601 UTC. Use it as the “last checked” stamp on your page.window— the window actually applied, after the fallback described above. Echo it back rather than trusting your request.upcoming/recent— arrays of meetings.
Meeting fields
id— row id, ornullfor a meeting we hold without one. Not a stable public identifier; key onhrefif you need one.starts_at— ISO 8601 UTC instant. Read it together withhas_meeting_time.has_meeting_time— whentrue, the agency published a clock time andstarts_atis a real instant: render it in America/Los_Angeles. Whenfalse, only the date is known andstarts_atis midnight UTC on that date — render the date alone. Showing “12:00 AM” is the classic bug here.body— the agency key as the source writes it (e.g.BBLDWP).body_label— display label, expanded from the tenant’s body catalog when we have it; otherwise identical tobody.agenda_url/minutes_url— the official documents, ornullwhen not posted. Always link these rather than us.highlights— agenda items we extracted. Always an array: it is[](never omitted, nevernull) when the packet has not been parsed yet.highlights_es— the same agenda items in Spanish, written by the same model call. Always an array:[](never omitted, nevernull) when the packet has not been parsed. Model-generated and unofficial — linkagenda_urlas the authority.decisions— minutes-derived outcomes, also always an array. Empty until minutes are published and parsed; we never infer votes from an agenda.decisions_es— the Spanish decisions, also always an array and empty on the same terms.agenda_unparsed—truewhen a packet exists but we could not read it, which is whyhighlightsis empty.location— free text from the agency notice, ornull. Not geocoded, not normalized.href— absolute URL of the CivicCatch meeting page on the tenant subdomain.
Cancellations are not exposed yet: CivicCatch tracks a meeting status internally and shows cancelled and rescheduled meetings on the tenant site, but /api/v1/meetings does not currently return a status field. Until it does, treat a listed meeting as posted-not-confirmed and link the official agenda.
4. Request documents
GET https://www.civcatch.com/api/v1/documents serves what an agency published between meetings — a rate study, a fee schedule, a public notice. Same Authorization: Bearer <key> header, same Access-Control-Allow-Origin: * and Cache-Control: no-store, same error bodies, and the same 60-requests-per-minute budget — that budget is shared across the whole v1 API, not counted per endpoint, so meetings and documents calls draw on one per-key allowance. There is no pagination here either.
curl -sS "https://www.civcatch.com/api/v1/documents?window=month&body=bbccsd" \ -H "Authorization: Bearer cc_live_YOUR_KEY"
Query parameters
window takes week, month, or all, each evaluated against the America/Los_Angeles calendar. As on the meetings endpoint an unrecognised value is not an error — it silently falls back to all. Note the vocabulary differs from the meetings windows: a document has no future, so there is no upcoming.
window | What comes back |
|---|---|
all (default) | Every document we hold for the tenant, up to a year back — including undated ones. |
week | Documents published in the Pacific Sunday–Saturday week containing “now”. |
month | Documents published in the current Pacific calendar month. |
Plenty of agency PDFs carry no publication date. An undated document — one whose published_at is null — is excluded from week and month entirely and appears only under all, sorted after every dated document. If you are filtering by window, expect to miss undated material.
?body=<slug> narrows the list to one agency by its body slug (e.g. bbccsd). An unknown slug is not an error: it returns an empty documents array.
Response
documents is sorted newest first by published_at.
{
"ok": true,
"tenant": {
"slug": "bigbear",
"display_name": "Big Bear Valley",
"site_url": "https://bigbear.civcatch.com"
},
"as_of": "2026-08-24T17:04:11.523Z",
"window": "month",
"documents": [
{
"id": "4a1f0b2c-7d3e-4f51-9c62-8ab0d1e2f345",
"body": "bbccsd",
"body_label": "BBCCSD · Big Bear City Community Services District",
"kind": "rate-study",
"title": "2026 Water Rate Study",
"summary": "The district proposes a 4% water rate increase beginning in January.",
"summary_es": "El distrito propone un aumento del 4% en las tarifas de agua a partir de enero.",
"highlights": ["Rates rise 4% in January"],
"highlights_es": ["Las tarifas suben 4% en enero"],
"tags": ["rate-change", "water"],
"published_at": "2026-08-20T00:00:00.000Z",
"source_url": "https://example.gov/rate-study.pdf",
"href": "https://example.gov/rate-study.pdf"
}
]
}Document fields
id— row id, ornull. Not a stable public identifier; key onsource_urlif you need one.body— the agency slug the document belongs to, the same value?body=takes. Empty string when the document is not attached to a body in the tenant’s catalog.body_label— display label from the tenant’s body catalog.kind— coarse genre, one ofstaff-report,public-notice,rate-study,consultant-report,press-release, orother. Topic lives intags, so an annual budget isothertaggedbudget.title— the document title as the agency publishes it.summary/summary_es— a resident-readable summary in English and Spanish, ornullwhen the document could not be read.highlights/highlights_es— one to three key points in each language. Always arrays:[](never omitted, nevernull) when the document could not be read.tags— topic labels drawn fromrate-change,fee-hearing,contract-award,public-comment,budget,water,wastewater,solid-waste,fire, andother. Always an array.published_at— ISO 8601 UTC instant the agency published the document, ornullwhen it printed no date. See the undated rule above before you filter on it.source_url— the official document on the agency’s own site. This is the record; link it. It isnullin the rare case the stored link is not a plain http(s) URL, and we never render such a value as a link.href— the canonical URL a reader should open. Today it is identical tosource_url, because no CivicCatch document page exists yet. The field is in the contract so the response shape does not change when that page ships andhrefstarts pointing at it — readhreffor the link andsource_urlfor the record.
Unofficial summary. summary, summary_es, highlights, and highlights_es are model-generated and unofficial — the same posture the tenant meeting pages take. They can be wrong, and they are never a substitute for the document itself: source_url is the authority. Publish them with the source link attached, and never present them as the agency’s own words.
Errors
Both endpoints return the same four errors. Every error body is { "ok": false, "error": "<code>" }.
401 invalid_api_key— missing, malformed, unknown, or revoked key.403 tenant_not_ready— the key’s tenant is neitherpreviewnoractive(still draft, or suspended).429 rate_limited— 60 requests per minute per key, shared across every v1 endpoint (a request to/api/v1/documentsspends the same allowance as one to/api/v1/meetings).500 query_failed— the key and tenant are fine, but the query behind the payload failed. Both endpoints return this rather than an empty200, because an empty list is the honest answer for a quiet week and must never also mean “we could not read the data.” Transient; retry with backoff.503 tenant_unavailable— the key is valid but its tenant record could not be read. Transient; retry with backoff rather than treating the key as dead.