US Building Permits Scraper
Building permits from 14 US cities and counties: permit number, type, status, issue date, address, work description, declared value and contractor.
How it works
- 1Open it on Apify
Hit Run on Apify — it opens the tool in the cloud, no install.
- 2Set the inputs
Adjust
jurisdictions,issuedAfter,issuedBefore(sensible defaults are pre-filled). - 3Click Run
The tool runs on Apify’s cloud and collects the data for you.
- 4Export the results
Download as JSON, CSV or Excel, or pipe straight into your app, Google Sheets, or an AI agent.
Pricing
$0.00185 per permit = $1.85 per 1,000
| You are charged for | When | Price |
|---|---|---|
| Permit scraped | One building permit with its type, status, value and address. Jurisdictions that cannot answer a filter are skipped and never charged. | $0.00185 |
Pay-per-event pricing: you are billed per result, not per subscription. Billing is handled by Apify on your own account. These are the live Apify store prices, in effect since 2026-09-20, and they are what you are actually charged.
Inputs
| Field | What it does | Type |
|---|---|---|
jurisdictions | Which permit rolls to read. Leave empty to read all fourteen. Keys: new-york-city, chicago, los-angeles, austin, seattle, new-orleans, cincinnati, mesa-az, orlando, baton-rouge, cleveland, columbus-oh, raleigh, nashville. | array |
issuedAfter | Only permits issued on or after this date, as YYYY-MM-DD. This is the filter that makes the feed useful as a leading indicator: a permit issued this week is a job starting next month. Every jurisdiction supports it. | string |
issuedBefore | Only permits issued on or before this date, as YYYY-MM-DD. Combine with the field above for a fixed window. | string |
permitNumbers | Up to 50 permit numbers. When this is filled in, every other filter is ignored so you get exactly those permits. The number has to match the jurisdiction's own formatting. | array |
addressContains | Match part of the site address, case-insensitive - a street name such as BROADWAY, or a house number and street together. Matched against whatever address text the jurisdiction publishes, which on some rolls is the street name only. | string |
city | Narrow to one city. On a roll that covers a single city already, this either matches that city or the jurisdiction is skipped and nothing is charged for it. | string |
zipCode | A five-digit US postal code. Four of the fourteen rolls publish no postal code column; those are skipped, free, with a row saying so. | string |
permitTypeContains | Match part of the permit type or work class, case-insensitive - ROOF, SOLAR, ELECTRIC, DEMOLITION, NEW. Each city names its own types, so try a short word rather than a whole phrase. | string |
descriptionContains | Match part of the free-text scope of work, case-insensitive - SOLAR, ROOF REPLACEMENT, KITCHEN. Orlando publishes no scope-of-work text and is skipped, free, when you use this. | string |
contractorContains | Match part of the contractor or permit-holder business name. Los Angeles and Cincinnati do not name the contractor on their permit roll and are skipped, free, when you use this. | string |
statusContains | Match part of the permit status - ISSUED, FINAL, EXPIRED. Each city has its own vocabulary. Baton Rouge and Nashville publish no status and are skipped, free, when you use this. | string |
minValueUsd | Only permits whose declared construction value is above this figure, in US dollars. Four rolls publish that value as text rather than a number, so they cannot be compared against it and are skipped, free, with a row saying so. | integer |
maxItems | Total permits to return across every jurisdiction you picked. The budget is shared evenly first, then the rolls that still have rows take whatever the short ones did not use. Default 50, hard ceiling 20,000. Keep it low while testing - you pay per row. | integer |
proxyUrls | Leave this empty. The run brings its own addresses and the cost of them is already inside the price you see. Fill it in only if your own network policy requires the traffic to leave through servers you already pay for, as http://user:pass@host:port. | array |
What you get
A structured dataset — each result includes fields like:
jurisdictionjurisdictionKeypermitNumberpermitTypeworkClassstatusissuedDateappliedDateexpiresDateaddresscitystatezipdescriptionExport every run as JSON, CSV or Excel, or send it to your app, a database, Google Sheets, or an AI agent.
Related tools in Developer & Research Tools
Other ready-to-run tools in the same category — all pay-per-use on the Apify cloud.
GitHub Scraper
Search GitHub repos and users: stars, forks, language, topics, licence, plus user bio, company and followers. No token needed. $0.90 per 1,000 rows.
Stack Overflow / Stack Exchange Scraper
Search Stack Overflow and Stack Exchange by keyword or tag. Score, answer count, views, reputation and body text. $2 per 1,000 questions.
Package Registry Scraper (npm + PyPI)
Get npm and PyPI package metadata as JSON. Version, license, author, repo, keywords and npm monthly downloads. $2 per 1,000 packages.
arXiv Scraper
Search arXiv papers by title, author, abstract or category. Get full abstracts, authors, categories, DOI, dates and PDF links. $2 per 1,000 papers.
OpenAlex Scholarly Works Scraper
Search 250M+ OpenAlex papers with no API key. Get titles, authors, venue, year, citations, DOI, OA links and full abstracts. $2.00 per 1,000 papers.
Crossref Scholarly Works Scraper
Search 150M+ papers on Crossref: DOI, title, authors, journal, publisher, date, citations and abstract. No API key. $1.00 per 1,000 works.
Where this tool sits
- Categories
- Developer & Research Tools
US Building Permits Scraper
A permit issued this week is a job starting next month. This reads fourteen US city and county permit rolls, 7,984,114 permits between them, and gives you one row each: permit number, type, status, issue date, site address, what the work actually is, the declared value and the contractor. Public records, read straight from the government host. No account, no key, no browser.
- Fourteen jurisdictions across twelve states. The full list, with the number of permits on each roll, is below. So is the list of the cities that are NOT in it.
- Filter by issue date and you get a working leading indicator: everything permitted since a date you pick, newest first.
- A jurisdiction that cannot answer your filter is skipped, free, with a row telling you which filter and why. It is never handed a looser question and billed for the answer.
- One row per permit, same field names in every city, so Cleveland and Austin load into the same table without cleaning.
- No phone numbers, no email addresses and no owner names, even where a city publishes them.
Price
$1.85 per 1,000 permits, plus a $0.0005 start fee per run.
This is a flat rate on every plan, free or paid. There are no volume tiers, no minimum spend, no subscription and no add-on fees. What you read here is what you pay on day one and on day four hundred.
| Permits | Total cost |
|---|---|
| 100 | $0.1855 |
| 1,000 | $1.8505 |
| 10,000 | $18.5005 |
| 100,000 | $185.0005 |
What is actually charged
- One
permit-scrapedevent per permit row written to the dataset. Nothing else is metered per row. - Free: the sample row an empty run returns, and every diagnostic row — a blocked target, a dead URL, a search that matched nothing. Those rows all carry
"charged": false. - A jurisdiction skipped because it cannot honour one of your filters. You get a row saying which, and pay nothing for it.
- A row from a roll that came back without a permit number or an issue date. It is dropped before it is charged, because it is not a permit.
- Duplicates within one run. The same permit cannot be charged twice in a single run.
- A run that finds nothing costs the start fee and nothing more.
- Rows never leave the dataset without a charge, and are never charged without a row. The billed event is a named one, so there is no price quietly attached to
apify-default-dataset-item— the trick that makes some scrapers bill you for their own error messages.
Input
{
"jurisdictions": [
"seattle",
"raleigh",
"cleveland"
],
"issuedAfter": "2026-09-01",
"permitTypeContains": "ROOF",
"minValueUsd": 10000,
"maxItems": 200
}
| Field | What it does |
|---|---|
jurisdictions | Which rolls to read, by key. Leave it empty to read all fourteen. The keys are in the coverage table below. |
issuedAfter | Only permits issued on or after this date, YYYY-MM-DD. Every jurisdiction supports it. This is the field that turns the Actor into a feed. |
issuedBefore | The other end of the window, same format. |
permitNumbers | Up to 50 permit numbers for a direct lookup. When this is set every other filter is ignored, so you get exactly those permits. |
addressContains | Part of the site address, case-insensitive. Some rolls publish a full address and some publish only the street name, so a street name matches more widely than a house number. |
city | Narrow to one city. On a roll that already covers a single city this either matches or the roll is skipped, free. |
zipCode | A five-digit postal code. Some rolls do not publish one; those are skipped, free. |
permitTypeContains | Part of the permit type or work class: ROOF, SOLAR, DEMOLITION, NEW. Every city names its own types, so a short word travels further than a phrase. |
descriptionContains | Part of the free-text scope of work: SOLAR, KITCHEN, TEAR OFF. Orlando publishes no scope text and is skipped, free, when you use this. |
contractorContains | Part of the contractor or permit-holder business name. Los Angeles and Cincinnati do not name the contractor and are skipped, free. |
statusContains | Part of the permit status: ISSUED, FINAL, EXPIRED. Each city has its own vocabulary; see the field notes. |
minValueUsd | Only permits whose declared value is above this figure. Three rolls store that value as text and cannot compare it to a number; they are skipped, free, with a row saying so. |
maxItems | Total permits across every jurisdiction you picked. The budget is split evenly first, then whatever the short rolls did not use goes to the ones that still have rows. Default 50, ceiling 20,000. |
proxyUrls | Leave it empty. The run brings its own addresses and the cost of them is already inside the price you see. Fill it in only if your own network policy requires traffic to leave through servers you already own. |
Run it with empty input and you get one clearly labelled sample row, free, so you can see the output shape before you spend anything.
Output
One row per permit. A real row from a real run:
{
"ok": true,
"charged": true,
"recordType": "permit",
"jurisdiction": "Raleigh, NC",
"jurisdictionKey": "raleigh",
"permitNumber": "BLDR-032987-2026",
"permitType": "Building",
"workClass": "Residential New Residential Dwelling",
"status": "Issued",
"issuedDate": "2026-09-17",
"appliedDate": "2026-07-08",
"expiresDate": "2027-03-16",
"address": "404 Bickett Blvd",
"city": "RALEIGH",
"state": "NC",
"zip": "27608",
"description": "New Single Family Dwelling Lot 1 Bickett Blvd Sealed Crawl Rear Deck Fireplace in Living Room DEMO-0",
"declaredValueUsd": 419795.64,
"contractorName": "Ravenscape Construction, LLC",
"parcelId": "1704652944",
"latitude": 35.84738770690897,
"longitude": -78.51643665136687,
"publisher": "City of Raleigh Development Services",
"scrapedAt": "2026-09-20T10:31:37.103Z"
}
Field notes
permitNumber— the jurisdiction’s own number, in its own format, and stable between runs. It is not unique on its own: two cities use the same number, and one number can cover several work types. Measured on a 1,400-row pull, 11 rows shared a number with another row and none of them were duplicates. The safe key isjurisdictionKey+permitNumber+permitType.permitType— the city’s own wording, not a normalised label. Seattle says "Addition/Alteration", Chicago says "PERMIT - RENOVATION/ALTERATION", Austin says "Electrical Permit". Filter with a short substring.status— again the city’s own vocabulary. Seattle uses Issued / Completed / Expired, Cincinnati uses ISSUED / CLOSED, New York uses Permit Issued / Signed-off. Two rolls publish no status at all and it comes back null.issuedDate— the date the permit was issued, normalised toYYYY-MM-DD. A row without one is never delivered, because a permit with no issue date cannot be part of a date window.description— the scope of work as the applicant wrote it. Length and quality vary enormously — Mesa runs to a paragraph of code references, Cincinnati sometimes says "Alteration". Columbus publishes a work category rather than free text, and Orlando publishes none.declaredValueUsd— what the applicant declared the work is worth, in dollars. It is a declaration, not an appraisal, and cities do not audit it. Zero is treated as "not stated" and returned as null. Roughly a fifth of Austin’s recent permits carry one at all, because most are trade permits with no declared value.contractorName— the contractor or permit-holder business name where the city publishes it. Seattle fills this in on well under a fifth of its permits; New York names the permittee business on effectively all of them.recordUrl— the city’s own page for that permit, where one is published. Six of the fourteen do.latitude / longitude— as published by the city, not geocoded here. Null where the city does not publish coordinates.
Every real row carries "charged": true. Sample rows carry "_sample": true and diagnostic rows carry "_diagnostic": true with an errorCode you can filter on, and neither is ever billed.
How it works
- Each jurisdiction publishes its permit roll as a public open-data table, and the run reads that table over plain HTTP. No page rendering, no headless browser, no login, no key.
- It asks for the columns that end up in a row and nothing else. Asking for every column costs two to three times the bandwidth on most of these rolls, and on one of them the untrimmed record carries a map polygon bigger than the permit itself.
- Results come back newest first, and the query says explicitly that the issue date must not be empty. Six of these rolls otherwise hand back their undated rows first when asked for the newest ones. That would have meant a page of blanks at the top of every feed.
- Before any request is made, the run works out which jurisdictions can actually answer your filters. The ones that cannot are skipped with a written reason and cost you nothing.
- Requests are paced between pages, so a city server that throttles is answered automatically rather than by waiting. These are city servers paid for by residents, not a CDN.
What people use it for
- Roofing, solar, HVAC and remodelling contractors watching for work that has just been permitted in their own area, before it shows up anywhere else.
- Suppliers and distributors sizing next quarter from what has actually been approved rather than from a forecast. Filter by declared value and count the new-build permits.
- Proptech and lending teams flagging renovation activity on a property before it reaches a listing.
- Analysts tracking construction volume in a city over time: run the same date window each month and total
declaredValueUsdby permit type. - Anyone who has been checking four different city portals by hand every Monday morning and would rather have one table.
Which cities are in it
| Jurisdiction | Key | Permits on the roll | Published by |
|---|---|---|---|
| New York City, NY | new-york-city | 1,002,770 | NYC Department of Buildings (DOB NOW: Build) |
| Chicago, IL | chicago | 847,927 | City of Chicago Department of Buildings |
| Los Angeles, CA | los-angeles | 412,133 | Los Angeles Department of Building and Safety |
| Austin, TX | austin | 2,376,402 | City of Austin Development Services Department |
| Seattle, WA | seattle | 193,083 | Seattle Department of Construction and Inspections |
| New Orleans, LA | new-orleans | 464,215 | City of New Orleans Department of Safety and Permits |
| Cincinnati, OH | cincinnati | 180,257 | City of Cincinnati Buildings and Inspections |
| Mesa, AZ | mesa-az | 156,208 | City of Mesa Development Services |
| Orlando, FL | orlando | 1,110,846 | City of Orlando Permitting Services |
| Baton Rouge, LA | baton-rouge | 143,535 | City of Baton Rouge / Parish of East Baton Rouge |
| Cleveland, OH | cleveland | 200,435 | City of Cleveland Department of Building and Housing |
| Columbus, OH | columbus-oh | 682,606 | City of Columbus Department of Building and Zoning Services |
| Raleigh, NC | raleigh | 184,185 | City of Raleigh Development Services |
| Nashville, TN | nashville | 29,512 | Metropolitan Government of Nashville and Davidson County |
| Total | 7,984,114 |
Those counts were read off the live rolls on 20 September 2026. They move as cities publish, and a roll that carries decades of history will contain far more old permits than recent ones, so sort or filter by issuedDate if you only want what is current.
Two of these are not city-wide in the way the name suggests, and it is worth knowing which. Seattle covers building permits only. The city files electrical, trade and land-use permits in separate tables that are not read here. Nashville publishes a rolling recent window rather than full history.
Which cities are NOT in it
This is the part most listings leave out, so here it is plainly. Thirty-two US jurisdictions were probed for this Actor and eighteen were dropped. San Francisco, Boston, Philadelphia, Denver, Phoenix, Houston, Dallas, San Antonio, San Diego, Miami, Detroit, Baltimore, Washington DC, Kansas City and Honolulu are not covered, along with every jurisdiction not in the table above.
The reasons were mostly the same four:
- The roll is stale. Two published nothing at all in 2026; one stops in mid-2025. A permit feed that is a year behind is not a leading indicator.
- The host will not let a request name the columns it wants. One large city answers a plain row fetch but returns 403 to any query that asks for a named list of fields, checked twice an hour apart and from four different clients. Reading it would mean pulling every column of every row, at two to three times the bandwidth, which is not a cost worth passing on.
- The roll is too thin to be a permit record. Several publish a date and an address and nothing else: no permit number, no type, no status, no value, no scope of work.
- The roll is split into a dataset per year, which is a different product to maintain and a worse one to query.
If a city you need is missing, it is missing on purpose and the reason is one of those four. Ask and it can be re-checked. These rolls do improve.
What each roll leaves out
Every jurisdiction publishes a slightly different set of columns, and no amount of normalising invents a field a city does not publish. Rather than quietly returning nulls, here is exactly where the gaps are:
| Jurisdiction | What it does not publish |
|---|---|
| Los Angeles | contractor name |
| Cincinnati | contractor name (the city files permit contacts in a separate table, and this Actor does not join tables) |
| Orlando | scope-of-work text |
| Baton Rouge | permit status |
| Nashville | permit status |
| Chicago, Mesa, Orlando | postal code |
| Cincinnati, Cleveland | postal code on some rows only |
| New York City, Los Angeles, Cincinnati | a numeric declared value you can filter on. The value is there, it is just stored as text |
Where a filter needs a column a roll does not have, that roll is skipped and not charged, and you get a diagnostic row naming the jurisdiction and the filter. It is never given a looser query so it has something to bill you for.
Two fill rates worth knowing before you build a workflow on them: Seattle names the contractor on well under a fifth of its permits, and about a fifth of Austin’s recent permits carry a declared value, because most of them are trade permits that do not require one.
About the people named in these records
Permits are public records, and a permit names people. The applicant is often a company, but on a homeowner permit it is a private individual at their own address.
So this Actor ships the permit, and only the permit. It reads no phone numbers, no email addresses and no owner names, even on the rolls that publish all three in the same row. Where a city’s only free-text field is a project title that is usually the homeowner’s name, that field is not read either, which is why Orlando comes back with no scope-of-work text. Chicago files everyone attached to a permit as a numbered contact list; only entries typed as a contractor are read, and "owner as general contractor" is skipped.
Nothing is joined to anything else and nothing is enriched. A row is what one city published about one permit, with the contact details left off.
Reading the output
Every run writes three kinds of row and they are easy to tell apart:
- Real rows carry
"charged": trueand"recordType": "permit". One billed event each. - The sample row carries
"_sample": trueand"charged": false. There is exactly one, it only appears when the input had nothing to search for, and it is there so you can see the shape before you spend anything. - Diagnostic rows carry
"_diagnostic": true,"charged": falseand anerrorCodeyou can switch on:FILTER_UNSUPPORTEDwhen a jurisdiction was skipped because it cannot answer one of your filters,NO_RESULTSwhen a roll had nothing matching,BAD_INPUTwhen a host refused the query,RATE_LIMITEDorSERVER_ERRORwhen a city server was having a bad day,TIME_BUDGETwhen the run ran out of time. Each carries thejurisdictionit belongs to and a plain-English explanation.
If you only want the data, filter on charged == true. The count of those rows always equals the number of events you were billed for, so the dataset is its own invoice.
Limitations
- Fourteen jurisdictions, not fifty states. The full list is above, and so is the list of the big cities that are deliberately not in it.
- Each city’s vocabulary is its own.
permitTypeandstatusare not normalised across cities, because flattening "PERMIT - RENOVATION/ALTERATION" and "Addition/Alteration" into one label would throw away what the city actually said. Filter with short substrings. declaredValueUsdis what the applicant declared, not an appraisal and not a contract price. Cities do not audit it, and a wrong figure on a city roll comes through as a wrong figure here.- Contractor coverage is patchy and it varies by city rather than by permit. Two rolls do not name the contractor at all; Seattle names one on well under a fifth of its permits.
- Three rolls publish the declared value as text, so
minValueUsdcannot be applied to them and they are skipped rather than answered loosely. - Two rolls publish no permit status, one publishes no scope-of-work text, and three publish no postal code.
- This reads what the city published, at the moment it published it. A permit revised or revoked after the roll was last refreshed still reads as it did then, and refresh cadence is the city’s, not ours.
- Address text is whatever the city stores. Some rolls hold a full street address, some hold the street name in one column and the number in another, and one holds city and postal code inside the same string.
addressContainsmatches the text that roll actually publishes. - No geocoding, no owner lookup, no contact enrichment, no joining to assessment or parcel data. One permit in, one row out.
- The ceiling is 20,000 rows and 50 permit numbers per run. For more than that, split it across runs by date window.
- A permit that appears twice in one run is charged once. Across two runs it is charged twice, so de-duplicate on
jurisdictionKey+permitNumber+permitTypeif you schedule it. - One permit number can cover more than one line of work, and each line is its own row. New York files a sprinkler job under both "Plumbing" and "Sprinklers" on the same number, and Cincinnati files the building line and the excavation line separately. They are not duplicates and the declared value can differ between them.
Questions
How current is the data?
It is whatever the city last published. On most of these rolls that means permits issued within the last day or two; the newest issue date on each one at the time of writing was between 4 and 18 September 2026. Cities refresh on their own schedule and a long weekend shows up as a gap.
What happens if a city cannot answer one of my filters?
It is skipped and you are not charged for it. You get an uncharged row with errorCode: "FILTER_UNSUPPORTED" naming the jurisdiction and the filter, so you know exactly what you did not get and why. The run carries on with the cities that can answer.
Can I get a daily feed of new permits?
Yes, and it is the main way people run it. Schedule it with issuedAfter set to a recent date and de-duplicate on jurisdictionKey + permitNumber + permitType against what you already have. Nothing is held between runs, so the same input is always safe to repeat.
Why is contractorName empty on some rows?
Because the city did not publish it. Los Angeles and Cincinnati do not name the contractor on their permit roll at all, and Seattle publishes the column but fills it in on well under a fifth of its permits. Nothing is inferred to fill the gap.
Do I get phone numbers or owner names?
No, deliberately. Several of these rolls publish a contractor phone number, an email address or the homeowner’s name beside their home address, and none of it is read. This returns the permit, not a contact list.
Will the run fail if a city server is down?
No. That city produces an uncharged diagnostic row saying what happened, the other thirteen carry on, and the run finishes as succeeded. A failed run would still bill you the start fee, which would mean paying to be told something went wrong.
Why is the same address in there several times?
Because a single job often needs several permits: a building permit, then electrical, then plumbing, then mechanical. They are separate permits with separate numbers and they are separate rows. In Austin in particular, trade permits outnumber building permits several to one.
Can I search one specific permit?
Yes. Put its number in permitNumbers and every other filter is ignored, so you get that permit rather than that permit plus this month’s date window. Up to 50 numbers in one run.
Do I need a proxy?
No. These are public government open-data hosts with no bot wall, and the run brings its own addresses at no extra cost to you. The proxyUrls field exists only for callers whose own network policy requires traffic to leave through servers they already own.