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

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`](https://data.heytownhall.com/docs/reference/documents/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
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")
```

```js
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`:

```bash
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

```csv
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](https://data.heytownhall.com/docs/change-feed).
- **The terms a bylaw defines** are in [`GET /v1/defined-terms`](https://data.heytownhall.com/docs/reference/documents/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.
