Skip to Content
Hey Town Hall For Businessdocs

Recipes

Export a town's bylaws

Write every bylaw a town has published to a CSV file, page by page, with a link to each one.

View as Markdown

After this recipe you can write one town's bylaws to bylaws.csv, with each bylaw's number, title, kind, subject, adoption date and link to the town's own copy.

Uses: GET /v1/bylaws, one records request a page of up to 100.

How it works

  1. List the town's bylaws with jurisdiction set, 100 to a page.
  2. Write each page's rows, then follow next_cursor until it is null.

To narrow the export, add subject (such as zoning or fees_charges) or kind (bylaw, amendment, consolidated or repeal). The text of each bylaw is at its source_url.

The code

Python 3.11 or later
import csv
import os

import requests

BASE = "https://data.heytownhall.com"
TOWN = "marlow-county"
FIELDS = ["number", "title", "kind", "subject", "adopted_on", "source_url", "read_at"]
HEADERS = {"Authorization": f"Bearer {os.environ['HTD_KEY']}"}

params = {"jurisdiction": TOWN, "limit": 100}
rows = 0
with open("bylaws.csv", "w", newline="", encoding="utf-8") as out:
    writer = csv.DictWriter(out, fieldnames=FIELDS, extrasaction="ignore")
    writer.writeheader()
    while True:
        res = requests.get(f"{BASE}/v1/bylaws", params=params, headers=HEADERS, timeout=30)
        page = res.json()
        if not res.ok:
            raise RuntimeError(f"{page['error']['code']}: {page['error']['message']}")
        writer.writerows(page["results"])
        rows += len(page["results"])
        if page["next_cursor"] is None:
            break
        params["cursor"] = page["next_cursor"]

print(f"Wrote {rows} bylaws to bylaws.csv")
JavaScript (Node 18 or later)
import { writeFileSync } from 'node:fs';

const BASE = 'https://data.heytownhall.com';
const TOWN = 'marlow-county';
const FIELDS = ['number', 'title', 'kind', 'subject', 'adopted_on', 'source_url', 'read_at'];
const headers = { Authorization: `Bearer ${process.env.HTD_KEY}` };

const cell = (v) => (v === null || v === undefined ? '' : /[",\n]/.test(String(v)) ? `"${String(v).replaceAll('"', '""')}"` : String(v));
const lines = [FIELDS.join(',')];

let cursor = null;
do {
  const url = new URL('/v1/bylaws', BASE);
  url.searchParams.set('jurisdiction', TOWN);
  url.searchParams.set('limit', '100');
  if (cursor) url.searchParams.set('cursor', cursor);
  const res = await fetch(url, { headers });
  const page = await res.json();
  if (!res.ok) throw new Error(`${page.error.code}: ${page.error.message}`);
  for (const bylaw of page.results) lines.push(FIELDS.map((f) => cell(bylaw[f])).join(','));
  cursor = page.next_cursor;
} while (cursor);

writeFileSync('bylaws.csv', `${lines.join('\n')}\n`);
console.log(`Wrote ${lines.length - 1} bylaws to bylaws.csv`);

One page from the shell, with jq:

curl and jq
curl -s "https://data.heytownhall.com/v1/bylaws?jurisdiction=marlow-county&limit=100" \
  -H "Authorization: Bearer $HTD_KEY" \
  | jq -r '["number","title","kind","subject","adopted_on","source_url"], (.results[] | [.number, .title, .kind, .subject, .adopted_on, .source_url]) | @csv'

What the sandbox writes

bylaws.csv from the sandbox
number,title,kind,subject,adopted_on,source_url,read_at
1503,Water Utility Bylaw,bylaw,other,2026-08-13,https://marlowcounty.example/bylaws/400013.pdf,2026-10-30T16:00:00.000Z
1502,"Fees and Charges Bylaw, Consolidated",consolidated,fees_charges,2026-07-13,https://marlowcounty.example/bylaws/400012.pdf,2026-10-30T16:00:00.000Z
1501,Short-Term Rental Regulation Amendment Bylaw,amendment,business_licence,2026-06-13,https://marlowcounty.example/bylaws/400011.pdf,2026-10-30T16:00:00.000Z
1500,Zoning Bylaw,bylaw,zoning,2026-05-13,https://marlowcounty.example/bylaws/400010.pdf,2026-10-30T16:00:00.000Z

Going further

  • Keep it current with since: since=2026-11-01T00:00:00Z lists only the bylaws read at or after that instant. Or follow bylaw.seen events in the change feed.
  • The terms a bylaw defines are in GET /v1/defined-terms, with the start of each definition.
  • Every town at once: leave out jurisdiction and set region=BC. Add jurisdiction_id to the columns.

On this page