# US Building Permits Scraper — City & County Permit Records (`owlabee/building-permits-scraper`) Actor

Scrape building permits from 26 US city and county open-data portals (NYC, Chicago, LA, SF, Seattle, Austin, Philadelphia, Boston, DC, Miami-Dade, more) into a normalized JSON schema. Filter by date, type, cost and keyword; monitor new permits for contractor, solar and roofing leads. $0.003/permit.

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

## Pricing

from $3.00 / 1,000 permit 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/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

## US Building Permits Scraper — City & County Permit Records

### What it does

This Actor pulls **building permits** from the official open-data portals of 26 US cities and counties and writes them into **one normalized JSON schema**: permit number, type and subtype, work description, status, applied / issued / finalized / expiration dates, estimated cost, address with coordinates and parcel id, contractor and license, and the owner or applicant names where the jurisdiction publishes them. Every record links back to its public source.

It talks directly to each city's **Socrata (SODA), CKAN DataStore, ArcGIS REST or CARTO SQL** API — no browser, no proxies, no login — so runs are fast, cheap and stable. Filter by issued-date window, permit-type keywords, work-description keyword and minimum cost, and turn on **Only new permits since last run** to get a daily feed of fresh permits for contractor, solar, roofing, HVAC, remodeling or real-estate lead generation. Results are available as JSON, CSV, Excel, via the Apify API, webhooks and the Apify MCP server.

### How to use

1. Pick one or more **Jurisdictions** (or "All supported jurisdictions") and set **Issued within (days)**.
2. Optionally narrow with **Permit type keywords** (e.g. `roof`, `solar`, `electrical`), a **Work description keyword** (e.g. `kitchen`), a **Minimum estimated cost**, and cap **Max permits per jurisdiction**.
3. Click **Start**. Permits appear in the dataset as each jurisdiction is paged. For a daily feed, schedule the Actor with **Only new permits since last run** enabled and add a **Webhook URL**.

### Input

| Field | Type | Required | Description |
|---|---|---|---|
| `jurisdictions` | string\[] | yes | Registry ids (see table below) or `all`. Default `["chicago","seattle"]`. |
| `issuedWithinDays` | integer | no | Only permits issued in the last N days (1–3650). Default 30. |
| `permitTypes` | string\[] | no | Keep permits whose type/subtype contains any of these words (case-insensitive). |
| `keyword` | string | no | Keep permits whose work description contains this text. |
| `minEstimatedCost` | integer | no | Drop permits with a declared cost below this (permits without a cost are kept). |
| `maxResultsPerJurisdiction` | integer | no | Cap per jurisdiction, newest first. Default 1000. |
| `onlyNewSinceLastRun` | boolean | no | Monitor mode: emit only permits not seen in earlier runs (per jurisdiction, up to 50,000 remembered). |
| `stateStoreName` | string | no | Named key-value store for the seen-sets. Default `building-permits-scraper-state`. |
| `includeRaw` | boolean | no | Attach the untouched source row as `raw`. |
| `socrataAppToken` | string (secret) | no | Optional Socrata app token to raise rate limits on the 13 Socrata-backed portals. |
| `webhookUrl` | string | no | POST target for the run summary + emitted permits (max 500). |

Filters are pushed to the server wherever the portal supports it (Socrata `$where`, ArcGIS `where`, CARTO SQL) and always re-applied client-side, so behaviour is identical across APIs. CKAN portals (Boston, Pittsburgh) have no date filter: they are scanned newest-first until the window ends.

### Output example

```json
{
  "jurisdiction": "nyc",
  "jurisdictionName": "New York City, NY",
  "permitNumber": "B01186301-S6-MS",
  "permitType": "Mechanical Systems",
  "permitSubtype": "Initial Permit",
  "workDescription": "Mechanical Systems filed in conjunction to NB application #B01186301-I1.",
  "status": "Permit Issued",
  "appliedDate": "2026-02-20",
  "issuedDate": "2026-10-14",
  "finalizedDate": null,
  "expirationDate": "2027-10-14",
  "estimatedCost": null,
  "address": "280 BERGEN STREET",
  "city": "Brooklyn",
  "state": "NY",
  "zip": "11217",
  "parcelId": "3003880020",
  "latitude": 40.683782,
  "longitude": -73.982946,
  "contractorName": "DEVELOPING NY STATE LLC",
  "contractorLicense": "612632",
  "ownerName": "DEVELOPING NY STATE",
  "applicantName": "YITCHOK KATZ",
  "sourceApi": "socrata",
  "sourceUrl": "https://data.cityofnewyork.us/d/rbx6-tga4",
  "sourceRecordId": "B01186301-S6-MS-1",
  "fetchedAt": "2026-10-16T13:25:32.995Z"
}
```

Dates are ISO-8601 as published (date-only when the portal stores midnight). `estimatedCost` is a number in USD or `null` when the jurisdiction publishes no valuation (DC, Philadelphia, Maricopa County). In monitor mode every emitted permit carries `isNew: true`. A jurisdiction that cannot be queried produces one error item instead of failing the run: `{ "jurisdiction": "boston", "error": "HTTP_503", "errorMessage": "...", "sourceApi": "ckan", "fetchedAt": "..." }`.

### Supported jurisdictions

All entries were verified live on 2026-10-16 (dataset id, field mapping, freshness of the newest issued date).

| Id | Jurisdiction | Source (API) | Update frequency / notes |
|---|---|---|---|
| `nyc` | New York City, NY | DOB NOW: Build – Approved Permits `rbx6-tga4` (Socrata) | Daily. Electronic DOB NOW filings; cost, contractor license, owner. |
| `chicago` | Chicago, IL | Building Permits `ydr8-5enu` (Socrata) | Daily. Contractor/owner/applicant from contact roles. |
| `los-angeles` | Los Angeles, CA | LADBS Permits Issued 2020–present `pi9x-tg5x` (Socrata) | Weekly; issued dates lag ~1 week. No contractor/owner columns. |
| `san-francisco` | San Francisco, CA | DBI Building Permits `i98e-djp9` (Socrata) | Daily. One row per permit address. |
| `seattle` | Seattle, WA | SDCI Building Permits `76t5-zqzr` (Socrata) | Daily. Per-record portal links. |
| `austin` | Austin, TX | Issued Construction Permits `3syk-w9eu` (Socrata) | Daily. Contractor + applicant, per-record links. |
| `philadelphia` | Philadelphia, PA | L\&I Permits (CARTO SQL, OpenDataPhilly) | Daily. Contractor + OPA owner; no valuation. |
| `boston` | Boston, MA | Approved Building Permits (CKAN DataStore) | Daily. Applicant, declared valuation. |
| `washington-dc` | Washington, DC | DCRA Building Permits in {year} (ArcGIS) | Daily. Layer per calendar year, resolved at run time; no valuation. |
| `miami-dade-county` | Miami-Dade County, FL | Permits issued by the County, 2 previous years to present (ArcGIS) | Daily. County-issued permits (unincorporated areas), no coordinates. |
| `maricopa-county-az` | Maricopa County, AZ | Building Permits (ArcGIS) | Daily. Unincorporated county only; no valuation. |
| `nashville` | Nashville / Davidson County, TN | Building Permits Issued (ArcGIS) | Daily. |
| `detroit` | Detroit, MI | BSEED Building Permits (ArcGIS) | Daily. No status/contractor columns. |
| `raleigh` | Raleigh, NC | Building Permits (ArcGIS) | Daily. Contractor, parcel owner. |
| `baltimore` | Baltimore, MD | DHCD Building Permits (ArcGIS) | Every 2–4 weeks; issued dates lag up to a month. |
| `cincinnati` | Cincinnati, OH | Building Permits `uhjb-xac9` (Socrata) | Daily. Per-record links. |
| `pittsburgh` | Pittsburgh, PA | PLI Permits (CKAN DataStore, WPRDC) | Daily. Contractor + owner when published. |
| `louisville` | Louisville / Jefferson County, KY | Active Construction Permits (ArcGIS) | Daily. Active permits only; no free-text description. |
| `memphis` | Memphis / Shelby County, TN | DPD Building Permits (ArcGIS) | ~Every 2 weeks; issued dates lag up to ~3 weeks. |
| `new-orleans` | New Orleans, LA | One Stop Permits `rcm3-fn58` (Socrata) | Daily. Contractor, owner, applicant. |
| `tucson` | Tucson, AZ | Residential Permits (ArcGIS) | Daily. Residential layer only. |
| `buffalo` | Buffalo, NY | Permits `9p2d-f3yt` (Socrata) | Daily. Applicant + license number. |
| `orlando` | Orlando, FL | Permit Applications `ryhf-m453` (Socrata) | Daily refresh; issued dates lag ~3 weeks. Contractor + owner. |
| `montgomery-county-md-residential` | Montgomery County, MD | Residential Permits `m88u-pqki` (Socrata) | Daily. |
| `montgomery-county-md-commercial` | Montgomery County, MD | Commercial Permits `i26v-w6bd` (Socrata) | Daily. |
| `cambridge-ma` | Cambridge, MA | Building Permits: Addition/Alteration `qu2z-8suj` (Socrata) | Daily. Applicant + contractor firm. |

For lagging feeds (Baltimore, Memphis, Orlando, Los Angeles) use `issuedWithinDays` of 30–45 so the window overlaps the publication delay. Not included on purpose: Dallas (dataset frozen since 2020), Denver (empty layers), Virginia Beach (not updated since July 2026), the NYC legacy BIS feed (free-text dates, 15–30 s queries), and Mesa, San Diego and King County (no queryable API; see "Phase B" in `docs/listing.md`).

### Monitor mode

With `onlyNewSinceLastRun`, the Actor keeps a seen-set of `sourceRecordId`s per jurisdiction in the state store (max 50,000 per jurisdiction, oldest pruned first) and emits only permits it has not seen before, flagged `isNew: true`. The **first run is a baseline**: it emits and charges for every permit in the window. Permits that drop out of the date window and reappear are not reported again. Use one state store name per schedule if you run different filters against the same jurisdiction.

### Pricing

Pay per event. You only pay for what the Actor produces; set a **maximum cost per run** in the run options to cap spend.

| Event | Price | Notes |
|---|---|---|
| Permit record scraped | $0.003 | Per dataset item, including error items for jurisdictions that failed |
| Jurisdiction crawled | $0.01 | Once per jurisdiction per run, regardless of result count |
| Actor start | $0.00005 | Apify platform event |

**Examples:** 1,000 permits ≈ **$3.00**; a daily check of 10 cities ≈ **$0.10** + $0.003 per new permit found. The default input (2 jurisdictions × 25 permits) costs $0.17. Monitor runs that find nothing new cost only the per-jurisdiction fee.

### Limits and fair use

- Requests are rate-limited to at most 2 per second per portal, with exponential backoff on 429/5xx; three jurisdictions are crawled in parallel.
- Page sizes are 1,000 rows; a jurisdiction with 20,000 permits in the window takes about 20 requests.
- Only public open-data endpoints are used. No login, no proxies, no browsers. Socrata portals allow anonymous use; add a free app token for heavy daily use.
- Each portal defines its own permit-type vocabulary; use generic words in `permitTypes` and check `permitType`/`permitSubtype` values in a first run.
- Coverage is the jurisdiction's own dataset: county entries cover county-issued permits, Louisville publishes active permits only, Tucson residential only.

### Related Actors

Other pay-per-event Actors from the same account, all limited-permission and agent-friendly:

- [US Professional License Lookup & Verification API](https://apify.com/owlabee/us-license-verifier) — check the contractor license on a permit against Texas, Florida, Colorado, Washington and NPI records.
- [US Government Grants & Contract Opportunities Feed](https://apify.com/owlabee/gov-opportunity-feed) — federal grants, SAM.gov notices and USAspending awards with new/updated flags.
- [Domain WHOIS, DNS, Email Security & TLS Checker](https://apify.com/owlabee/domain-intelligence) — enrich contractor and developer domains with WHOIS, DNS, email security and tech stack hints.

### FAQ

**Does this need proxies or an API key?** No. It calls public open-data APIs over plain HTTPS from Apify's standard infrastructure. A Socrata app token is optional and only raises rate limits.

**How is cost controlled?** Set `maxResultsPerJurisdiction`, narrow `issuedWithinDays` and filters, and set the run's maximum total charge; the Actor stops cleanly when any limit is reached. In monitor mode you pay per *new* permit only.

**Why does a permit have `estimatedCost: null` or no contractor?** The jurisdiction does not publish that column (see the table). Turn on `includeRaw` to see every field the portal does publish.

**Can I get only solar or roofing permits?** Yes: `permitTypes: ["solar","roof"]` and/or `keyword: "solar"`. Type filters match the portal's type and subtype labels; the keyword matches the free-text work description, which is where most solar/re-roof jobs are described.

**Can I run it on a schedule or from an AI agent?** Yes — Apify Schedules, the API, or the Apify MCP server. The Actor is pay-per-event and uses limited permissions, so agents can discover and call it. Example prompts: "List building permits issued in Chicago this week with an estimated cost over $50,000", "Every morning send me new roofing permits in Austin and Raleigh", "Which contractors pulled the most solar permits in Philadelphia in the last 30 days?".

**What happens when a city portal is down?** That jurisdiction yields one error item (charged like a permit) and the rest of the run continues; the daily Store test therefore never fails on a single portal outage.

**Can you add my city?** Open an issue with the portal URL. Any Socrata, CKAN DataStore, ArcGIS FeatureServer/MapServer or CARTO dataset with an issued-date column can be added as a registry entry.

### Legal

All data comes from public records published by the jurisdictions themselves under their open-data terms; this Actor does not access permit portals that require login or forbid automated access. Owner, applicant and contractor names are reproduced exactly as published by the jurisdiction and may be stale or incomplete. Use the data in compliance with the source's terms and applicable law. This Actor is not a consumer reporting agency and its output is not a consumer report under the Fair Credit Reporting Act (FCRA); it must not be used for employment, credit, insurance, housing or tenant screening decisions, or for any purpose that requires FCRA compliance. Respect do-not-contact rules and telemarketing laws when using the records for outreach.

# Actor input Schema

## `jurisdictions` (type: `array`):

Cities and counties to query. Each entry is one open-data portal (Socrata, CKAN, ArcGIS or Carto). Pick "All supported jurisdictions" to crawl every registry entry. Each jurisdiction crawled is one paid event; see the README table for source, freshness and update frequency per jurisdiction.

## `issuedWithinDays` (type: `integer`):

Only permits whose issued date falls in the last N days (1–3650). Applied server-side on every portal that supports it, so narrow windows are fast and cheap.

## `permitTypes` (type: `array`):

Keep only permits whose type or subtype contains one of these words, case-insensitive (for example "electrical", "roof", "solar", "demolition", "new construction"). Each portal uses its own type vocabulary, so prefer generic words. Applied server-side where the API allows, otherwise client-side.

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

Keep only permits whose work description contains this text, case-insensitive (for example "solar", "re-roof", "kitchen", "ADU"). Applied server-side where the API allows, otherwise client-side.

## `minEstimatedCost` (type: `integer`):

Drop permits whose declared valuation / estimated cost is below this amount. Permits with no published cost are kept (cost is null) so you do not silently lose jurisdictions that do not publish valuations.

## `maxResultsPerJurisdiction` (type: `integer`):

Upper bound on permit records saved per jurisdiction per run (newest first). Use it together with the run's maximum cost to control spend.

## `onlyNewSinceLastRun` (type: `boolean`):

Monitor mode for schedules: remember the permit numbers already seen per jurisdiction (up to 50,000 each) in the state store and emit only permits not seen before, flagged isNew: true. The first run is a baseline and emits (and charges for) everything in the window.

## `stateStoreName` (type: `string`):

Named key-value store that holds the seen-permit sets for monitor mode. Use a different name per schedule if two schedules query the same jurisdiction with different filters.

## `includeRaw` (type: `boolean`):

Attach the untouched source row as "raw" on every permit, so you can read jurisdiction-specific fields (fees, units, zoning, inspector...) that the normalized schema does not carry. Roughly doubles dataset size.

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

Optional free Socrata application token (sign up on any data.\*.gov portal → Developer Settings). Raises the per-IP rate limit for the Socrata-backed jurisdictions. Works without it. Stored encrypted and never logged.

## `webhookUrl` (type: `string`):

If set, the Actor POSTs a JSON run summary plus the emitted permits (max 500; only the new ones in monitor mode) to this URL when it finishes. Failures are logged, never fatal.

## Actor input object example

```json
{
  "jurisdictions": [
    "chicago",
    "seattle"
  ],
  "issuedWithinDays": 7,
  "maxResultsPerJurisdiction": 25,
  "onlyNewSinceLastRun": false,
  "stateStoreName": "building-permits-scraper-state",
  "includeRaw": false
}
```

# Actor output Schema

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

One item per building permit in the default dataset (jurisdictionName, permitNumber, permitType, issuedDate, estimatedCost, address, contractorName, status, isNew, sourceUrl; error items carry error and errorMessage). Fields are described in dataset_schema.json.

## `summary` (type: `string`):

SUMMARY record in the run's default key-value store: jurisdictions queried, permits produced, charges and webhook delivery status, written when the run finishes.

# 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 = {
    "jurisdictions": [
        "chicago",
        "seattle"
    ],
    "issuedWithinDays": 7,
    "maxResultsPerJurisdiction": 25,
    "onlyNewSinceLastRun": false,
    "includeRaw": false
};

// Run the Actor and wait for it to finish
const run = await client.actor("owlabee/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 = {
    "jurisdictions": [
        "chicago",
        "seattle",
    ],
    "issuedWithinDays": 7,
    "maxResultsPerJurisdiction": 25,
    "onlyNewSinceLastRun": False,
    "includeRaw": False,
}

# Run the Actor and wait for it to finish
run = client.actor("owlabee/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 '{
  "jurisdictions": [
    "chicago",
    "seattle"
  ],
  "issuedWithinDays": 7,
  "maxResultsPerJurisdiction": 25,
  "onlyNewSinceLastRun": false,
  "includeRaw": false
}' |
apify call owlabee/building-permits-scraper --silent --output-dataset

```

## MCP server setup

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