Permits and property
Building Permits API: 61 US Open-Data Portals as JSON
Many US cities and counties publish their building permits on open-data portals, but each portal has its own API, column names and permit-type labels. The Building Permits Scraper reads 61 of them and returns every permit in one schema, with a link back to the source row.
Building Permits Scraper — Roofing & Construction Leads. Pay per result; see the Store page for current pricing.
The source and its limits
The data comes from the jurisdictions' own open-data portals, which run on four kinds of software: Socrata (the SODA API), CKAN DataStore, ArcGIS REST and CARTO SQL. Examples are New York City (DOB NOW approved permits), Chicago, Los Angeles (LADBS), Seattle (SDCI), Austin, Philadelphia (L&I, via CARTO), Boston (CKAN), Washington, DC and Miami-Dade County (ArcGIS), plus one state, New Jersey. Each entry was checked live, and the Actor calls the portal APIs directly over HTTPS: no browser, no login, no proxies.
For a place that is not in the list, Socrata auto-discovery sends one request to the public Socrata Discovery API, finds datasets whose name says "permit" for that place, maps their columns by name and crawls them. Records from a discovered dataset carry a mappingConfidence of high, medium or low. Discovery only searches Socrata portals.
Limits worth knowing
- Coverage is each jurisdiction's own dataset. Some publish no valuation (Washington, DC, Philadelphia, Maricopa County) or no contractor columns (Los Angeles, Detroit). Louisville publishes active permits only and Tucson residential permits only.
- Update frequency differs: most portals refresh daily, a few weekly or every few weeks, and some issued dates lag by one to three weeks.
- Requests are limited to two per second per portal. Socrata allows anonymous use; an optional free Socrata app token raises its rate limits.
- Each portal uses its own permit-type vocabulary, so use generic words such as
rooforsolarand checkpermitTypevalues after a first run.
Fields you get
| Field | What it holds |
|---|---|
permitNumber, permitType, permitSubtype | The permit as the jurisdiction labels it |
workDescription, status | Free-text scope of work and the current status |
appliedDate, issuedDate, finalizedDate, expirationDate | ISO-8601 dates as published |
estimatedCost | Declared job value in USD, or null where the jurisdiction publishes none |
address, city, state, zip, parcelId, latitude, longitude | Location of the work |
contractorName, contractorLicense, contractorPhone | Only where the jurisdiction publishes them; nothing is inferred |
ownerName, applicantName | Where published |
sourceApi, sourceUrl, sourceRecordId | Which API answered and a link to the source dataset or row |
isNew | Present in monitor mode for permits not seen on earlier runs |
Example output
One permit record from New York City (from the Actor's README):
{
"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",
"contractorPhone": null,
"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"
}
Use cases
Weekly re-roof lead list
Set permitTypes to ["roof", "re-roof"] and issuedWithinDays to 7 for one city. Each record has the address and, where published, the contractor who pulled the permit.
Daily new-permit feed
Schedule the Actor with onlyNewSinceLastRun and a webhookUrl. The first run is a baseline; later runs send only permits not seen before, flagged isNew: true.
Larger jobs only
Use minEstimatedCost to drop permits below a declared value (permits with no declared cost are kept), for example to find commercial projects for suppliers.
How to run it
You need an Apify account and its API token (Console → Settings → API & Integrations). Each run is billed per result to your account; set a maximum total charge for the run to cap spend.
With curl
The sync endpoint starts a run, waits up to 300 seconds and returns the dataset items. Permits issued in Chicago and Seattle in the last 7 days with a declared cost of at least 50,000 USD:
export APIFY_TOKEN=<your Apify API token>
curl -sS -X POST \
"https://api.apify.com/v2/acts/owlabee~building-permits-scraper/run-sync-get-dataset-items" \
-H "Authorization: Bearer ${APIFY_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"jurisdictions": [
"chicago",
"seattle"
],
"issuedWithinDays": 7,
"minEstimatedCost": 50000,
"maxResultsPerJurisdiction": 12,
"onlyNewSinceLastRun": false,
"includeRaw": false
}'
From an AI agent (MCP)
Apify hosts an MCP server. This config exposes only this Actor to the agent:
{
"mcpServers": {
"owlabee-building-permits-scraper": {
"url": "https://mcp.apify.com/?tools=owlabee/building-permits-scraper",
"headers": {
"Authorization": "Bearer ${APIFY_TOKEN}"
}
}
}
}
Then ask, for example: List the building permits issued in Chicago and Seattle in the last 7 days with an estimated cost above fifty thousand dollars, including the contractor and address.
Python, Node.js and more examples
Runnable curl.sh, python.py, node.mjs and mcp.json files for this Actor are in examples/building-permits-scraper on GitHub.
No code: Zapier, Make or n8n
Apify's official apps on these platforms can run this Actor and pass its results on. Step-by-step recipes: Building Permits to Google Sheets with Make.
FAQ
Do I need an API key for the city portals?
No. The portals are public open-data APIs. A Socrata app token is optional and only raises Socrata's rate limits.
Why is estimatedCost or the contractor empty?
The jurisdiction does not publish that column. Turn on includeRaw to see every field the portal does publish.
My city runs Accela or EnerGov instead of an open-data portal.
Use the Accela Citizen Access and EnerGov permit scraper, which returns the same schema.
What happens when a portal is down?
That jurisdiction produces one error item and the rest of the run continues.
How do I control what a run costs?
Set maxResultsPerJurisdiction, keep the date window short, and set a maximum total charge for the run. In monitor mode you pay only for new permits.