Skip to Content
Hey Town Hall For Businessdocs

Recipes

A meeting digest

Turn one council meeting into a short digest of what was on the agenda and what was decided, with links to the minutes.

View as Markdown

After this recipe you can turn a town's latest council meeting into a plain-text digest: each agenda item in order, its summary, and the motions moved on it with their outcome and vote count.

Uses: GET /v1/meetings and GET /v1/meetings/{id}, two records requests.

How it works

  1. List the town's council meetings, newest first, with kind=council.
  2. Take the first one with minutes_read_at set. A meeting whose minutes are not yet read has its agenda but no motions.
  3. Read that meeting. One request answers with its agenda items, documents, recordings and motions.
  4. Print each agenda item, then the motions whose item_position is that item's position.

The code

Python 3.11 or later
import os

import requests

BASE = "https://data.heytownhall.com"
TOWN = "port-ellery"
HEADERS = {"Authorization": f"Bearer {os.environ['HTD_KEY']}"}


def get(path, params=None):
    res = requests.get(f"{BASE}{path}", params=params, headers=HEADERS, timeout=30)
    body = res.json()
    if not res.ok:
        raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
    return body


meetings = get("/v1/meetings", {"jurisdiction": TOWN, "kind": "council", "limit": 10})["results"]
latest = next(m for m in meetings if m["minutes_read_at"] is not None)
meeting = get(f"/v1/meetings/{latest['id']}")

print(f"{meeting['title']}, {meeting['meeting_date']} ({meeting['jurisdiction_id']})")
print(meeting["source_url"])
for item in meeting["agenda_items"]:
    print()
    print(f"{item['number']} {item['title']}")
    if item["plain_summary"]:
        print(f"   {item['plain_summary']}")
    for motion in meeting["motions"]:
        if motion["item_position"] == item["position"]:
            print(f"   Motion {motion['number']}: {motion['outcome']}, {motion['votes_for']} to {motion['votes_against']}")
            print(f"   Moved by {motion['mover']}, seconded by {motion['seconder']}. {motion['source_url']}")
JavaScript (Node 18 or later)
const BASE = 'https://data.heytownhall.com';
const TOWN = 'port-ellery';
const headers = { Authorization: `Bearer ${process.env.HTD_KEY}` };

async function get(path, params = {}) {
  const url = new URL(path, BASE);
  for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v);
  const res = await fetch(url, { headers });
  const body = await res.json();
  if (!res.ok) throw new Error(`${body.error.code}: ${body.error.message}`);
  return body;
}

const { results: meetings } = await get('/v1/meetings', { jurisdiction: TOWN, kind: 'council', limit: 10 });
const latest = meetings.find((m) => m.minutes_read_at !== null);
const meeting = await get(`/v1/meetings/${latest.id}`);

console.log(`${meeting.title}, ${meeting.meeting_date} (${meeting.jurisdiction_id})`);
console.log(meeting.source_url);
for (const item of meeting.agenda_items) {
  console.log();
  console.log(`${item.number} ${item.title}`);
  if (item.plain_summary) console.log(`   ${item.plain_summary}`);
  for (const motion of meeting.motions.filter((m) => m.item_position === item.position)) {
    console.log(`   Motion ${motion.number}: ${motion.outcome}, ${motion.votes_for} to ${motion.votes_against}`);
    console.log(`   Moved by ${motion.mover}, seconded by ${motion.seconder}. ${motion.source_url}`);
  }
}

What the sandbox prints

Sandbox output
Regular Council Meeting, 2026-08-07 (port-ellery)
https://port-ellery.civicweb.example/meetings/900004

1. Data centre servicing study terms of reference
   Staff ask council to consider: data centre servicing study terms of reference.
   Motion R807-1: carried, 5 to 0
   Moved by Ravi Ostrander, seconded by June Halvorsen. https://port-ellery.civicweb.example/meetings/900004/minutes.pdf

2. 2027 operating budget: first draft
   Staff ask council to consider: 2027 operating budget: first draft.
   Motion R807-2: carried, 3 to 2
   Moved by Ravi Ostrander, seconded by June Halvorsen. https://port-ellery.civicweb.example/meetings/900004/minutes.pdf

3. Active transportation network plan update
   Staff ask council to consider: active transportation network plan update.

4. Rezoning application for 412 Harbour Road to permit a six-storey residential building
   Staff ask council to consider: rezoning application for 412 harbour road to permit a six-storey residential building.

5. Short-term rental licensing bylaw amendments
   Staff ask council to consider: short-term rental licensing bylaw amendments.

Going further

  • Each member's vote is on GET /v1/motions/{id}, one more request a motion.
  • What was said is in the meeting's transcript, GET /v1/meetings/{id}/transcript, billed on the transcripts meter. A recording's has_transcript says whether one is held.
  • Send it on. The digest is plain text, ready for an email or a chat message. Keep the source_url links, so a reader can check each line against the minutes.

On this page