# US Building Permits Scraper - 8 City Open-Data Portals (`neverempty/us-building-permits-scraper`) Actor

For contractor lead gen, construction suppliers and property analysts: newly issued building permits from 8 official city open-data portals in one schema. New York alone issued 1,829 permits in a 7-day window. One row per permit, and value filters compare numbers, not text. No API key.

- **URL**: https://apify.com/neverempty/us-building-permits-scraper.md
- **Developed by:** [NeverEmpty](https://apify.com/neverempty) (community)
- **Categories:** Real estate, Lead generation, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $5.84 / 1,000 permit returneds

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?

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 Scraper - 8 City Open-Data Portals

**Built for contractors, suppliers, roofers, solar installers and anyone who follows new construction**: newly issued building permits from the official open-data portals of **New York City, Los Angeles, Chicago, Austin, San Francisco, Seattle, New Orleans and Cincinnati**, returned in one schema. Filter by issue date, keyword (roof, solar, pool, HVAC…) and minimum job value, up to **10,000** permits per city.

**One permit is not one portal row, and paying per row would cost you money for nothing.** Measured against the live portals on 2026-09-10: in the last 7 days New York returned **2,168 rows for 1,829 permits** (**15.6% extra rows** — one row per trade) and San Francisco **251 rows for 231 permits**. You get one row per permit.

**Four of the eight portals store the job value as text, so a numeric filter there is a string comparison.** Asking for permits over $1,000,000 in the last 30 days, a text comparison matched **9,997** New York permits, **4,479** in Los Angeles, **1,122** in San Francisco and **767** in Cincinnati — numbers that are wrong in ways you cannot see. The value is compared as a number here.

**And the README tells you which city publishes which field**, measured on the newest 200 portal rows per city on 2026-09-10 — for example a contractor name on **100%** of New York permits, **10%** of Seattle's and not published at all in Los Angeles or San Francisco; a job value on **12%** of Austin's (often placeholders) against **100%** in New York. Portals publish one to five days after issue, so the newest permit is dated, not assumed.

**A rejected search is never reported as "no permits".** A mistyped filter comes back from the portals as HTTP 400 with a technical message; here a rejected search, a search with no results and a portal that could not be reached each get their own row with the reason.

No API key, no browser, no proxy — every row comes from the city's own published dataset, with a link back to it. Export as JSON, CSV or Excel.

### What the portals get wrong, and what this Actor does about it

All numbers below were measured against the live portals on 2026-09-10.

| The portals' behaviour | What you get here |
|---|---|
| **The same permit appears on several rows.** In the last 7 days New York returned 2,168 rows for 1,829 permits (15.6% extra rows - one row per trade on the permit), San Francisco 251 rows for 231 permits (one row per address), Cincinnati 107 rows for 101 permits (one row per line item) | **One row per permit.** New York's trades become a `workTypes` list, San Francisco uses the permit's primary address, Cincinnati's line items are added up (and also listed one by one in `valuationLinesUsd`). `sourceRowCount` says how many portal rows were combined. You are charged once per permit, not once per portal row |
| **Four portals store the job value as text.** Asking for permits over $1,000,000 in the last 30 days, a text comparison matched 9,997 New York permits, 4,479 in Los Angeles, 1,122 in San Francisco and 767 in Cincinnati. Compared as numbers the real counts were 699, 63, 35 and 12 | The minimum value is always compared as a number |
| Every city names its columns differently - the issue date alone is called nine different things across US portals | The same column names for every city |
| In Chicago most permits list the property **owner** first, and contractors appear under many different contact types ("CONTRACTOR-ELECTRICAL", "GENERAL CONTRACTOR", "TENT CONTRACTOR"...) | `contractorName` is taken from any contractor-type contact and never from an owner-type one, including "OWNER AS GENERAL CONTRACTOR" |
| When owners pull their own permit, the portal's contractor field can hold the owner's name | The owner columns are never returned. In New York, Chicago and New Orleans, where the portal also publishes the owner, a contractor name identical to the owner's is left out |
| Some job values are placeholders such as `1` or `0` (common in Austin and present in San Francisco) | Values are kept as published and `valuationIsPlaceholder` is `true` for anything of $1 or less, so you can filter them out |
| Portals can change while they are being read, which shifts page boundaries | Results are read in a fully fixed order, and a row that arrives twice is counted once; if that happens, a row says the data changed during the run |
| A mistyped filter comes back as HTTP 400 with a technical message, which is easy to mistake for "no permits" | A rejected search, a search with no results and a portal that could not be reached each get their own row with the reason. None of them is charged |

### What each city publishes

Share of recent permits with each field, measured on the newest 200 portal rows per city on 2026-09-10:

| City | Contractor name | Job value | Application date | ZIP |
|---|---|---|---|---|
| New York City | 100% (permit holder's business) | 100% | not published | 100% |
| Los Angeles | not published | 100% | 56% | 100% |
| Chicago | 98% | 89% | 100% | not published |
| Austin | 96% | 12% (often placeholders) | 100% | 100% |
| San Francisco | not published | 99% | 100% | 100% |
| Seattle | 10% | 100% | 91% | 93% |
| New Orleans | 92% | 100% | 100% | not published |
| Cincinnati | 97% | 100% | 100% | 99% |

A few things to know about individual cities:

- **New York renewals.** When a permit is renewed, New York issues it again under the same permit number with a new filing; that renewal is its own row, and `permitType` says so ("Renewal Permit Without Changes" / "Renewal Permit with Changes"). About a third of New York's recent permit rows are renewals.
- **Cincinnati keywords.** Cincinnati publishes only a permit category (HVAC, Alteration, Plumbing Permits...) rather than a written description, so a keyword such as `roof` can only match category names there - searched over the last 365 days, roof, solar, pool and deck matched nothing in Cincinnati.
- **Cincinnati line items.** Line items are added into `valuationUsd`. In a small share of permits two line items carry the same amount, which may be the project total repeated; `valuationLinesUsd` lists every line so you can check.
- **Publication delay.** Portals publish permits one to five days after they are issued (on 2026-09-10 the newest permits were from 09-08 in New York, 09-05 in Los Angeles and 09-08 in Cincinnati), and New York published no permits at all for 09-05 to 09-07. The last few days of any search fill in over the following week.

### Input

| Field | Default | What it does |
|---|---|---|
| `cities` | - | Any of `nyc`, `la`, `chicago`, `austin`, `sf`, `seattle`, `nola`, `cincinnati`. If none is chosen, Austin and Chicago are searched |
| `daysBack` | `30` | Permits issued in the last N days, counting today (1-365) |
| `issuedFrom` / `issuedTo` | - | Exact issue-date range as `YYYY-MM-DD`, instead of `daysBack` |
| `keyword` | - | Keep permits whose description or type contains this text, such as `roof` or `solar` |
| `minValueUsd` | - | Keep permits with a declared job value of at least this many dollars. Permits with no declared value are left out when this is set - in Austin that is most permits |
| `maxPermitsPerCity` | `200` | Newest permits first, up to 10,000 per city |

Example - roofing permits over $20,000 issued in the last two weeks in New York and Chicago:

```json
{ "cities": ["nyc", "chicago"], "daysBack": 14, "keyword": "roof", "minValueUsd": 20000 }
```

### Output

Two real rows (from the portals on 2026-09-10). The New York permit was spread over two portal rows, one per trade:

```json
{
  "city": "New York City", "cityKey": "nyc", "state": "NY",
  "permitNumber": "Q01360158-I1-MS",
  "issuedDate": "2026-09-08",
  "appliedDate": null,
  "permitType": "Initial Permit",
  "workTypes": ["Earth Work", "Mechanical Systems"],
  "description": "VERTICAL & HORIZONTAL EXTENSION OF EXISTING ONE FAMILY AND CONVERTING TO TWO FAMILY. OBTAIN NEW C OF O",
  "permitStatus": "Permit Issued",
  "address": "43-21 220 PLACE, Queens",
  "zip": "11361",
  "latitude": 40.762651, "longitude": -73.761407,
  "valuationUsd": 1650030,
  "valuationIsPlaceholder": false,
  "valuationLinesUsd": [],
  "contractorName": "STREAM ROCK CONTSRUCTION*",
  "sourceRowCount": 2,
  "sourceDataset": "https://data.cityofnewyork.us/d/rbx6-tga4"
}
```

```json
{
  "city": "Chicago", "cityKey": "chicago", "state": "IL",
  "permitNumber": "101071485",
  "issuedDate": "2026-09-09",
  "appliedDate": "2025-07-08",
  "permitType": "PERMIT - NEW CONSTRUCTION",
  "workTypes": [],
  "description": "SPR 2019 CBC ERECT A 4 STORY, 9 DWELLING UNIT MIXED USE SPRINKLERED MASONRY BUILDING WITH AN ELEVATOR. ...",
  "permitStatus": "ACTIVE",
  "address": "2053 W IRVING PARK RD",
  "zip": null,
  "latitude": 41.95401572558783, "longitude": -87.68086669907031,
  "valuationUsd": 1275000,
  "valuationIsPlaceholder": false,
  "valuationLinesUsd": [],
  "contractorName": "MIREX ELECTRIC & CONSTRUCTION,",
  "sourceRowCount": 1,
  "sourceDataset": "https://data.cityofchicago.org/d/ydr8-5enu"
}
```

Text such as contractor names is returned exactly as the city published it, typos included. A value the city does not publish is `null`, never a guess. Contractor names are the contractor of record as the city lists it; for sole traders that can be a person's name.

Rows whose `status` is not `ok` (`no-permits`, `bad-input`, `city-skipped`, `failed`, `incomplete`) explain themselves in `note` and are never charged.

### Pricing

Pay per permit returned. A permit the portal spreads over several rows is one permit and one charge. Rows that only explain an empty, rejected, skipped or failed search are free.

### Sources

NYC Open Data (DOB NOW: Build - Approved Permits), Los Angeles Open Data (Building Permits), City of Chicago Data Portal (Building Permits), Austin Open Data (Issued Construction Permits), DataSF (Building Permits), Seattle Open Data (Building Permits), New Orleans Open Data (Permits), Cincinnati Open Data (Building Permits). This Actor is not affiliated with any of these cities.

# Actor input Schema

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

Which city portals to search. Each city publishes different columns: New York City, Austin, Cincinnati, New Orleans and Chicago publish a contractor name; Los Angeles and San Francisco do not. All eight publish a job value.

## `daysBack` (type: `integer`):

Permits issued in the last N days, counting today. Cities publish permits one to five days after issue, so the most recent days fill in over the following week. Ignored when an issued-from date is given.

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

Earliest issue date, instead of the last N days.

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

Latest issue date. Empty means today.

## `keyword` (type: `string`):

Keep permits whose description or permit type contains this text, such as roof, solar, pool or HVAC. Case does not matter. Cincinnati publishes only a permit category (HVAC, Alteration, Plumbing Permits...) rather than a written description, so there a keyword can only match category names.

## `minValueUsd` (type: `integer`):

Keep permits whose declared job value is at least this many dollars. Four of the eight portals store the value as text; it is compared as a number here (compared as text, 'over $1,000,000' matched 9,997 New York permits in 30 days instead of 699). Permits with no declared value are left out when this is set - in Austin that is most permits.

## `maxPermitsPerCity` (type: `integer`):

The most permits returned for each city, newest first. A permit that the portal splits over several records (for example one record per trade in New York) counts once.

## Actor input object example

```json
{
  "cities": [
    "austin",
    "chicago"
  ],
  "daysBack": 30,
  "issuedFrom": "",
  "issuedTo": "",
  "keyword": "roof",
  "maxPermitsPerCity": 200
}
```

# Actor output Schema

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

One row per building permit, plus one row when a city's portal rejected the search, matched nothing, was skipped, or could not be read.

# 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": [
        "austin",
        "chicago"
    ],
    "keyword": "roof"
};

// Run the Actor and wait for it to finish
const run = await client.actor("neverempty/us-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 = {
    "cities": [
        "austin",
        "chicago",
    ],
    "keyword": "roof",
}

# Run the Actor and wait for it to finish
run = client.actor("neverempty/us-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 '{
  "cities": [
    "austin",
    "chicago"
  ],
  "keyword": "roof"
}' |
apify call neverempty/us-building-permits-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,neverempty/us-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/kiKZzMQbe7FOFZONt/builds/FToN9mOF2t05xncFu/openapi.json
