Skip to Content
Hey Town Hall Data docs

Using the API

The change feed

Read what changed since you last looked. There are no webhooks: you poll the feed.

View as Markdown

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.

One event (sandbox, fictional)
{
  "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

  1. Read the feed once, from the start or with since, and keep the last event id you read.
  2. Next time, pass that id as after. You get only the events that came after it.
  3. Follow next_cursor until it is null, then keep the new last id.
What changed after event 1
curl "https://data.heytownhall.com/v1/events?after=1" \
  -H "Authorization: Bearer $HTD_KEY"
Python
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.
  • since works 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

Filters
ParameterWhat it takes
jurisdictionA jurisdiction id.
kindThe family the changed record is in: meeting, agenda_item, motion, recording, bylaw, post, jurisdiction, topic_tag.
sinceRecords read at or after this instant: what is new since your last read.
afterEvents after this event id (the last id you read).

Pages with limit and cursor (pagination).

Fields of each result, GET /v1/events
FieldTypeWhat it holds
idstringThe event id: pass as after to read what came next.
kindstring
jurisdiction_idstring or null
familystringWhich list holds the subject: meeting, agenda_item, motion, recording, bylaw, post, jurisdiction, topic_tag.
subject_idstring
occurred_atstringISO 8601 instant, UTC.

On this page