# Chicago Building Permits - Contractors, Costs & Timeline (`j0401/chicago-permits`) Actor

Chicago building permits (public open data, 846k records, live): the city's full permit register with the contractor / owner / architect behind each job, the fee schedule and reported cost, and the application-to-issue timeline. Filter by type/status/contractor/cost/ward.

- **URL**: https://apify.com/j0401/chicago-permits.md
- **Developed by:** [Wenhao Yang](https://apify.com/j0401) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$0.10 / 1,000 chicago permit records

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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Chicago Building Permits - Contractors, Costs & Full Timeline

Chicago publishes its **entire building-permit register** - every permit the city has issued since 2006, across every lane it processes - as open data. This actor turns that register into a **charged-per-record lookup, filter and aggregate tool**: find the permits a contractor has pulled, list the jobs above a cost threshold, pull this month's issuances, or read the register by ward.

**Built for:** contractors and suppliers researching a competitor or a market, developers and GCs sizing a pipeline, and anyone doing property or market diligence who needs the **party behind the permit**, not just the address.

### What it covers

**846,660 permits** - the city's full file, one row per permit, spanning every processing lane:

| Lane | Permits |
|---|---|
| Express Permit Program | 323,001 |
| Easy Permit Process | 207,655 |
| Renovation / Alteration | 167,499 |
| Signs | 54,985 |
| New Construction | 31,882 |
| Elevator (Conveyance Device) | 22,293 |
| Wrecking / Demolition | 21,903 |
| Scaffolding / Porch / Reinstate / Extension | ~17,400 |

Each permit carries the **parties behind the job** - the city's contact slots resolved to named roles:

- **contractor** (general / electrical / plumbing / masonry / wrecking / sign / elevator / ventilation / ...) - named on **~98% of the file**
- **owner** and **owner-as-general-contractor**
- **architect**, engineer, and expeditor
- the **full contact list** on the record (role, name, city, state, ZIP per party)

plus the **fee schedule** (total fee, reported project cost, paid / unpaid / waived subtotals - cost and fee are real numeric columns, so they **range-filter**), the **application -> issue timeline** (application date, issue date, processing time), the work description, and location (**street address, community area, census tract, ward, PIN, latitude/longitude**).

### The fine print that matters

Chicago's permit status is **blank by design on the express, easy, sign, elevator and scaffolding lanes** - the city does not status-track them, so a third of the register carries no status word. Those permits are live and complete; the `status` filter matches `blank` as its own explicit value, so you can pull them deliberately instead of losing them.

The city's role vocabulary overlaps on purpose: **"OWNER AS GENERAL CONTRACTOR"** names a party who is both, so a `contractor` search and an `owner` search both return those jobs, and the record's `contractor` / `owner` fields may resolve to the same person. That is the city's own classification, kept verbatim - the full **`contacts`** list on every record carries each party's exact role if you need to separate them.

### Typical questions

- "Every permit **contractor X** has pulled - and where."
- "New-construction permits over **$5M** issued this year."
- "This **month's issuances** by type and ward."
- "Permits in **ward 42** / community area **49**."
- "Signs, demolitions and the **blank-status express lane**."
- "Aggregate the register by **status**, **permit type** or **ward**."

### Inputs

| Input | What it does |
|---|---|
| `mode` | `rows` (default) / `recent` (last 30 days) / `aggregate` |
| `permitType` / `status` / `reviewType` / `workType` | what kind of permit |
| `contractor` / `owner` | the party behind it (searches every contact slot) |
| `address` / `communityArea` / `ward` | where |
| `keywords` | free text over the work description |
| `costMin/Max` / `feeMin/Max` | reported cost & permit fee ranges |
| `issuedFrom/To` | issue-date range |
| `groupBy` | aggregate over status / permitType / reviewType / workType / communityArea / ward |
| `maxResults` | cap records (default 200) |

**Default run = the 200 most recently issued permits** - fast for the daily auto-test. For a targeted query add a filter; for a broad view use `aggregate`.

### Low cost

**From $0.0001 per record** - billed only for the rows you use, at the platform floor. Cost scales with what you pull, not with the size of the register, and because each record is metered individually there's no per-run charge cap to hit on a big pull.

Chicago's permit file is a 122-column table where the interesting part - **who did the work** - is not a column at all. It is spread across **fifteen contact slots**, each holding a role, a name and a city/state/ZIP, and the roles are free-form strings ("CONTRACTOR-GENERAL CONTRACTOR", "OWNER AS GENERAL CONTRACTOR", "CONTRACTOR-PLUMBER/PLUMBING", "SELF CERT ARCHITECT"), so "find this contractor's permits" means scanning every slot of every permit and understanding the role vocabulary - which a thin scraper simply does not do. On top of that the **status column is empty on a third of the register** (the city's express / sign / elevator lanes are never status-tracked), so a naive status filter silently drops a third of Chicago's permits; the **permit types carry a mix of hyphen and en-dash characters** that defeats exact matching; and the fee columns are **comma-free numerics with negative values** that a text comparison would sort wrong. Normalizing the roles, the blank-status lane and the numeric ranges into a schema where a `contractor` / `status=blank` / `costMin` query returns exactly the permits you mean is the actual product. Every pull is integrity-checked against the register's known shape, so a degraded source fails loudly instead of returning bad rows.

### Source

- [City of Chicago: Building Permits](https://data.cityofchicago.org/Buildings/Building-Permits/ydr8-5enu) - the full city permit register, live. Public open data. Records are the city's own published fields; not an endorsement of any contractor or property.

# Actor input Schema

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

rows = permits matching your filters (default). recent = permits issued in the last 30 days. aggregate = one count row per group (see groupBy).

## `permitType` (type: `string`):

Permit type. Use a short word - express, easy, renovation, sign, new construction, elevator, demolition, scaffolding, porch, extension, reinstate - or the exact source value. Blank = any.

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

Permit status: COMPLETE, ACTIVE, EXPIRED, SUSPENDED, PHASED PERMITTING, CANCELLED, REVOKED - or 'blank' for the express / easy / sign / elevator / scaffolding lanes the city does not status-track. Blank = any.

## `reviewType` (type: `string`):

The review lane: EXPRESS PERMIT PROGRAM, EASY PERMIT, STANDARD PLAN REVIEW, SIGN PERMIT, SELF CERT, EASY PERMIT WEB, CONVEYANCE DEVICE PERMIT, DEMOLITION PERMIT, TRADITIONAL DEVELOPER SERVICES, DIRECT DEVELOPER SERVICES. Blank = any.

## `workType` (type: `string`):

Work-classification substring, e.g. 'Electrical Work', 'Reroofing', 'Masonry', 'Solar', 'Fire Alarm'. Present on ~38% of the file. Blank = any.

## `contractor` (type: `string`):

Name of a party in any contact slot whose role is a contractor (general / electrical / plumbing / masonry / wrecking / ...). Substring match. Blank = any.

## `owner` (type: `string`):

Name of a party in any contact slot whose role is an owner. Substring match. Blank = any.

## `address` (type: `string`):

Street name substring, e.g. 'HARRISON' or 'WESTERN'. Blank = any.

## `communityArea` (type: `string`):

Chicago community-area number (exact), e.g. '49' (Uptown). Blank = any.

## `ward` (type: `string`):

Chicago ward number (exact), e.g. '42'. Blank = any.

## `keywords` (type: `string`):

Free-text match on the work description, e.g. 'solar', 'roof', 'garage'. Blank = any.

## `costMin` (type: `integer`):

Only permits whose reported project cost is at least this amount (numeric range; populated on ~96% of the file).

## `costMax` (type: `integer`):

Only permits whose reported project cost is below this amount. 0 = any.

## `feeMin` (type: `integer`):

Only permits whose total permit fee is at least this amount.

## `feeMax` (type: `integer`):

Only permits whose total fee is below this amount. 0 = any.

## `issuedFrom` (type: `string`):

Only permits issued on/after this date (YYYY-MM-DD).

## `issuedTo` (type: `string`):

Only permits issued before this date (YYYY-MM-DD).

## `groupBy` (type: `string`):

Which dimension to aggregate over (only mode=aggregate). status -> the done vs live vs express-lane mix; permitType / reviewType -> what kind of work; ward / communityArea -> where.

## `maxResults` (type: `integer`):

Cap the records pushed (0 = up to 10k per run). Each record is metered individually, so there is no per-run charge cap. An aggregate has at most a few hundred groups.

## Actor input object example

```json
{
  "mode": "rows",
  "permitType": "",
  "status": "",
  "reviewType": "",
  "workType": "",
  "contractor": "",
  "owner": "",
  "address": "",
  "communityArea": "",
  "ward": "",
  "keywords": "",
  "costMin": 0,
  "costMax": 0,
  "feeMin": 0,
  "feeMax": 0,
  "issuedFrom": "",
  "issuedTo": "",
  "groupBy": "status",
  "maxResults": 50
}
```

# Actor output Schema

## `recordsUrl` (type: `string`):

Chicago building permit records or aggregates - as JSON

## `datasetUrl` (type: `string`):

No description

## `runUrl` (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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("j0401/chicago-permits").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 = {}

# Run the Actor and wait for it to finish
run = client.actor("j0401/chicago-permits").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 '{}' |
apify call j0401/chicago-permits --silent --output-dataset

```

## MCP server setup

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

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/JXhRzROXAJCpm1siW/builds/Hl6SOlmyrwTQ5evTv/openapi.json
