# 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.

```json
{
  "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.

```bash
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](https://data.heytownhall.com/docs/rate-limits).** 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](https://data.heytownhall.com/docs/reference/changes/get-v1-events)

| 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](https://data.heytownhall.com/docs/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.                                                                                       |
