Commercial Building Permit Opportunity Feed
Pricing
from $100.00 / 1,000 qualified projects
Commercial Building Permit Opportunity Feed
Qualify commercial building permits from official city open data for one trade and sales stage, group amendments into one project, and emit only new or changed opportunities.
Pricing
from $100.00 / 1,000 qualified projects
Rating
0.0
(0)
Developer
Olalekan Johnson
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
10 days ago
Last modified
Categories
Security
Limited permissionsShare
Unofficial monitor of commercial building permits for contractors and suppliers who sell into a specific trade. It reads official city open-data APIs, groups amendments into one project, and returns a project only when it is new or changed and still inside the sales window for that trade.
An issued electrical permit is often already too late for a design-stage pitch. A renewal with no scope change is not a new job. This actor keeps those cases out unless you ask for them.
Unofficial, not affiliated with the City of Chicago, the City of New York, the Department of Buildings, DataSF, the City of Austin, the City of Seattle, or the City of Los Angeles.
Contact: plusonecares@gmail.com
Who it is for
Estimators, manufacturer reps, and subcontractors who want a short list of commercial jobs in one trade, with the stage, valuation, published contractor or business name, and a link back to the city row. It is a permit observation, not permission to call or email anyone.
What you put in
Leave the input empty and the actor runs the default: Chicago, HVAC, last 14 days, residential wording removed, only new or changed projects, at most 300 source rows and 25 projects.
| Field | Default | What it does |
|---|---|---|
cities | chicago | One or more of chicago, new-york, san-francisco, austin, seattle, los-angeles |
trades | hvac | HVAC, fire and life safety, electrical, plumbing, roofing, restaurant fit-out, signage, demolition and waste, solar, or custom |
customKeywords | empty | Extra phrases when you select custom. Letters, numbers, spaces, and &./+- |
lookbackDays | 14 | Filing, issue, and status dates inside this window |
stages | empty | Empty uses each trade's window. Electrical defaults to application only. Other trades default to application and issued |
excludeResidential | true | Drops single-family, duplex, townhouse, apartment, ADU, and occupancy groups R-2 through R-5 |
onlyChanges | true | Later runs of the same monitor emit a project only when it changed |
minValuation | unset | When set, projects with a missing or lower valuation are skipped |
maxRecords | 300 | Normalized source rows per city |
maxProjects | 25 | New or changed projects pushed this run. The rest wait for the next run |
includeNormalizedRecords | false | Also push one flat row per trade-matching filing |
monitorId | derived | Isolates saved state. Derived from cities, trades, keywords, stages, residential filter, and minimum valuation |
socrataAppToken | empty | Optional token you supply. Sent only as the X-App-Token header. The actor runs on the public API without one |
What you get back
Qualified projects land in the default dataset (view Qualified projects). A run summary named OUTPUT lands in the default key-value store: coverage, counts, source lag, and charged units.
Each project row is flat, so a CSV export or a Google Sheet mapping works without nested JSON. Pipe characters separate multiple trades, permit numbers, and matched terms.
changeType is new or changed. materialChange is true when the filing itself is a renewal or amendment that the city marked as having scope changes. qualifiesBecause is the plain-language reason. sourceLink opens the city row. sourceLagHours is how old the portal's last refresh was when this run observed it. confidence is high when the city published a structured permit type for the trade, medium for a description match, and low for a short custom keyword with no occupancy.
Example from the default input
Observed 23 October 2026. Chicago HVAC, last 14 days. One of 11 qualified projects:
{"recordType": "qualified-project","projectId": "chicago:permit:101086447","monitorId": "m2f97b6857eb39a9a1fcf","changeType": "new","city": "chicago","cityName": "Chicago","primaryTrade": "hvac","trades": "hvac","stage": "issued","stageReason": "Permit issued.","salesTiming": "in-window","qualifiesBecause": "Issued as of 2026-10-21. Scope matches HVAC (MECHANICAL). The city classifies the occupancy as commercial.","confidence": "medium","valuationUsd": 1050000,"valuationSource": "reported_cost","contractorName": "LEOPARDO CONSTRUCTION INC","contractorRole": "CONTRACTOR-GENERAL CONTRACTOR","address": "222 S RIVERSIDE PLZ","addressCity": "Chicago","region": "IL","permitNumbers": "101086447","recordCount": 1,"amendmentCount": 0,"applicationDate": "2026-09-15","issuedDate": "2026-10-21","eventDate": "2026-10-21","eventAgeDays": 2,"sourceUpdatedAt": "2026-10-22T12:08:50+00:00","observedAt": "2026-10-23T04:45:58+00:00","sourceLagHours": 16.6,"sourceName": "City of Chicago Building Permits","sourceDatasetId": "ydr8-5enu","sourceLink": "https://data.cityofchicago.org/resource/ydr8-5enu.json?id=3455865","datasetUrl": "https://data.cityofchicago.org/d/ydr8-5enu","evidenceSnippet": "SELF-CERT 2019 CBRC: TENANT BUILDOUT IN EXISTING OFFICE BUILDING ON THE 22ND FLOOR, OFFICE SUITE 2250. NEW PARTITIONS, DOORS, CEILINGS, LIGHTING, MILLWORK, AND FINISHES. ASSOCIATED ELECTRICAL, MECHANICAL AND PLUMBING WORK AS PER PLANS. (OCCUPANCY GROUP B; TYPE 1A CONSTRUCTION)","evidenceHash": "sha256:366cb6cc0def63c946a32a798fcaa391faf32c7060e34ce0b820a6ccccfbd7e2","matchedTerms": "MECHANICAL","occupancy": "commercial","materialChange": false,"runCoverage": "complete"}
That same default run also returned a $6.5 million ballpark air system, a restaurant HVAC remodel, and several office interiors. A second run with the same state emitted nothing new.
Cities and datasets
Verified against the public SODA 2.0 APIs on 23 October 2026. Older history datasets are out of scope. The actor requests only these hosts, refuses redirects, and will not fetch a private IP, localhost, or a non-https URL. Portal links on a row are returned to you and are never downloaded.
| City | Dataset | ID | How a row is kept |
|---|---|---|---|
| Chicago | Building Permits | ydr8-5enu | Status and milestone. No occupancy class, so the description is used |
| New York | DOB NOW: Build – Approved Permits | rbx6-tga4 | Issued mechanical, plumbing, sprinkler, standpipe, boiler, solar, sign, and demolition permits. This set does not include electrical |
| New York | DOB NOW: Electrical Permit Applications | dm9a-ab7w | Application through issued. Used for electrical, and for HVAC when the filing flags HVAC or boiler wiring |
| San Francisco | Building Permits | i98e-djp9 on data.sf.gov | Filed, issued, complete, reinstated. data.sfgov.org now redirects here |
| Austin | Issued Construction Permits | 3syk-w9eu | Commercial class when residential is excluded. Trade permits share masterpermitnum |
| Seattle | Building Permits | 76t5-zqzr | Non-Residential class when residential is excluded |
| Los Angeles | Building permits issued, 2020 to present | pi9x-tg5x | Issued building, sign, and demolition |
| Los Angeles | Building permits submitted | gwh9-jnip | Plan check and other pre-issue statuses |
| Los Angeles | Mechanical permits issued / submitted | 67is-svtd, 9rag-xmmd | HVAC, plumbing, fire sprinkler |
| Los Angeles | Electrical permits issued / submitted | ysqd-apz7, 9k3p-zrda | Electrical |
Los Angeles Building and Safety's issued dataset metadata was last refreshed at 2026-10-19T17:02:00Z, about three days before this check. Treat sourceLagHours as part of the lead, especially in Los Angeles.
Source attribution
- Chicago. City of Chicago, Building Permits (
ydr8-5enu). The dataset licence field says "See Terms of Use". - New York. Department of Buildings. Approved permits (
rbx6-tga4) and electrical applications (dm9a-ab7w). The metadata names no separate licence; the dataset pages carry NYC Open Data terms. - San Francisco. DataSF Building Permits (
i98e-djp9). Open Data Commons Public Domain Dedication and License. - Austin. City of Austin, Issued Construction Permits (
3syk-w9eu). The portal metadata labels the licence "Public Domain U.S. Government". - Seattle. City of Seattle, Building Permits (
76t5-zqzr). Public domain. - Los Angeles. Building and Safety permits listed above. Metadata attribution is "TSB" and no licence name is published.
How a filing becomes a project
Addresses are folded into a single line (direction and street suffix normalized). Project ids:
- New York jobs drop the work-type suffix, so
B01424126-S2andB01424126-S2-MSare one project. - Austin trade permits join their master permit.
- Seattle child permits join the parent permit number.
- A filing that cites another permit number (
PERMIT 101086907) joins that permit, including Chicago revisions. - Everything else is one project per permit number. Permits are not merged just because they share an address.
Stages, in the order a later date wins: application, issued, renewal, inspection, completed. "Corrections Issued" in Los Angeles stays an application. Chicago "inspection eligible" stays issued. Seattle "Ready for Issuance" stays an application. A New York renewal with changes stays issued. A renewal without changes is stage renewal and is dropped unless you add that stage.
Default windows:
| Trade | Stages that qualify |
|---|---|
| Electrical | application |
| HVAC, fire and life safety, plumbing, roofing, restaurant fit-out, signage, demolition and waste, solar, custom keywords | application, issued |
Add issued to stages if you want issued electrical permits. Those rows are marked salesTiming: late-for-design. Add renewal, inspection, or completed only when you want work that is already in the field or closed.
State across runs
State lives in the named key-value store permit-opportunity-state, under a key for the monitor id. A successful run saves the fingerprint of every project it emitted and every project that was unchanged. Projects past maxProjects are not marked emitted, so the next run can send them.
A failed run does not replace that baseline. Two runs of the same monitor will not update state at the same time; the second exits if the lock is still held.
record-normalized is charged for each source row normalized in this run, including rows that do not become a project. The next scheduled run reads the window again and can charge those rows again. qualified-project is charged only when a new or changed project is pushed. The same fingerprint is not charged twice.
Pricing
This Actor uses pay-per-event pricing. Apify also charges a small start event once per run (about $0.0025 at the default 512 MB).
| Event | When it is charged | Price |
|---|---|---|
record-normalized | A source row was normalized. Invalid rows are not charged. A duplicate id in the same run is charged once | $0.002 |
qualified-project | A new or changed project was pushed to the dataset | $0.10 |
Measured on 23 October 2026 with public endpoints and no app token.
| Run | What happened | Events | Charge |
|---|---|---|---|
| Default: Chicago, HVAC, 14 days | Coverage complete. 72 rows normalized, 11 projects pushed. 42 residential filings and 15 inspection-stage filings were held back | 72 × $0.002 + 11 × $0.10 | $1.244 |
| Same Chicago monitor, run again with no city changes | Coverage complete. 72 rows normalized again. 0 projects pushed | 72 × $0.002 + 0 × $0.10 | $0.144 |
| New York, HVAC, 14 days, default caps | Coverage partial. 300 rows normalized, 25 projects pushed, 189 further qualified projects left for the next run | 300 × $0.002 + 25 × $0.10 | $3.10 |
$3.10 is also the ceiling of the default caps when every normalized row is unique and 25 projects qualify. Raising maxRecords or maxProjects raises that ceiling. A quiet daily Chicago schedule is about $0.144 plus $0.10 for each project whose stage, scope, valuation, contractor, or evidence actually changed.
Local runs and runs that are not on pay-per-event pricing still deliver the rows. OUTPUT.billed is false in that case, and chargedUnits shows what would have been billed.
Coverage
OUTPUT.coverage is one of:
complete— every selected portal answered and the caps were not hitpartial— a cap or a charge limit stopped the read, or only some portals failedempty— the portals answered and nothing in the window matched (statusno-data, exit 0)blocked— every portal failed, or another run holds the monitor lock (exit 1)unsupported— the input was rejected (exit 1)
Limits
- Six cities, current Socrata datasets only. No county portals, no scraped contractor websites, no login-only systems.
- Chicago does not publish an occupancy class. A house described only as "interior remodel" can remain, with
occupancy: unknownandconfidence: medium. Dwelling units, ADUs, coach houses, apartments, and groups R-2 through R-5 are removed whenexcludeResidentialis true. A hotel (group R-1) stays when the text says hotel. - "Mechanical room" by itself is not treated as HVAC. A description that only says "mechanical, electrical, and plumbing" on a real commercial build-out still matches.
- Contractor values are the business name, role, and license the city published. Owner names, architect names, phone numbers, insurance policies, and firm street addresses are not requested and are not returned.
- New York electrical is a second dataset. A New York run that fills
maxRecordson approved permits may read only part of the electrical applications. Coverage ispartialwhen that happens. - San Francisco's feed is incremental: a row can show up because
data_loaded_atchanged, not only because a permit was filed or issued in the window. - Los Angeles mechanical and electrical datasets do not carry valuation. A minimum valuation skips those rows.
- Valuation is whatever the city typed in. It is not a bid.
- Lookback and the caps are per run. They are not a full historical export.
- The actor does not geocode. Latitude and longitude are copied when the city published them.
FAQ
Why is the second run empty? onlyChanges is on, and the monitor already stored those projects. You still pay to re-read the window. Turn onlyChanges off to push the current matches again (each pushed project is charged again), or change monitorId to start a fresh baseline.
Why did electrical in Chicago return nothing? Issued electrical permits are outside the default window. Add issued to stages if you want them, and read salesTiming.
A renewal showed up as issued. The city marked it as a renewal with scope changes, so the work is still an open issued permit. A renewal without changes is stage renewal and is omitted until you select that stage.
Can I trust the contractor field? Only as a business name printed on the permit. There is no phone, no email, and no claim that the company wants to be contacted.
The description looks residential but the row came through. Chicago often has no occupancy field. If the description never says dwelling, family, apartment, or a residential occupancy group, the row stays and says that no residential occupancy was published.
What does source lag mean? Hours between the portal's rowsUpdatedAt (or the row's load time, when the city publishes one) and observedAt. A permit dated last week can still be sitting on a dataset that refreshed two days ago.
Integrations
- Webhook. Point an Apify webhook at the run event
ACTOR.RUN.SUCCEEDED. The dataset items are the payload. Also readOUTPUTso you can ignore a run whose coverage isempty,partial, orblocked. - Make and n8n. Use the Apify app or node. Map the dataset fields straight onto the scenario;
permitNumbers,trades, andmatchedTermsare already single strings. - Google Sheets. Use the Apify integration to append dataset items. The qualified-project view is the tab most teams want. Schedule the actor daily and keep
onlyChangeson so the sheet grows only when a project is new or changed. - CSV. Export the dataset. Every field is a string, number, or boolean.
Run it locally
Python 3.12.
python -m venv .venvsource .venv/bin/activatepip install -r requirements.txtpython -m src
With no INPUT record, the default Chicago HVAC run starts. To pass input, write JSON to storage/key_value_stores/default/INPUT.json before launching. Results land in storage/datasets/default and the summary in storage/key_value_stores/default/OUTPUT.
pip install -r requirements-dev.txtpytest
tests/test_live.py calls the public portals. It skips if the network fails.