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. New York City, Los Angeles and Cincinnati 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 unless you want the requests to go through proxy servers of your own. Add each one 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: new permits from 14 US cities and counties in one table
Pick the cities, set an issue date, and add a filter if you want one: permit type, scope of work, contractor, status or declared value. You get one row per permit with the number, type, status, issue date, address, scope of work, declared value, contractor and the city's own record link, under the same field names in every city. The awkward part: every city publishes different columns, so a roll that cannot answer one of your filters is skipped, free, with a row saying why, and big cities such as San Francisco, Boston, Houston and Philadelphia are not in it at all.
| Input | Jurisdiction keys, an issue-date window, optional filters on address, city, postal code, type, scope, contractor, status or value, or exact permit numbers |
| Output | One row per permit |
| Ceiling | 20,000 permits and 50 permit numbers per run |
| Account needed | None |
| Price | $1.85 per 1,000 permits, flat on every plan |
🔍 What US Building Permits Scraper does
A permit issued this week is a job starting next month. Each roll is read newest first, so issuedAfter turns the run into a feed of what was just approved.
Before anything is fetched, the run works out which rolls can answer your filters. A roll that cannot, such as one with no postal codes when you filter on a postal code, is skipped with a free FILTER_UNSUPPORTED row naming the roll and the filter. It is never sent a looser question and billed for the answer.
Your row budget is split evenly across the rolls first. Whatever a short roll does not use goes to the ones that still have permits.
No phone numbers, email addresses or owner names are read, even where a city publishes them. Orlando's only free-text field is usually the homeowner's name, so it is not read, which is why Orlando comes back with no scope of work. On Chicago only contacts typed as a contractor are read.
Two rolls are narrower than their name. Seattle covers building permits only, not its electrical, trade or land-use permits. Nashville publishes a rolling recent window rather than its full history.
📥 What you give it
| Field | If you leave it out | What it does |
|---|---|---|
jurisdictions | all fourteen | Keys from the table below. A key that does not exist gets a free BAD_INPUT row listing the real ones. |
issuedAfter | no start date | YYYY-MM-DD. Every roll supports it. |
issuedBefore | no end date | The other end of the window, same format. |
permitNumbers | none | Up to 50 numbers for a direct lookup, in the city's own format. When set, every filter except jurisdictions is ignored, dates included. |
addressContains | any address | Part of the site address, case-insensitive. Some rolls hold only the street name, so a street name matches more widely than a house number. |
city | any city | Narrows to one city. A single-city roll either is that city or is skipped, free. |
zipCode | any | A five-digit postal code. Chicago, Mesa, Orlando and Cleveland cannot be filtered on one and are skipped. |
permitTypeContains | any type | Part of the permit type or work class, such as ROOF, SOLAR, DEMOLITION or NEW. Every city names its own types, so a short word travels further than a phrase. |
descriptionContains | any | Part of the scope of work, such as KITCHEN or TEAR OFF. Orlando is skipped. |
contractorContains | any | Part of the contractor or permit-holder business name. Los Angeles and Cincinnati are skipped. |
statusContains | any | Part of the status, such as ISSUED, FINAL or EXPIRED, in each city's own words. Baton Rouge and Nashville are skipped. |
minValueUsd | no minimum | Only permits declared above this figure, in dollars. New York City, Los Angeles and Cincinnati store the value as text and are skipped. |
maxItems | 50 | Total permits, 1 to 20,000, shared across the rolls. |
proxyUrls | not used | Your own servers to send the traffic through, as http://user:pass@host:port. Leave it empty unless you need that. |
| Jurisdiction | Key | Permits on the roll, 20 Sep 2026 | Skipped when you filter on |
|---|---|---|---|
| New York City, NY | new-york-city | 1,002,770 | declared value |
| Chicago, IL | chicago | 847,927 | postal code |
| Los Angeles, CA | los-angeles | 412,133 | contractor, declared value |
| Austin, TX | austin | 2,376,402 | nothing |
| Seattle, WA | seattle | 193,083 | nothing |
| New Orleans, LA | new-orleans | 464,215 | nothing |
| Cincinnati, OH | cincinnati | 180,257 | contractor, declared value |
| Mesa, AZ | mesa-az | 156,208 | postal code |
| Orlando, FL | orlando | 1,110,846 | scope of work, postal code |
| Baton Rouge, LA | baton-rouge | 143,535 | status |
| Cleveland, OH | cleveland | 200,435 | postal code |
| Columbus, OH | columbus-oh | 682,606 | nothing |
| Raleigh, NC | raleigh | 184,185 | nothing |
| Nashville, TN | nashville | 29,512 | status |
A roll that carries decades of history holds far more old permits than new ones, so set issuedAfter if you only want what is current. The counts move as cities publish.
{
"jurisdictions": ["seattle", "raleigh", "cleveland"],
"issuedAfter": "2026-09-01",
"permitTypeContains": "ROOF",
"minValueUsd": 10000,
"maxItems": 200
}
An input with nothing in it, such as {} sent through the API, returns one free sample row so you can see the shape first.
📤 What you get back
A real row from a real run:
{
"ok": true,
"charged": true,
"recordType": "permit",
"jurisdiction": "Seattle, WA",
"jurisdictionKey": "seattle",
"permitNumber": "7088872-CN",
"permitType": "Addition/Alteration",
"workClass": "Non-Residential Institutional",
"status": "Issued",
"issuedDate": "2026-09-28",
"appliedDate": "2026-06-24",
"expiresDate": "2028-03-28",
"statusDate": null,
"address": "3919 OKANOGAN LN NE",
"city": "SEATTLE",
"state": "WA",
"zip": "98195",
"description": "Shoring and excavation for future Institutional building [UW CHEMICAL SCIENCES] per plan",
"declaredValueUsd": 1885061,
"contractorName": null,
"contractorLicense": null,
"contractorLicenseType": null,
"contractorTrade": null,
"buildingAreaSqFt": null,
"housingUnits": null,
"parcelId": null,
"latitude": 47.65377801,
"longitude": -122.30999134,
"recordUrl": "https://services.seattle.gov/portal/customize/LinkToRecord.aspx?altId=7088872-CN",
"publisher": "Seattle Department of Construction and Inspections",
"scrapedAt": "2026-09-30T02:37:45.865Z"
}
| Field | What it is |
|---|---|
permitNumber | The city's own number. Not unique on its own: two cities can share a number, and one number can cover several lines of work. Key on jurisdictionKey, permitNumber and permitType together. |
permitType, workClass, status | The city's own wording, never normalised. Seattle says "Addition/Alteration", Austin "Plumbing Permit". Filter with a short substring. |
issuedDate | Always filled, as YYYY-MM-DD. A record with no issue date is never delivered. |
description | The scope of work as the applicant wrote it. Columbus gives a work category instead, and Orlando gives nothing. |
declaredValueUsd | What the applicant declared the work is worth. It is not an appraisal and cities do not audit it. A zero comes back as null. |
contractorName, contractorTrade, contractorLicense | The contractor or permit-holder business, where the city publishes one. Seattle fills it on well under a fifth of its permits, as in the row above. |
recordUrl | The city's own page for the permit. Six of the fourteen publish one. |
latitude, longitude | As the city published them, not geocoded here. null where it publishes none. |
🧾 Reading the output
Three kinds of row can share a dataset.
| Row | How to spot it | Charged |
|---|---|---|
| A permit | recordType: "permit", charged: true | yes |
| The sample | recordType: "sample", _sample: true, charged: false | no |
| A diagnostic | recordType: "diagnostic", _diagnostic: true, charged: false, an errorCode | no |
To keep only permits, filter on recordType == "permit". A diagnostic about one roll names it in jurisdiction.
| Code | What it means |
|---|---|
FILTER_UNSUPPORTED | One roll was skipped because it cannot answer a filter you set. The row names the roll and the filter. |
NO_RESULTS | A roll had nothing matching, or none of the rolls you picked can answer your filters. |
BAD_INPUT | A jurisdiction key that does not exist, or a city refused the query as sent. |
RATE_LIMITED, SERVER_ERROR | That city's server was busy or answered with an error. The other rolls carry on. |
NETWORK | A city's server could not be reached, or sent back something unreadable. |
BLOCKED | A city answered, but not with permit records. |
TIME_BUDGET | The run ran out of time before a roll was finished. |
▶️ How to run it
1. Open US Building Permits Scraper and click Try for free. 2. Pick your cities in Jurisdictions. The form arrives with Seattle and Austin chosen. 3. Set Issued on or after. It arrives set to 2026-09-01, so move it to the date you care about. 4. Add a filter such as Permit type contains or Declared value over if you want one, and set Maximum rows. 5. Click Start, then download the dataset as CSV, JSON or Excel.
💰 How much does it cost?
$1.85 per 1,000 permits, which is $0.00185 a permit, flat on every Apify plan. The 200-permit example above comes to about 37 cents.
You pay for permit rows and nothing else per row. The sample row and every diagnostic row are free, and so are rolls skipped for a filter they cannot answer, records missing a permit number or an issue date, and repeats inside one run. If you set a maximum charge on the run, it stops at that figure instead of going past it.
💡 What people use it for
- A roofing, solar or HVAC contractor watching what has just been permitted in their own area, filtered with
permitTypeContains. - A supplier sizing next quarter from what has actually been approved: count new-build permits above a declared value.
- A lender or proptech team spotting renovation work on a property before it reaches a listing.
- Anyone who has been checking four city portals by hand every Monday morning and would rather have one table.
🚧 What it does not do
- Fourteen jurisdictions, not fifty states. San Francisco, Boston, Philadelphia, Denver, Phoenix, Houston, Dallas, San Antonio, San Diego, Miami, Detroit, Baltimore, Washington DC, Kansas City and Honolulu are not covered. The cities that were checked and left out mostly had rolls that were stale, too thin to be a permit record, or split into one dataset per year.
- No contact details. No phones, emails or owner names, and nothing joined to parcel or assessment data.
- No normalised vocabulary. Types and statuses stay in each city's own words.
- No fields a city does not publish. A missing contractor or value is
null, never filled in. - Only as fresh as the city. A permit revised after the roll was last refreshed still reads as it did, and each city refreshes on its own schedule.
- Repeats across runs are billed again. Inside one run a permit is charged once. On a schedule, de-duplicate on
jurisdictionKey,permitNumberandpermitType.
🧭 Which property data scraper do you need?
| If you want | Use |
|---|---|
| Building permits from 14 US cities and counties | This one |
| Who owns a property, from the public assessment roll | US Property Owner & Parcel Records Scraper |
| Homes for sale with price, tax history and schools | Zillow Scraper |
| Listings with the agent and the broker | Realtor.com Scraper |
| Open bids from states, counties, cities and school districts | US Local Bid Portals Scraper |
❓ Questions people ask
How current is the data? Whatever the city last published. On most rolls that means permits issued within the last day or two, and a long weekend shows up as a gap.
Can I get a daily feed of new permits? Yes, and it is the main way people run it. Schedule it with a recent issuedAfter and de-duplicate against what you already have.
Why is contractorName empty on some rows? The city did not publish one. Los Angeles and Cincinnati never name the contractor, and Seattle rarely does.
Why is the same address in there several times? One job often needs several permits: building, then electrical, plumbing and mechanical. Each is its own permit and its own row.
Can I look up one specific permit? Yes. Put its number in permitNumbers and every other filter is ignored.
Do I get phone numbers or owner names? No, on purpose, even on rolls that publish them.
🆘 If something breaks
Open the Issues tab on the actor page and send the run ID with the input you used. The errorCode, jurisdiction and details on a diagnostic row usually name the reason already.