Every construction project in the United States starts the same way: somebody pulls a permit. That
permit becomes a public record on the day it is issued, and most large cities publish it as open data
within a day or two — the address, what is being built, who is building it, and what it is worth.
It is one of the most useful public datasets in the country, and it is almost unusable in aggregate,
because every city publishes it differently.
Why permits matter
A permit is the earliest broadly available signal that something is about to physically change at an
address. It precedes the dumpster, the crew, the listing, the sale comp and the assessor's revaluation.
Concretely, that makes permit data useful for:
- Contractor lead generation. A new roof permit is a buying signal for gutters, solar and siding.
- Materials and equipment sales. Declared valuation tells a supply rep roughly how big a job is before the framing goes up.
- Real-estate research. Permit velocity by ZIP shows where capital is landing, months ahead of sales comps.
- Competitive analysis. Grouping by contractor name shows who is winning work, by trade and by neighborhood.
None of this requires anything private. It is all public record. The problem is purely mechanical.
The fragmentation problem
There is no national permit feed. There is no shared schema. There is not even a shared platform.
Across the eleven cities covered here you get three:
-
Socrata (SODA 2.1) — eight cities. Query with
$where,$order,$limit,$offset. Pages cap at 1,000 rows for anonymous callers. -
CKAN datastore — two cities (Boston, San Antonio). Query with
datastore_search_sql, which takes actual SQL withLIMIT/OFFSET. -
Carto SQL API — one city (Philadelphia), at
phl.carto.com/api/v2/sql.
Then, inside each platform, the field names drift. Take the single most important column — the date a
permit was issued, which is what any date filter has to run against:
| Column name | Cities |
|---|---|
issue_date |
Austin, Chicago, Los Angeles |
issued_date |
New York, San Francisco |
issueddate |
Seattle, Cincinnati, Baton Rouge |
"DATE ISSUED" |
San Antonio (quoted, uppercase, with a space) |
The drift also happens over time, within one city. Austin's dataset used to expose permit_num and
issued_date; it now exposes permit_number and issue_date. That kind of rename is worse than an
outage, because a query against the old names does not error — it returns nothing, and a nightly job
quietly starts writing zero rows.
Multiply that across permit number, permit type, permit class, work description, status, address,
ZIP, latitude, longitude, contractor, valuation and owner, and "just query the open data" turns into
a per-city mapping problem.
The eleven cities
All eleven feeds below were verified live before publishing, and all eleven refresh daily. Lag is the
gap between a permit being issued and it appearing in the feed.
| City | Source portal | Platform | Lag |
|---|---|---|---|
| Austin, TX | data.austintexas.gov — Issued Construction Permits (3syk-w9eu) |
Socrata | same day |
| San Antonio, TX | data.sanantonio.gov — Permits Issued | CKAN | 2–4 days |
| New York, NY | data.cityofnewyork.us — DOB NOW: Build Approved Permits (rbx6-tga4) |
Socrata | same day |
| Los Angeles, CA | data.lacity.org — LADBS Building + Electrical + Mechanical | Socrata | 1–2 days |
| Chicago, IL | data.cityofchicago.org — Building Permits (ydr8-5enu) |
Socrata | same day |
| Philadelphia, PA | OpenDataPhilly — L&I Building Permits | Carto | same day |
| Boston, MA | data.boston.gov — Approved Building Permits | CKAN | same day |
| Seattle, WA | data.seattle.gov — Building Permits (76t5-zqzr) |
Socrata | 1–3 days |
| San Francisco, CA | data.sf.gov — Building Permits (i98e-djp9) |
Socrata | same day |
| Cincinnati, OH | data.cincinnati-oh.gov — Building Permits (uhjb-xac9) |
Socrata | 1–3 days |
| Baton Rouge, LA | data.brla.gov — Permits Issued (7fq7-8j7r) |
Socrata | 1–3 days |
Two of these need an asterisk. Los Angeles is three separate LADBS datasets merged into one city.
San Antonio's feed is a general permits register, so administrative items (garage sale permits, tree
affidavits) sit next to construction work, and its work-description column is really the city's
PROJECT NAME field, which on small permits is often just the address repeated.
The normalized schema
Eighteen flat fields, one item per permit. Anything a city does not publish is null rather than
absent, so the output exports to CSV or a database without ragged columns. A real record:
{
"city": "Austin, TX",
"permit_number": "1995-014830 BP",
"permit_type": "Building Permit",
"permit_class": "Residential",
"work_description": "Add Bedroom & Level Portion Of Exist Foundation",
"status": "Active",
"issued_date": "2026-08-31",
"applied_date": "1995-05-08",
"address": "4907 SHOAL CREEK BLVD",
"zip": "78756",
"lat": 30.3230315,
"lng": -97.74450439,
"contractor_name": "Butlin Homes, Inc.",
"contractor_phone": "5127732944",
"valuation": 11000.0,
"owner_name": null,
"source_url": "https://abc.austintexas.gov/web/permit/public-search-other?t_detail=1&t_selected_folderrsn=629045",
"raw": { "permittype": "BP", "work_class": "Addition", "tcad_id": "0227000414", "...": "..." }
}
raw carries the original city record, minus geometry blobs and internal index columns, so nothing
the city publishes is lost to the normalization.
The important caveat is that field coverage is a property of the city, not of the normalizer.
Austin is the only one of the eleven that publishes contractor phone numbers, at roughly 92% fill.
New York, Los Angeles, Boston, Seattle, San Francisco, Cincinnati and Baton Rouge fill valuation at
or near 100%; Austin fills it at 14%. Owner name exists in New York, Chicago, Philadelphia and Baton
Rouge and mostly does not elsewhere. No amount of schema work creates a column a city never wrote.
The metros that are missing, and why
Dallas, Houston, Phoenix, Denver, Miami, Fort Worth, Nashville and Mesa are all absent. Every one of
them has a permit dataset you can find in a search engine. Every one of those datasets is
discontinued, frozen years in the past, or served only as an ArcGIS layer that no longer responds.
A city that silently returns zero rows is worse than a city that is openly missing, because it turns
into an invisible gap in someone's analysis. They stay out until their feeds come back.
If you are evaluating a municipal open-data feed yourself, the check takes ten seconds: sort
descending on the date column and look at the newest row. If it is from 2019, the dataset is a museum
piece regardless of what the portal page says.
Use it
The eleven cities are packaged as an Apify Actor:
https://apify.com/make_no_mistakes/us-building-permits-scraper
Pick cities, pick a window, optionally filter by keyword. Priced per record returned.
curl -X POST \
"https://api.apify.com/v2/acts/make_no_mistakes~us-building-permits-scraper/run-sync-get-dataset-items?token=YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"cities":["austin"],"daysBack":7,"maxItems":200}'
import requests
ACTOR = "make_no_mistakes~us-building-permits-scraper"
URL = f"https://api.apify.com/v2/acts/{ACTOR}/run-sync-get-dataset-items"
permits = requests.post(
URL,
params={"token": "YOUR_TOKEN"},
json={"cities": ["austin", "boston"], "daysBack": 7, "maxItems": 500,
"permitTypes": "roof, solar"},
timeout=300,
).json()
for p in permits:
print(p["issued_date"], p["city"], p["zip"], p["contractor_name"], p["work_description"])
run-sync-get-dataset-items blocks until the run finishes and hands back the dataset as JSON, which
is the right shape for a one-off pull or a daily cron. For anything larger, start the run
asynchronously and read the dataset when it finishes.
Or skip all of it and go straight to the portals in the table above. The data is public records and
the cities are the system of record — the only thing being sold here is the eleven mappings and the
work of keeping them alive.
Top comments (0)