# US Building Permits - 19 sources, one schema (`formal_sadness_xvx/us-building-permits`) Actor

Building permits from 19 actively-maintained US municipal open-data portals (~11.4M records) normalised into a single schema with one date format, one address field and one currency field.

- **URL**: https://apify.com/formal\_sadness\_xvx/us-building-permits.md
- **Developed by:** [Rishi Vinodkumar](https://apify.com/formal_sadness_xvx) (community)
- **Categories:** E-commerce, Lead generation, News
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 record scrapeds

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/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

## US Building Permits — 19 sources, one schema

Building permits from nineteen US municipal open-data portals, normalised
into a single schema with one date format, one address field, and one currency
field.

**~11,150,026 permit records across 19 actively-maintained sources.**

| Source | State | Records | Last verified |
|---|---|---:|---|
| New York City | NY | ~3,989,647 | 2026-07-28 |
| Austin (all trades) | TX | ~2,367,694 | 2026-07-28 |
| Chicago | IL | ~842,592 | 2026-07-28 |
| New York City (electrical) | NY | ~580,864 | 2026-07-30 |
| Seattle (electrical) | WA | ~498,027 | 2026-07-28 |
| New Orleans | LA | ~460,490 | 2026-07-29 |
| Los Angeles | CA | ~403,243 | 2026-07-25 |
| Montgomery County | MD | ~319,392 | 2026-07-28 |
| Austin | TX | ~285,851 | 2026-07-06 |
| Seattle (trade) | WA | ~284,688 | 2026-07-29 |
| Seattle | WA | ~191,830 | — |
| Montgomery County (residential) | MD | ~185,696 | 2026-07-30 |
| Cincinnati | OH | ~178,460 | — |
| Montgomery County (mechanical) | MD | ~157,388 | 2026-07-30 |
| Mesa | AZ | ~155,569 | 2026-07-27 |
| Baton Rouge | LA | ~142,483 | — |
| Marin County | CA | ~50,280 | 2026-07-29 |
| Montgomery County (commercial) | MD | ~41,675 | 2026-07-30 |
| Cambridge | MA | ~14,157 | 2026-07-28 |

**Registered but excluded by default** (abandoned upstream, kept so the staleness is documented rather than rediscovered):

- **Dallas, TX** — last record 2020-08-29
- **Gainesville, FL** — last record 2023-02-28

### Why this exists

Every city below publishes permits on Socrata, so the *transport* is identical
and trivial. Everything else differs. The issue-date column alone is spelled
nine ways:

| City | Column | Type |
|---|---|---|
| Chicago | `issue_date` | timestamp |
| New York City | `issuance_date` | **text `MM/DD/YYYY`** |
| Seattle | `issueddate` | timestamp |
| Austin | `issue_date` | timestamp |
| Los Angeles | `issue_date` | timestamp |
| Cincinnati | `issueddate` | timestamp |
| Baton Rouge | `issueddate` | timestamp |
| Mesa | `opened_date` | timestamp (application date) |
| Cambridge | `issue_date` | timestamp |

Addresses are worse — Chicago splits into number/direction/name, NYC into
house/street/borough, Seattle keeps one preformatted line, Cambridge uses
`full_address`. Project value appears as `estprojectcost`, `valuation`,
`total_job_valuation`, `projectvalue`, `building_cost`, and `icc_value`.

Writing nineteen adapters is a week. *Maintaining* them as cities silently
rename and retype columns is the actual cost, and it never ends.

\* NYC's dataset holds 4M rows going back to 1989, but only ~4.6k permits were
issued in 2026 and ~10k in 2025 — the bulk is historical.

### Output schema

```json
{
  "source_city": "chicago",
  "city_label": "Chicago",
  "state": "IL",
  "permit_number": "101084879",
  "permit_type": "PERMIT - RENOVATION/ALTERATION",
  "permit_subtype": "STANDARD PLAN REVIEW",
  "status": "ACTIVE",
  "description": "INTERIOR ALTERATION OF EXISTING CORRIDORS AND RESTROOMS...",
  "issue_date": "2026-07-28",
  "issue_date_is_approximate": false,
  "application_date": "2026-06-25",
  "expiration_date": null,
  "address": "600 W CHICAGO AVE",
  "city_name": "Chicago",
  "zip_code": "60654",
  "latitude": 41.8965851912,
  "longitude": -87.6429961817,
  "project_value_usd": 351750.0,
  "fee_usd": 600.0,
  "contractor_name": "MATTHEW RYAN",
  "applicant_name": null,
  "dwelling_units": null,
  "source_url": "https://data.cityofchicago.org/resource/ydr8-5enu.json",
  "source_dataset_id": "ydr8-5enu"
}
```

`permit_number`, `issue_date`, `address`, `latitude` and `longitude` are
populated for **100%** of records across all nine live cities.

#### Honest field-availability notes

Some fields are absent because the city does not publish them, not because
normalisation failed. Rather than emit a plausible-looking null and let you
discover this in your own funnel metrics:

- **No project valuation:** NYC, Mesa
- **No contractor name:** Seattle, Austin, Los Angeles
- **No site ZIP:** Mesa, Cambridge (Mesa publishes contractor ZIP only)
- **`issue_date_is_approximate: true`** on Mesa — it publishes an application
  date, not an issue date. Every record is flagged so you can exclude it.

Set `includeRawRecord: true` to attach the untouched municipal record under
`raw` and check anything yourself.

### Who this is for

- **Solar / roofing / HVAC contractors** buying geocoded permit leads. LA
  additionally exposes a `solar` flag.
- **Construction-materials suppliers** sizing demand by metro and value band.
- **Proptech and lead-gen platforms** needing a permit feed without owning
  nine scrapers.
- **Market researchers** tracking construction activity across metros.

### Usage

Typical lead-generation run — high-value residential work in the last month:

```json
{
  "cities": ["chicago", "los_angeles", "seattle", "cincinnati"],
  "issuedAfter": "2026-07-01",
  "minProjectValue": 50000,
  "requireAddress": true,
  "maxResults": 5000
}
```

Solar-specific:

```json
{ "cities": ["los_angeles", "austin", "mesa"], "search": "solar", "issuedAfter": "2026-01-01" }
```

**Careful with `minProjectValue`:** records where the city publishes no
valuation are excluded, so setting it drops NYC and Mesa entirely. That is
deliberate — a lead you cannot size did not meet your threshold.

### Local development

No Apify runtime needed; the scraping logic is independent of the platform.

```bash
pip install requests
python local_run.py --coverage                       # registry + row counts
python local_run.py --cities chicago,seattle --issued-after 2026-06-01 --limit 20
python verify.py                                     # health-check every source
```

`verify.py` is the maintenance early-warning system. Run it on a schedule — it
catches the three ways these feeds break:

1. **Abandoned upstream** (Dallas: metadata claims fresh, data ends 2020)
2. **Renamed column** (mapping is alias-tolerant, but a rename matching no
   alias silently nulls a field)
3. **Retyped column** (NYC's text dates make range filters return
   plausible-looking nonsense instead of an error)

It caught a real bug during development: a `suffix` column on Chicago that
does not exist.

### Legal

Public, logged-out municipal open-data endpoints served through each city's
official Socrata API. No authentication, no account, no terms accepted, no
anti-bot circumvention, no personal contact data. Contractor and applicant
names are published by the municipality as part of the public record.

Rate limits are respected with exponential backoff — these are public-good
servers. Supplying a free Socrata app token raises your limit above the shared
anonymous pool.

# Actor input Schema

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

Which sources to fetch. Defaults to all 19 actively-maintained ones. Dallas and Gainesville are supported but abandoned upstream and excluded unless selected explicitly.

## `issuedAfter` (type: `string`):

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

## `issuedBefore` (type: `string`):

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

## `permitTypeContains` (type: `string`):

Case-insensitive substring match on permit type, e.g. 'solar', 'roof', 'electrical'. Applied server-side where the city exposes a permit\_type column; skipped for cities that do not.

## `search` (type: `string`):

Case-insensitive substring match across description, type, address, contractor and applicant.

## `minProjectValue` (type: `integer`):

Excludes permits below this declared value. NOTE: records where the city publishes no valuation are excluded too - NYC and Mesa publish none at all, so setting this drops them entirely.

## `requireAddress` (type: `boolean`):

Drop records with no resolvable street address.

## `maxResultsPerCity` (type: `integer`):

Caps results from each city. Leave empty for no limit.

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

Hard cap across all cities. Use this to bound cost.

## `includeRawRecord` (type: `boolean`):

Attach the original municipal record under `raw`, so nothing is lost to normalisation.

## `socrataAppToken` (type: `string`):

A free Socrata app token raises rate limits from the shared anonymous pool. Only worth setting for large multi-city sweeps.

## Actor input object example

```json
{
  "cities": [
    "chicago",
    "seattle",
    "los_angeles",
    "new_orleans"
  ],
  "issuedAfter": "2026-01-01",
  "requireAddress": false,
  "maxResults": 1000,
  "includeRawRecord": false
}
```

# 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",
        "seattle",
        "los_angeles",
        "new_orleans"
    ],
    "issuedAfter": "2026-01-01",
    "maxResults": 1000
};

// Run the Actor and wait for it to finish
const run = await client.actor("formal_sadness_xvx/us-building-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 = {
    "cities": [
        "chicago",
        "seattle",
        "los_angeles",
        "new_orleans",
    ],
    "issuedAfter": "2026-01-01",
    "maxResults": 1000,
}

# Run the Actor and wait for it to finish
run = client.actor("formal_sadness_xvx/us-building-permits").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print("💾 Check your data here: https://console.apify.com/storage/datasets/" + run["defaultDatasetId"])
for item in client.dataset(run["defaultDatasetId"]).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",
    "seattle",
    "los_angeles",
    "new_orleans"
  ],
  "issuedAfter": "2026-01-01",
  "maxResults": 1000
}' |
apify call formal_sadness_xvx/us-building-permits --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=formal_sadness_xvx/us-building-permits",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/acts/nskCpz0BNrOUIB9wa/builds/ojHuriPu9jxzmLemz/openapi.json
