# Chicago Building Permits Scraper (`crawlerbros/chicago-building-permits-scraper`) Actor

Scrape City of Chicago building permit records from the public Socrata Open Data API. Search by permit type, work type, status, community area, ward, date range, or work description. Lookup by permit number(s) or by street address for full permit history.

- **URL**: https://apify.com/crawlerbros/chicago-building-permits-scraper.md
- **Developed by:** [Crawler Bros](https://apify.com/crawlerbros) (community)
- **Categories:** Automation, Developer tools, Real estate
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.00 / 1,000 results

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

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#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 Scraper

Scrape City of Chicago building permit records straight from the city's public Socrata Open Data API. Search and filter by permit type, status, milestone, work type, review type, community area, ward, date range, work description or contractor/owner name keyword, or reported cost / processing-time range. Look up exact permits by permit number, or pull the full permitting history for any street address.

### What this actor does

- **Three modes:** `search`, `byPermitNumbers`, `byAddress`
- **Rich filtering:** permit type, permit status, permit milestone, work type, review type, community area (all 77), ward (1-50), issue date range, application date range, work description keyword, contractor/owner name keyword, reported cost range, processing-time range, "near me" latitude/longitude radius search
- **Configurable sort:** sort by issue date, application date, reported cost, or processing time, ascending or descending
- **Full financial detail:** building/zoning/other fees paid, unpaid, waived, and per-category subtotal, plus total fee and reported project cost
- **Contractor/owner records:** every contact slot (owner, general contractor, engineer, architect, etc.) attached to a permit
- **Geolocation:** latitude/longitude and Cook County tax parcel PIN(s)
- **Empty fields are omitted**

### Output per permit

- `rowId` — unique Socrata row identifier for this permit record
- `permitNumber`, `permitStatus`, `permitMilestone`, `permitType`, `reviewType`
- `applicationStartDate`, `issueDate`, `processingTimeDays`
- `streetNumber`, `streetDirection`, `streetName`, `fullAddress`
- `workType`, `workDescription`, `permitCondition`
- `buildingFeePaid`, `zoningFeePaid`, `otherFeePaid`, `subtotalPaid`
- `buildingFeeUnpaid`, `zoningFeeUnpaid`, `otherFeeUnpaid`, `subtotalUnpaid`
- `buildingFeeWaived`, `zoningFeeWaived`, `otherFeeWaived`, `subtotalWaived`
- `buildingFeeSubtotal`, `zoningFeeSubtotal`, `otherFeeSubtotal` — per-category total (paid + unpaid + waived)
- `totalFee`, `reportedCost`
- `pinList[]` — Cook County tax parcel PIN(s)
- `contractors[]` — `{type, name, city, state, zipCode}` for every owner/contractor/engineer/architect on the permit
- `communityArea`, `communityAreaName`, `censusTract`, `ward`
- `latitude`, `longitude`
- `sourceUrl` — the exact Socrata API query for this permit
- `recordType: "buildingPermit"`, `scrapedAt`

### Input

| Field | Type | Default | Description |
|---|---|---|---|
| `mode` | string | `search` | `search` / `byPermitNumbers` / `byAddress` |
| `permitType` | select | – | e.g. New Construction, Renovation/Alteration, Signs |
| `permitStatus` | select | – | Active / Complete / Expired / Revoked / etc. |
| `permitMilestone` | select | – | Workflow milestone (Inspections, Certificate of Occupancy Issued, etc.) |
| `workType` | select | – | Electrical Work, Masonry Work, Reroofing, etc. |
| `reviewType` | select | – | Express Permit Program, Easy Permit, Standard Plan Review, etc. |
| `communityArea` | select | – | One of Chicago's 77 official community areas |
| `ward` | select | – | Chicago City Council ward, 1-50 |
| `workDescriptionContains` | string | – | Case-insensitive substring match, e.g. `kitchen`, `solar` |
| `contractorNameContains` | string | – | Case-insensitive substring match against any owner/contractor/architect/engineer name on the permit |
| `issueDateFrom` | string | `2024-01-01` | `YYYY-MM-DD`. Clear it to search the full history back to 2006 |
| `issueDateTo` | string | – | `YYYY-MM-DD` |
| `applicationDateFrom` / `applicationDateTo` | string | – | `YYYY-MM-DD` |
| `minReportedCost` / `maxReportedCost` | int | – | USD project cost bounds |
| `minProcessingTimeDays` / `maxProcessingTimeDays` | int | – | Application-to-issue processing time bounds, in days |
| `latitude` / `longitude` | number | – | Center point for a "near me" radius search (mode=search) |
| `radiusKm` | number | `1` | Radius in km around `latitude`/`longitude`, 0.1-50 |
| `permitNumbers` | array | – | Exact permit number(s) (mode=byPermitNumbers) |
| `streetNumber` / `streetDirection` / `streetName` | – | – | Address lookup (mode=byAddress) |
| `sortBy` | select | `issueDate` | `issueDate` / `applicationStartDate` / `reportedCost` / `processingTime` |
| `sortAscending` | bool | `false` | Oldest/lowest-first instead of newest/highest-first |
| `maxItems` | int | `50` | Hard cap (1-10000) |

#### Example: recent new-construction permits in the Loop

```json
{
  "mode": "search",
  "permitType": "PERMIT - NEW CONSTRUCTION",
  "communityArea": "32",
  "maxItems": 50
}
```

#### Example: full permit history for an address

```json
{
  "mode": "byAddress",
  "streetNumber": 401,
  "streetDirection": "N",
  "streetName": "ABERDEEN ST"
}
```

#### Example: lookup by permit number

```json
{
  "mode": "byPermitNumbers",
  "permitNumbers": ["100109616", "B100522727"]
}
```

#### Example: permits near a point (Loop, 1 km radius)

```json
{
  "mode": "search",
  "latitude": 41.8781,
  "longitude": -87.6298,
  "radiusKm": 1,
  "maxItems": 50
}
```

### Use cases

- **Real estate diligence** — pull the full permit history for a property before purchase
- **Contractor research** — track competitors' permit volume by type and community area
- **Construction market intelligence** — monitor new-construction and renovation trends by ward
- **Compliance monitoring** — flag stop-work, revoked, or suspended permits
- **Journalism / civic research** — analyze development patterns across Chicago's community areas

### FAQ

**What's the data source?** The City of Chicago's public Socrata Open Data API (`data.cityofchicago.org`), dataset `ydr8-5enu` — "Building Permits". No account, cookies, or API key required.

**How far back does the data go?** From 2006 to the present, excluding permits voided or revoked after issuance.

**Why are ward/community area sometimes missing?** A small fraction of permits (under 10%) have no ward or community area recorded in the source data, or use `0` as a placeholder for "not assigned" — these are omitted rather than showing a fake `0`.

**What are the `contractors[]` entries?** Every permit can list up to 15 parties (owner, general contractor, sub-contractors, engineers, architects). Each entry includes their role (`type`), name, and city/state/ZIP where available.

**What is `pinList`?** Cook County Property Index Number(s) (tax parcel identifiers) tied to the permit.

**How fresh is the data?** The city updates this dataset regularly (typically daily).

### Data Source / Limitations

This actor reads exclusively from the City of Chicago's public Socrata SODA API for the Building Permits dataset (`ydr8-5enu`) — no scraping, no authentication, no cookies. Every filter and output field above maps to a real column in that dataset; nothing is inferred or fabricated.

**What's intentionally NOT exposed, and why:**

- **`permitCondition` is not filterable.** It's shown in the output when present, but the underlying values are thousands of distinct free-text inspector notes (not a finite/stable set), so it can't be a `select` enum, and a substring filter over it would rarely match usefully — use `workDescriptionContains` for keyword search instead.
- **ZIP code is not a search filter.** The dataset has no permit-level ZIP column; ZIP only appears per-contractor (`contractors[].zipCode`), which isn't a reliable proxy for the permit's own location. Use `communityArea`/`ward` for boundary-based geographic filtering, or `latitude`/`longitude`/`radiusKm` for a precise "near me" radius search.
- **Contact/contractor "type" (e.g. `ELECTRICAL CONTRACTOR`, `EXPEDITOR`) is not a `select` enum filter.** Each of the 15 contact slots has its own inconsistent free-text vocabulary (43+ variants observed on slot 1 alone, with spelling drift like `EXPEDITOR` vs `EXPEDITER` and `MASONRY CONTRACTOR` vs `MASON CONTRACTOR`) — not a stable finite set. Use `contractorNameContains` to search by name instead.
- **Illinois State Plane X/Y coordinates and the internal Socrata "computed region" join columns** (ZIP-code/community-area/census-tract/ward boundary IDs) are present upstream but omitted — they're redundant with `latitude`/`longitude` and `communityArea`/`ward`, which are already exposed in a directly usable form.
- **No account/login-gated data.** The Socrata API is fully public; there is no additional "premium" or authenticated tier of this dataset to expose.

**Known upstream data quirks (not actor bugs):**

- \~8 of 844K+ rows (well under 0.01%) have no `reviewType` recorded — filtering by `reviewType` will simply not match those rows, same as any other real-world null.
- A small fraction of permits (well under 10%) have no `ward`/`communityArea` recorded, or use `0` as an "unassigned" placeholder — these are omitted from the output rather than emitting a misleading `0`.

# Actor input Schema

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

What to fetch.

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

Filter to a specific permit type (mode=search).

## `permitStatus` (type: `string`):

Filter to a specific current permit status (mode=search).

## `permitMilestone` (type: `string`):

Filter to a specific permit workflow milestone (mode=search).

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

Filter to a specific category of work performed under the permit (mode=search).

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

Filter to a specific permit-application review pathway (mode=search).

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

One of Chicago's 77 official community areas (mode=search).

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

Chicago City Council ward, 1-50 (mode=search).

## `latitude` (type: `number`):

Center latitude for a "near me" radius search. Combine with `longitude` and `radiusKm`.

## `longitude` (type: `number`):

Center longitude for a "near me" radius search. Combine with `latitude` and `radiusKm`.

## `radiusKm` (type: `number`):

Search radius in kilometers around `latitude`/`longitude`. Only applied when both are set. Default 1 km.

## `workDescriptionContains` (type: `string`):

Case-insensitive substring match against the free-text work description (e.g. `kitchen`, `roof`, `solar`) (mode=search).

## `contractorNameContains` (type: `string`):

Case-insensitive substring match against any of the up-to-15 owner/contractor/architect/engineer names on the permit (e.g. `turner construction`) (mode=search).

## `issueDateFrom` (type: `string`):

Only include permits issued on/after this date (YYYY-MM-DD) (mode=search). Prefilled to a recent date so the default search stays fast (unfiltered + sorted queries scan the full 800K+ row table); clear it to search the full history.

## `issueDateTo` (type: `string`):

Only include permits issued on/before this date (YYYY-MM-DD) (mode=search).

## `applicationDateFrom` (type: `string`):

Only include permits whose application was started on/after this date (YYYY-MM-DD) (mode=search).

## `applicationDateTo` (type: `string`):

Only include permits whose application was started on/before this date (YYYY-MM-DD) (mode=search).

## `minReportedCost` (type: `integer`):

Only include permits with a reported project cost at or above this amount (mode=search).

## `maxReportedCost` (type: `integer`):

Only include permits with a reported project cost at or below this amount (mode=search).

## `minProcessingTimeDays` (type: `integer`):

Only include permits whose application-to-issue processing time is at or above this many days (mode=search).

## `maxProcessingTimeDays` (type: `integer`):

Only include permits whose application-to-issue processing time is at or below this many days (mode=search).

## `permitNumbers` (type: `array`):

Exact City of Chicago permit number(s) to fetch, e.g. `100109616` or `B100522727`.

## `streetNumber` (type: `integer`):

House/building street number, e.g. `401`.

## `streetDirection` (type: `string`):

Street direction prefix.

## `streetName` (type: `string`):

Street name including suffix, e.g. `ABERDEEN ST`.

## `sortBy` (type: `string`):

Which date/amount field to sort results by (applies to mode=search and mode=byAddress).

## `sortAscending` (type: `boolean`):

Sort oldest/lowest-first instead of newest/highest-first.

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

Hard cap on emitted records.

## Actor input object example

```json
{
  "mode": "search",
  "permitType": "",
  "permitStatus": "",
  "permitMilestone": "",
  "workType": "",
  "reviewType": "",
  "communityArea": "",
  "ward": "",
  "radiusKm": 1,
  "workDescriptionContains": "",
  "contractorNameContains": "",
  "issueDateFrom": "2024-01-01",
  "permitNumbers": [],
  "streetDirection": "",
  "sortBy": "issueDate",
  "sortAscending": false,
  "maxItems": 50
}
```

# Actor output Schema

## `permits` (type: `string`):

Dataset containing all scraped Chicago building permit records.

# 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 = {
    "mode": "search",
    "permitType": "",
    "permitStatus": "",
    "permitMilestone": "",
    "workType": "",
    "reviewType": "",
    "communityArea": "",
    "ward": "",
    "radiusKm": 1,
    "workDescriptionContains": "",
    "contractorNameContains": "",
    "issueDateFrom": "2024-01-01",
    "permitNumbers": [],
    "streetDirection": "",
    "sortBy": "issueDate",
    "sortAscending": false,
    "maxItems": 50
};

// Run the Actor and wait for it to finish
const run = await client.actor("crawlerbros/chicago-building-permits-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 = {
    "mode": "search",
    "permitType": "",
    "permitStatus": "",
    "permitMilestone": "",
    "workType": "",
    "reviewType": "",
    "communityArea": "",
    "ward": "",
    "radiusKm": 1,
    "workDescriptionContains": "",
    "contractorNameContains": "",
    "issueDateFrom": "2024-01-01",
    "permitNumbers": [],
    "streetDirection": "",
    "sortBy": "issueDate",
    "sortAscending": False,
    "maxItems": 50,
}

# Run the Actor and wait for it to finish
run = client.actor("crawlerbros/chicago-building-permits-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 '{
  "mode": "search",
  "permitType": "",
  "permitStatus": "",
  "permitMilestone": "",
  "workType": "",
  "reviewType": "",
  "communityArea": "",
  "ward": "",
  "radiusKm": 1,
  "workDescriptionContains": "",
  "contractorNameContains": "",
  "issueDateFrom": "2024-01-01",
  "permitNumbers": [],
  "streetDirection": "",
  "sortBy": "issueDate",
  "sortAscending": false,
  "maxItems": 50
}' |
apify call crawlerbros/chicago-building-permits-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,crawlerbros/chicago-building-permits-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/8e82aphePfoPEEfjW/builds/9pFHVuUrac6u5ZHrM/openapi.json
