# Building Permits Scraper (Chicago, Austin, Calgary, Seattle) (`steadfetch/building-permits-extractor`) Actor

Unified public building-permit records from Chicago, Austin, Calgary and Seattle: permit number, property address, description, status and issue date in one JSON schema, with date filters and delta mode.

- **URL**: https://apify.com/steadfetch/building-permits-extractor.md
- **Developed by:** [Steadfetch](https://apify.com/steadfetch) (community)
- **Categories:** Lead generation, Business
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.20 / 1,000 building permit delivereds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## Building Permits Scraper (Chicago, Austin, Calgary, Seattle)

Public building-permit records from Chicago, Austin, Calgary and Seattle in one normalized schema. One row per permit with permit number, property address, work description, status and issue date, with common date filters, pagination handled and an optional delta mode for daily monitoring.

### What it does

- Queries the four official city open-data JSON endpoints (Socrata) in a single run and returns **one unified schema** regardless of city.
- Merges Chicago (`ydr8-5enu`), Austin (`3syk-w9eu`), Calgary (`c2es-76ed`) and Seattle (`76t5-zqzr`) into the same fields, so downstream code does not need four parsers.
- Applies `dateFrom` and paginates through SoQL `$limit`/`$offset` while respecting each portal's `Crawl-delay: 1` (at least 1 second between requests).
- Optional **delta mode** (`mode: "delta"`): keeps the newest issue date per city in the Actor key-value store and returns only permits issued after that date. A quiet day delivers no rows and bills 0 results.
- Ships no-code access through Apify plus stable JSON that AI and automation clients can consume directly.

### Sources and compliance

| City | Dataset id | Public endpoint | Licence / terms |
| --- | --- | --- | --- |
| Chicago, IL | `ydr8-5enu` | `https://data.cityofchicago.org/resource/ydr8-5enu.json` | City of Chicago Data Terms of Use: derivative applications must include the disclaimer below and attribution to `www.cityofchicago.org`. |
| Austin, TX | `3syk-w9eu` | `https://data.austintexas.gov/resource/3syk-w9eu.json` | Public domain with an attribution request and a no-re-identification clause. |
| Calgary, AB | `c2es-76ed` | `https://data.calgary.ca/resource/c2es-76ed.json` | Open Government Licence - City of Calgary v2.1: commercial use allowed with attribution. |
| Seattle, WA | `76t5-zqzr` | `https://data.seattle.gov/resource/76t5-zqzr.json` | Public Domain, City of Seattle (attribution given below). The portal has no separate terms page. |

All four portals allow `/resource/*` in `robots.txt` and declare `Crawl-delay: 1`. This Actor only calls those JSON endpoints, with a descriptive User-Agent that identifies Steadfetch, no login, no token, no CAPTCHA bypass, no browser and no proxies. The HTML front ends (including `abc.austintexas.gov`) are never requested.

**No personal data.** Applicant, owner and contractor contact fields present in the raw datasets (`contact_*`, `applicantname`, `contractorname`) are never read or emitted. Only public permit facts are returned, and Austin's no-re-identification clause is respected.

#### Chicago disclaimer (required by the City of Chicago Data Terms of Use)

> This site provides applications using data that has been modified for use from its original source, www.cityofchicago.org, the official website of the City of Chicago. The City of Chicago makes no claims as to the content, accuracy, timeliness, or completeness of any of the data provided at this site. The data provided at this site is subject to change at any time. It is understood that the data provided at this site is being used at one's own risk.

#### Attribution

- Contains information from the City of Chicago Open Data Portal (dataset `ydr8-5enu`), `www.cityofchicago.org`.
- Data provided by the City of Austin Open Data Portal (dataset `3syk-w9eu`).
- Contains information licensed under the Open Government Licence - City of Calgary (dataset `c2es-76ed`).
- Data provided by the City of Seattle Open Data Portal (dataset `76t5-zqzr`), public domain.

### Input

| Field | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `cities` | array | Yes | `["chicago"]` (prefill); all four if omitted | `chicago`, `austin`, `calgary`, `seattle`. |
| `dateFrom` | string | No | empty | ISO date (`YYYY-MM-DD`) lower bound on issue date. |
| `status` | string | No | empty | Case-insensitive exact match on the status reported by each city (`ACTIVE`, `Active`, `Completed`, ...). |
| `maxItems` | integer | No | `1000` | Maximum permits pushed to the dataset (1-10000). |
| `mode` | string | No | `full` | `full` or `delta`. |
| `strictSchema` | boolean | No | `true` | Fail the run when a row does not match the output schema. Keep enabled. |

#### Example input

Matches the declared `exampleRunInput`:

```json
{
  "cities": ["chicago"],
  "maxItems": 25,
  "mode": "full"
}
```

#### Delta mode

```json
{
  "cities": ["chicago", "austin", "calgary", "seattle"],
  "mode": "delta",
  "maxItems": 1000
}
```

The first delta run returns the selected range and saves the newest `issue_date` per city under `building-permits-extractor-state`. Later runs request `issue_date > lastIssueDate` for each city, so unchanged days deliver 0 rows and cost only the run start. Dates are day-granular, matching the source datasets.

### Output

#### Example output

```json
{
  "url": "https://data.cityofchicago.org/resource/ydr8-5enu.json?$where=id%3D'N2998605'",
  "title": "B200517005 - 3740 N SAYRE AVE",
  "scrapedAt": "2026-10-24T19:57:00.000Z",
  "city": "chicago",
  "permit_number": "B200517005",
  "property_address": "3740 N SAYRE AVE",
  "description": "INSTALL SOLAR PANELS ON ROOF OF ACCESSORY BUILDING. ROOF TYPE: STEEP (>5:12).",
  "status": "ACTIVE",
  "issue_date": "2026-10-23",
  "record_url": "https://data.cityofchicago.org/resource/ydr8-5enu.json?$where=id%3D'N2998605'",
  "source_dataset": "ydr8-5enu"
}
```

#### Output fields

| Field | Type | Description |
| --- | --- | --- |
| `url` | string | Public JSON row permalink used as the source URL. |
| `title` | string | Permit number plus property address. |
| `scrapedAt` | date-time | ISO 8601 UTC timestamp of the run. |
| `city` | string | `chicago`, `austin`, `calgary` or `seattle`. |
| `permit_number` | string | Permit number as published by the city. |
| `property_address` | string or null | Street address, without contact data. |
| `description` | string or null | Public work description. |
| `status` | string or null | Status published by the city. Chicago publishes it only for newer permits. |
| `issue_date` | string or null | Issue date (`YYYY-MM-DD`). |
| `record_url` | string | Public JSON permalink for the permit. |
| `source_dataset` | string | Socrata dataset id. |

New fields are additive and required fields never change, so downstream code can rely on the documented contract.

### Pricing

Pay-per-event with the two synthetic platform events:

- `apify-actor-start`: 0.00005 USD per run (Apify-recommended default, includes the first 5 seconds of compute).
- `apify-default-dataset-item`: **0.004 USD per row = 4.00 USD per 1,000 results**.

There is no custom per-item event, so users are never billed twice for the same permit. Rows rejected by the schema are never pushed and never charged.

Measured cost is about **0.001 USD per 1,000 permits** (JSON endpoints, no browser, no proxies, one request per 1,000 rows plus the 1 s crawl delay). The price keeps a wide margin above cost and is aligned with the closest Store competitors (leader about 11 users in 30 days at a comparable price).

**Works on the Apify free plan**: with the 5 USD monthly credit, about **1,249 permits** can be delivered per month (N = floor((5.00 - 0.00005) / 0.004)).

### Usage with the API

```bash
curl -X POST "https://api.apify.com/v2/acts/steadfetch~building-permits-extractor/runs?token=<YOUR_API_TOKEN>" \
  -H 'Content-Type: application/json' \
  -d '{"cities":["chicago","austin"],"dateFrom":"2026-10-01","maxItems":100}'
```

Then read the default dataset of the run.

### FAQ

**Do I need an API key for the city portals?** No. All four are anonymous public Socrata endpoints. The Actor sends only a descriptive User-Agent and respects each portal's crawl delay.

**Are the four cities really the same schema?** Yes. `city`, `permit_number`, `property_address`, `description`, `status`, `issue_date`, `record_url` and `source_dataset` are normalized. Fields that a city does not publish are `null`, never invented.

**Why is `status` null for some Chicago rows?** Chicago publishes `permit_status` only for newer permits; older rows have no status. The Actor reports `null` instead of guessing.

**Can I filter by status across cities?** Yes, but status vocabularies differ by city (`ACTIVE` in Chicago, `Active` in Austin, `Completed` in Calgary, `Issued` in Seattle). `status` is a case-insensitive exact match against the city's own value.

**What does delta mode bill?** Only delivered permits. The first run delivers the selected range; later runs deliver only permits with an issue date newer than the last delivered date, so a quiet day bills 0 results.

**Does the Actor use a browser?** No. The sources are JSON APIs, so HTTP is roughly 10x cheaper than a browser.

**Is the output schema stable?** Yes. New fields are added additively and required fields never change.

### Maintenance

The Actor is **tested daily** through the Steadfetch catalogue QA and the monorepo CI (`npm test` and `npm run publish:check` on every push). Repairs take priority over new features. Schema changes are announced through repository releases.

### Support

Report bugs or request features through the Apify Store page (Issues tab) of this Actor, or through the Steadfetch actors repository issues. Include the run ID and the input used.

# Actor input Schema

## `cities` (type: `array`):

Cities to query. Each city is a separate public Socrata dataset. Duplicates are removed.

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

Only permits issued on or after this ISO date (YYYY-MM-DD). Leave empty for no lower bound.

## `status` (type: `string`):

Optional case-insensitive exact match on the status reported by each city (for example ACTIVE in Chicago, Active in Austin, Completed in Calgary, Issued in Seattle). Leave empty for all statuses.

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

Maximum number of permits pushed to the dataset.

## `mode` (type: `string`):

full returns permits for the selected range. delta returns only permits issued after the newest date delivered in the previous delta run (state is kept per city in the Actor key-value store).

## `strictSchema` (type: `boolean`):

Fail the run when a row does not match the output schema. Keep enabled: Steadfetch never publishes Actors without output-schema validation.

## Actor input object example

```json
{
  "cities": [
    "chicago"
  ],
  "maxItems": 25,
  "mode": "full",
  "strictSchema": true
}
```

# Actor output Schema

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

Dataset items validated against the item schema in dataset_schema.json (fields).

# 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 = {
    "cities": [
        "chicago"
    ],
    "maxItems": 25,
    "mode": "full"
};

// Run the Actor and wait for it to finish
const run = await client.actor("steadfetch/building-permits-extractor").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 = {
    "cities": ["chicago"],
    "maxItems": 25,
    "mode": "full",
}

# Run the Actor and wait for it to finish
run = client.actor("steadfetch/building-permits-extractor").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 '{
  "cities": [
    "chicago"
  ],
  "maxItems": 25,
  "mode": "full"
}' |
apify call steadfetch/building-permits-extractor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,steadfetch/building-permits-extractor"
        }
    }
}
```

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/LRhjNoArBuwfcya2c/builds/8q4NQsGTgihH7fVbs/openapi.json
