# City Council Minutes (`publicrecords/legistar-meetings-scraper`) Actor

City Council Minutes — scrape US city council meetings, agendas, and minutes from public Legistar clients. Multi-city → flat JSON via official Web API.

- **URL**: https://apify.com/publicrecords/legistar-meetings-scraper.md
- **Developed by:** [Marc M](https://apify.com/publicrecords) (community)
- **Categories:** Business, Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 result rows

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

### What does Legistar Meetings Scraper do?

**Legistar Meetings Scraper** pulls **public municipal council meetings, agendas, minutes links, and legislation matters** from the official [Granicus Legistar Web API](https://webapi.legistar.com/Help) — plain HTTP JSON, **no browser**, **no login**.

Pass one or many **Legistar client slugs** (the `{slug}` in `https://{slug}.legistar.com`) and get flat dataset rows ready for PropTech pipelines, zoning intel, lobbying monitors, civic journalism, and compliance watches.

> **Disclaimer:** This Actor is **not affiliated with, endorsed by, or sponsored by Granicus, Legistar, or any municipality**. It only requests records the API marks as **public / InSite-visible**.

#### Why this Actor?

- 🏙 **Multi-city in one run** — `clients: ["seattle","milwaukee","boston"]`
- 📡 **Official Web API** — `webapi.legistar.com/v1/{Client}/…`, not brittle HTML
- 📋 **Meetings + matters** — events (agendas/minutes) and/or legislation matters
- 🧾 **Flat JSON** — one row per event/matter; optional flattened agenda titles
- 🐢 **Polite throttling** — short delay between clients and paginated requests; identifiable User-Agent
- ⏱ **Apify platform** — schedules, webhooks, API, integrations, monitoring
- 💵 **Simple pricing** — **$0.002 per successful result** (+ tiny Actor-start fee). **Error rows are not charged.**

#### What data can you extract?

| Field group | Examples |
|-------------|----------|
| Meeting meta | `bodyName`, `eventDate`, `eventTime`, `location`, `agendaStatus`, `minutesStatus` |
| Documents | `agendaUrl`, `minutesUrl`, `videoStatus`, `inSiteUrl` |
| Legislation | `matterFile`, `title`, `typeName`, `statusName`, intro/agenda/passed dates |
| Ops | `client`, `eventId` / `matterId`, `lastModifiedUtc`, `recordType` |

***

### How to scrape city council agendas with Legistar

1. Open **Legistar Meetings Scraper** on Apify Store (or Console).
2. Set **Client slugs** — e.g. `seattle` for `seattle.legistar.com`.
3. Optionally set **From / To dates** (YYYY-MM-DD). Defaults: last 30 days → +14 days.
4. Choose **Resource**: Meetings (`events`), Matters, or Both.
5. Set **Max items** and run.
6. Download the dataset (JSON/CSV/Excel) or pull via Apify API.

#### How to find the client slug

Open the city’s Legistar site. The hostname label is the slug:

| City site | Slug |
|-----------|------|
| https://seattle.legistar.com | `seattle` |
| https://milwaukee.legistar.com | `milwaukee` |
| https://boston.legistar.com | `boston` |

**Known-good (Week-0 probes):** seattle, milwaukee, madison, oakland, sanjose, boston, denver, columbus.\
**Known issues:** `chicago` → HTTP 500 (InSite connection); `phila` → 403. Failures are logged / written to a non-billed `errors` dataset; the default dataset stays success-only.

***

### Input

| Field | Required | Default | Description |
|-------|----------|---------|-------------|
| `clients` | **yes** | `["seattle"]` | Array of Legistar client slugs |
| `dateFrom` | no | 30 days ago | Inclusive YYYY-MM-DD |
| `dateTo` | no | today + 14d | Inclusive YYYY-MM-DD |
| `maxItems` | no | `50` | Cap on dataset rows (1–5000) |
| `resource` | no | `events` | `events` | `matters` | `both` |
| `includeEventItems` | no | `false` | Extra GET per event; flatten titles into `eventItemTitles` |

#### Example input (single city)

```json
{
  "clients": ["seattle"],
  "dateFrom": "2026-08-01",
  "dateTo": "2026-10-01",
  "maxItems": 50,
  "resource": "events",
  "includeEventItems": false
}
```

#### Example input (multi-city)

```json
{
  "clients": ["seattle", "milwaukee", "boston"],
  "dateFrom": "2025-09-16",
  "dateTo": "2026-10-16",
  "maxItems": 1200,
  "resource": "events"
}
```

Stress-validated (private run): **1169** events across those three cities.

***

### Sample output

Real fields from last-known-good canary (`seattle`):

```json
{
  "recordType": "event",
  "client": "seattle",
  "eventId": 6883,
  "eventGuid": "850BF790-9660-42F4-8FD4-B501956F4710",
  "bodyId": 279,
  "bodyName": "Human Services, Labor, and Economic Development Committee",
  "eventDate": "2026-09-18T00:00:00",
  "eventTime": "9:30 AM",
  "location": "Council Chamber, City Hall\r\n600 4th Avenue\r\nSeattle, WA 98104",
  "agendaStatus": "Final",
  "minutesStatus": "Draft",
  "agendaUrl": "https://legistar2.granicus.com/seattle/meetings/2026/9/6883_A_Human_Services%2C_Labor%2C_and_Economic_Development_Committee_26-09-18_Committee_Agenda.pdf",
  "minutesUrl": null,
  "videoStatus": "Public",
  "inSiteUrl": "https://seattle.legistar.com/MeetingDetail.aspx?LEGID=6883&GID=393&G=FFE3B678-CEF6-4197-84AC-5204EA4CFC0C",
  "comment": null,
  "lastModifiedUtc": "2026-09-16T16:02:48.8",
  "eventItemCount": 0,
  "eventItemTitles": ""
}
```

Default-dataset `recordType` is `event` or `matter`. Diagnostics use a separate non-billed `errors` dataset.

***

### How much does it cost to scrape Legistar meetings?

**Pay-per-event pricing:**

| Event | Price |
|-------|-------|
| Actor start | ~$0.00005 (platform default) |
| Dataset item (**successful** meeting / matter only) | **$0.002** |
| Error / diagnostic rows | **Not charged** |

**Examples**

| Run | Approx. cost |
|-----|----------------|
| 1 start + 100 meetings | ≈ **$0.20** |
| 1 start + 1,000 meetings | ≈ **$2.00** |
| Stress-scale 1,169 events (3 cities) | list ≈ **$2.34** at $0.002 |

Free Apify plan users can try small runs within platform free credits. No residential proxy required for normal public clients. Actor applies **polite throttling** between clients/requests.

***

### Use cases

- 🏗 **PropTech / zoning intel** — track planning, land-use, and council briefings across markets
- 📣 **Lobbying & public affairs** — monitor committee calendars and agenda packet links
- 📰 **Journalism** — structured feeds of upcoming/cancelled meetings and minutes status
- ⚖️ **Civic / FOIA workflows** — inventory public meetings without hand-copying InSite
- 🤖 **AI agents** — schedule the Actor, webhook new `event` rows into your stack

***

### Limitations

- **Public clients only** — cities that require an API `token` are out of scope for v1
- **Not every US city uses Legistar** — CivicPlus / PrimeGov / other portals need different Actors
- **OData page size** — API caps responses; Actor pages with `$top` / `$skip`
- **PDF contents** — we return `agendaUrl` / `minutesUrl`; we do **not** OCR PDFs in v1
- **Some slugs fail** — e.g. chicago 500 / phila 403; diagnostics are non-billed; run still completes

***

### Legal & fair use

- Data source: official **Legistar Web API** (`webapi.legistar.com`) public GET endpoints.
- Only InSite-public meeting/legislation metadata.
- No login, no scraping of gated clerk portals, no harvesting emails/phones from free text.
- **Not affiliated with Granicus, Legistar, or any municipality.**
- Polite request pacing (throttle between clients and pages).
- You are responsible for how you reuse public records in your jurisdiction.

API reference: https://webapi.legistar.com/Home/Examples

***

### FAQ

#### Is this an official Granicus product?

No. It is an independent Apify Actor that calls the documented public Web API. **Not affiliated with Granicus or Legistar.**

#### Can I scrape multiple cities at once?

Yes — put every slug in `clients[]`. The Actor throttles politely between clients.

#### Do I need proxies?

Usually no for public token-free clients. Use Apify proxy only if you hit rate limits.

#### Are error rows charged?

**No.** Only successful default-dataset rows (`event` / `matter`) bill under PPE.

#### Matters vs events?

`events` = meetings (agendas/minutes). `matters` = legislation files. Use `both` when you need both collections under one `maxItems` budget.

#### How do I automate this?

Use Apify **Schedules**, **Integrations**, or the Apify API / Python/JS client to run on a cron and push to your warehouse.

***

### SEO keywords

legistar scraper, legistar api, city council agenda scraper, municipal meetings scraper, granicus legistar, council minutes api, seattle legistar, municipal legislation scraper, public meeting records, zoning meeting monitor, proptech civic data

# Actor input Schema

## `clients` (type: `array`):

Legistar client codes. Use the hostname label of {slug}.legistar.com. Working examples: seattle, milwaukee, madison, oakland, sanjose, boston, denver, columbus.

## `dateFrom` (type: `string`):

Inclusive YYYY-MM-DD (EventDate / MatterIntroDate). Default: 30 days ago.

## `dateTo` (type: `string`):

Inclusive YYYY-MM-DD. Default: today + 14 days.

## `maxItems` (type: `integer`):

Cap on dataset rows (events + matters combined).

## `resource` (type: `string`):

Which Legistar collection to pull.

## `includeEventItems` (type: `boolean`):

Extra HTTP call per event. Titles flattened into eventItemTitles (pipe-separated). Default off.

## Actor input object example

```json
{
  "clients": [
    "seattle"
  ],
  "maxItems": 50,
  "resource": "events",
  "includeEventItems": false
}
```

# Actor output Schema

## `results` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "clients": [
        "seattle"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("publicrecords/legistar-meetings-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "clients": ["seattle"] }

# Run the Actor and wait for it to finish
run = client.actor("publicrecords/legistar-meetings-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "clients": [
    "seattle"
  ]
}' |
apify call publicrecords/legistar-meetings-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,publicrecords/legistar-meetings-scraper"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/i2i7vnKnDjv5Ahg6G/builds/aATSn93KZHsaxKtn7/openapi.json
