Using the API
The change feed
Read what changed since you last looked. There are no webhooks: you poll the feed.
There are no webhooks. The API never calls you. To learn what changed, read the change feed, GET /v1/events, on a schedule that suits you. In MCP the same feed is the changes_since tool.
How the feed works
Every change written to the data is an event with an ever-growing id. The feed lists events oldest first. Each event names the record that changed, by its family and id, so you know what to read again.
{
"id": "1",
"kind": "meeting.agenda_posted",
"jurisdiction_id": "port-ellery",
"family": "meeting",
"subject_id": "900000",
"occurred_at": "2026-05-03T01:00:00.000Z"
}family is one of meeting, agenda_item, motion, recording, bylaw, post, jurisdiction and topic_tag. Filter the feed to one with kind, as in ?kind=meeting.
Keep your place
- Read the feed once, from the start or with
since, and keep the last event id you read. - Next time, pass that id as
after. You get only the events that came after it. - Follow
next_cursoruntil it isnull, then keep the new last id.
curl "https://data.heytownhall.com/v1/events?after=1" \
-H "Authorization: Bearer $HTD_KEY"import os
import requests
def changes_after(last_id):
params = {"after": last_id, "limit": 100}
headers = {"Authorization": f"Bearer {os.environ['HTD_KEY']}"}
while True:
page = requests.get("https://data.heytownhall.com/v1/events", params=params, headers=headers, timeout=30).json()
if "error" in page:
raise RuntimeError(page["error"]["message"])
for event in page["results"]:
yield event
last_id = event["id"]
if page["next_cursor"] is None:
return
params["cursor"] = page["next_cursor"]Then read each changed record. A meeting, a motion, a bylaw and a jurisdiction each have a detail endpoint, such as GET /v1/meetings/{id}. The others are in their family's list.
Things to know
- Each feed page is one records request, billed like any other list page.
- An answer can be up to 15 seconds old. The feed is cached at the edge that long, so polling faster returns the same page.
- Poll within your rate limit. A sandbox key takes far fewer requests a minute than a live one.
sinceworks on most lists too. It keeps records read at or after an instant, for a sweep of one family without the feed.
The endpoint
GET /v1/events
Read the change feed. Every change written to the data, oldest first, by an ever-growing id. Keep the last id you read and pass it as after (or follow next_cursor) to read what changed since. Billed as one records request a page. Filters: jurisdiction, kind, since, after.
Reference and playground for read the change feed
| Parameter | What it takes |
|---|---|
jurisdiction | A jurisdiction id. |
kind | The family the changed record is in: meeting, agenda_item, motion, recording, bylaw, post, jurisdiction, topic_tag. |
since | Records read at or after this instant: what is new since your last read. |
after | Events after this event id (the last id you read). |
Pages with limit and cursor (pagination).
| Field | Type | What it holds |
|---|---|---|
id | string | The event id: pass as after to read what came next. |
kind | string | |
jurisdiction_id | string or null | |
family | string | Which list holds the subject: meeting, agenda_item, motion, recording, bylaw, post, jurisdiction, topic_tag. |
subject_id | string | |
occurred_at | string | ISO 8601 instant, UTC. |