US Building Permits Scraper — 45 Cities, 22 States avatar

US Building Permits Scraper — 45 Cities, 22 States

Pricing

from $4.80 / 1,000 building permit records

Go to Apify Store
US Building Permits Scraper — 45 Cities, 22 States

US Building Permits Scraper — 45 Cities, 22 States

Issued US building permits from 45 jurisdictions in 22 states — NYC, Chicago, LA County, San Francisco, Austin, Seattle. One normalised trade vocabulary, valuation with its basis, contractor, address, dates, and a named reason for every empty field. Pick cities or states, pick a date window.

Pricing

from $4.80 / 1,000 building permit records

Rating

0.0

(0)

Developer

Vladimir Chernykh

Vladimir Chernykh

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

15 minutes ago

Last modified

Share

US Building Permits Scraper — Contractor, Trade & Valuation

Building permits from 45 open municipal permit systems in 22 US states, normalised to one record shape and one trade vocabulary. 12.6 million permits of history sit behind these layers; a run returns the fresh ones.

This is a building permit lead feed, not a permit dump. Every record answers the four questions a contractor, roofer, solar installer or supplier actually pays for: who is doing the work, what trade it is, how much it is worth, and where.

Why it is not just "permit records"

A jurisdiction's permit type is not a trade. Columbus writes Roof, Siding, Windows, Doors, Nashville Building Residential - New, Anaheim Residential Remodel. The field workType maps all of them onto one small vocabulary (roofing, solar, hvac, electrical, plumbing, new-construction, remodel, demolition, signage, other), which is what lets someone who sells roofs filter for roofs in every city at once.

valuation never travels alone: valuationBasis says whether it is a declared job value or a construction cost. A $450 000 construction cost and a $2 685 job value are not the same number, and contractor totals never add them together silently — the portfolio row sets valuationBasisMixed instead.

Coverage — 45 jurisdictions, 22 states

StateJurisdictionsStateJurisdictions
AZ3MD2
CA5MI1
CO4NC1
CT1NM1
FL4NY1
ID1OH4
IL2OR1
KS1PA1
KY1TN2
LA1TX4
MA1WA3

Every jurisdiction, its history size and the API it comes from:

City IDJurisdictionStatePermits on recordAPI
us-tx-austinCity of AustinTX2 383 197Socrata
us-ca-san-franciscoCity and County of San FranciscoCA1 297 749Socrata
us-ny-nycNew York CityNY1 023 760Socrata
us-ca-la-countyLos Angeles County (unincorporated + contract cities)CA928 761ArcGIS
us-il-chicagoCity of ChicagoIL851 889Socrata
us-oh-columbusCity of ColumbusOH688 315ArcGIS
us-fl-miami-dadeMiami-Dade CountyFL487 686ArcGIS
us-wa-bellevueCity of BellevueWA445 189ArcGIS
us-la-new-orleansCity of New OrleansLA349 683Socrata
us-co-boulderCity of BoulderCO337 797ArcGIS
us-md-baltimore-cityBaltimore CityMD295 869ArcGIS
us-co-douglasDouglas CountyCO286 727ArcGIS
us-co-boulder-countyBoulder County (unincorporated)CO272 399ArcGIS
us-md-bpdsMaryland Building Permit Data SystemMD257 533ArcGIS
us-fl-miamiCity of MiamiFL236 389ArcGIS
us-fl-alachuaAlachua CountyFL217 559ArcGIS
us-oh-clevelandCity of ClevelandOH202 564ArcGIS
us-wa-seattleCity of SeattleWA194 053Socrata
us-ca-anaheimCity of AnaheimCA192 331ArcGIS
us-nc-raleighCity of RaleighNC184 893ArcGIS
us-oh-cincinnatiCity of CincinnatiOH181 639Socrata
us-ca-sacramento-countySacramento CountyCA159 396ArcGIS
us-tx-san-antonioCity of San AntonioTX140 143ArcGIS
us-wa-tacomaCity of TacomaWA112 920ArcGIS
us-az-coconinoCoconino CountyAZ111 645ArcGIS
us-az-maricopaMaricopa CountyAZ106 341ArcGIS
us-ma-somervilleCity of SomervilleMA105 594Socrata
us-fl-fort-pierceCity of Fort PierceFL94 909ArcGIS
us-nm-las-crucesCity of Las CrucesNM83 809ArcGIS
us-tx-san-marcosCity of San MarcosTX51 811ArcGIS
us-mi-detroitCity of DetroitMI47 998ArcGIS
us-il-napervilleCity of NapervilleIL38 886ArcGIS
us-ct-hartfordCity of HartfordCT37 207ArcGIS
us-tn-nashvilleMetro Nashville / Davidson CountyTN29 661ArcGIS
us-tn-memphisCity of Memphis / Shelby CountyTN28 404ArcGIS
us-ky-louisvilleLouisville Metro / Jefferson CountyKY22 639ArcGIS
us-az-tempeCity of TempeAZ20 744ArcGIS
us-ks-overland-parkCity of Overland ParkKS14 197ArcGIS
us-tx-kerrvilleCity of KerrvilleTX11 098ArcGIS
us-id-boiseCity of BoiseID11 022ArcGIS
us-ca-placentiaCity of PlacentiaCA7 782ArcGIS
us-oh-oxfordCity of OxfordOH7 460ArcGIS
us-co-aspenCity of AspenCO6 553ArcGIS
us-or-talentCity of TalentOR6 401ArcGIS
us-pa-allentownCity of AllentownPA6 079ArcGIS

37 layers are ArcGIS and 8 are Socrata. The Socrata half is what brought the largest systems into reach, because none of them publish an ArcGIS layer at all.

Verified live on 2026-11-02: all 45 jurisdictions answered, zero failures, 9 123 permits returned for a 7-day window (../../../docs/permits/measurement-notes.md).

What you get — one record per permit

{
"source": "us-oh-columbus",
"state": "OH",
"permitId": "RSWDR2644234",
"permitIdType": "accela-alt-id",
"permitType": "1,2,3 Family",
"permitSubtype": "Roof, Siding, Windows, Doors",
"workType": "roofing",
"status": "Permit Issued",
"jurisdiction": "City of Columbus",
"countyName": "Franklin",
"countyFips": "39049",
"issuedDate": "2026-10-31",
"lastStatusDate": "2026-10-31",
"valuation": 8000,
"valuationBasis": "job-value",
"situsAddress": {
"street1": "3728 PENDLESTONE DR",
"street2": null,
"city": "Columbus",
"state": "OH",
"zip": "43230"
},
"parcelId": "600188600",
"contractorName": "DTE ROOFING LLC",
"scopeOfWork": "Additions, Alterations and Conversions - Residential",
"sqft": 900,
"units": 1,
"completeness": "full",
"omitted": [
{ "field": "ownerName", "reason": "not-published", "note": null },
{ "field": "appliedDate", "reason": "source-lacks-field", "note": "the layer publishes the issue date only" }
]
}
GroupFields
IdentitypermitId, permitIdType, permitType, permitSubtype
TradeworkType, scopeOfWork
Moneyvaluation, valuationBasis
DatesappliedDate, issuedDate, finalizedDate, lastStatusDate
Statusstatus
PlacesitusAddress (street1, street2, city, state, zip), parcelId, countyName, countyFips, jurisdiction, state
PeoplecontractorName, contractorLicense, contractorPhone, ownerName
Sizesqft, units
Provenancesource, sourceApi, sourceLayerId, sourceUrl, scrapedAt, lookupType
Honestycompleteness, omitted[]

Two dataset views ship with it: permits (one row per permit) and portfolios (one row per contractor, free of charge).

omitted — why a field is empty

A missing value is never ambiguous. Every null carries an entry saying why:

ReasonMeaning
source-lacks-fieldthe jurisdiction publishes no such column
not-publishedthe column exists but is empty for this permit
derivedbuilt from several columns (a glued address, a split ZIP)
zero-means-unknownthe source writes 0 where it means "not recorded"

So Nashville's status is null with source-lacks-field, not "cancelled". Anaheim publishes a contractor phone on under 9 % of rows. Both facts are in the dataset.

Inputs — cities, states, and dates

You choose which jurisdictions and which dates. All three selectors work together:

InputWhat it does
citiespick exact jurisdictions by ID (us-tx-austin, us-ny-nyc, …); empty means all 45
statespick whole states (CA, TX, FL, …); combines with cities, a route listed twice is not fetched twice
lastNDayswindow size in days, default 7
issuedAfter / issuedBeforeexact window, YYYY-MM-DD; either alone is honoured, and they win over lastNDays
{ "states": ["TX", "FL"], "lastNDays": 7, "maxResultsPerCity": 1000 }
{ "cities": ["us-ca-san-francisco", "us-il-chicago"], "issuedAfter": "2026-10-01", "issuedBefore": "2026-10-31" }

Four search modes run alongside the window:

InputQuestion it answers
permitIdswhat exactly was filed under this permit number
contractorNameseverything this contractor built, plus a free portfolio row
addressesevery permit ever filed on this address
mode: "new"what was issued since the last run (default)

Window and limits

The ceiling of the window is today, and it is not cosmetic: several layers carry rows dated into the future, and a permit dated tomorrow has not been issued today — returning it on every run would bill the same permit twice.

maxResultsPerCity (default 1000) is per city, so adding cities never shrinks the others. A city that reaches it is marked truncated in the summary, which is how a cut-off stays distinguishable from a quiet city. Two hard ceilings sit above it: maxResultsPerCity cannot exceed 5000, and the whole run stops at 20 000 records.

The run summary lists, for every city, returned, inWindow and truncated — so a buyer can always tell "this suburb issued nothing" from "we cut you off at 1000".

Incremental mode

mode: "new" asks each layer for permits issued inside the window, newest first, and filters out everything an earlier run already pushed. The seen-set lives in the named store us-building-permits-state, one key per route, and the key deliberately does not contain the window — putting lastNDays in the key makes every new window look like a new search and re-charges the same permits.

Filtering happens before maxResultsPerCity is applied, so a page full of already-seen permits cannot starve a run of the new ones behind them.

Cost

$5 per 1000 permits — that is $0.005 per permit, and Apify adds its 20 % margin at checkout. One charged event (permit-scraped) per distinct permit pushed. A lookup that matches nothing is not charged, the contractor portfolio row is free, and a permit already returned by an earlier run is never charged twice.

A 7-day run across all 45 jurisdictions returns roughly 9 000 permits, which is about $45.

Honest limits

  • Nine of the 45 feeds had nothing inside a 7-day window on 2026-11-02. That is the feed, not the actor: Boulder County's application feed lags six months, Aspen publishes closed permits only, Boise covers new housing and demolitions only. Each one still answers.
  • Some columns are fees, not valuations (Bellevue TOTALBILLED, Coconino TotalFees, New Orleans totalfees, Cincinnati fee, Somerville application_amount) — they are not published as valuation and say so in omitted.
  • A column named owner_contractor (Placentia) is published as neither, because the layer never says which of the two it is.
  • New Orleans bldgarea holds only 0 or 4000, so it is a class flag, not an area, and is not published as sqft.
  • Somerville publishes building permits, farmers-market licences and block-party licences in one table. The route carries an internal filter for department = 'ISD Building', so a permit lookup cannot return a licence that shares its digits.

Search terms

building permits, building permit scraper, construction permits, permit data, US building permits, permit leads, contractor leads, roofing permits, solar permits, HVAC permits, electrical permits, plumbing permits, new construction permits, remodel permits, demolition permits, permit valuation, job value, construction cost, issued permits, permit number lookup, contractor license, contractor name, owner name, parcel number, APN, property address, situs address, county FIPS, census tract, New York City permits, NYC DOB permits, Austin permits, Chicago permits, San Francisco permits, Los Angeles County permits, Seattle permits, Columbus Ohio permits, Cleveland permits, Cincinnati permits, Miami-Dade permits, Baltimore permits, Nashville permits, Memphis permits, Detroit permits, Sacramento permits, Anaheim permits, Raleigh permits, Tacoma permits, Bellevue permits, Boulder permits, Tempe permits, Maricopa County permits, Coconino County permits, Douglas County permits, Alachua County permits, lead generation, real estate data, construction intelligence, building activity, permit feed, daily permit feed, incremental scraping

Run locally

npm run stage -- us-building-permits
cd packages/actors/us-building-permits
mkdir -p storage/key_value_stores/default
# then write storage/key_value_stores/default/INPUT.json
APIFY_LOCAL_STORAGE_DIR=./storage node src/main.js

npm run stage inlines packages/core into src/core; without it the import ./core/index.js does not resolve outside the workspace.

The input directory is key_value_stores — plural. The SDK v3 creates it itself and silently ignores a hand-made key_value_store, so a run with a singular-name directory starts on the defaults and looks exactly like "nothing matched": zero records, zero misses, zero errors. That symptom means the input was not read; it never means the layers are empty.

Adding a jurisdiction

Adding a city is a row in src/routes.json, not a new parser, and a test checks the registry. The mapper enforces three rules: only mapped columns are requested (a missing column makes ArcGIS reject the whole query), 0 in a valuation column is "unknown", and every null gets an entry in omitted.

Tests

$node --test packages/actors/us-building-permits/test/*.test.js

408 tests. The fixtures are real rows from the layers, including the awkward ones: the zero valuation, the glued Anaheim address, the Nashville row with no status, the Somerville licence that shares digits with a permit.