By Loomwise
If you sell to contractors (materials, equipment rental, insurance and bonding, payroll, software), the best lead is a business that has just started a new kind of work. Two public datasets show exactly that, and both are free to query:
-
New contractor registrations. In New York State, contractors and subcontractors must register with the NYS Department of Labor's Bureau of Public Work and Prevailing Wage Enforcement before bidding on public work or starting a private project covered by prevailing wage. A new certificate means a business is getting ready for that kind of work. The registry is published as open data at data.ny.gov (dataset
i4jv-zkey) and refreshed daily. - Newly issued building permits. A permit means a project has been approved and work is about to start at a specific address. Many large cities publish permits on Socrata or CKAN open-data portals, and some include the contractor's business name.
This tutorial turns both into one daily, de-duplicated lead list. You'll filter each source, join them on business name, schedule the run, and send the rows to your CRM or a sheet. You'll also see where the data has gaps.
Step 0: Write down who you're looking for
Settle three things before you touch any data:
- Trade: electrical, roofing, solar, HVAC, paving and so on.
- Territory: ZIP prefixes or cities.
- Disqualifiers: do you want to skip businesses with a debarment or outstanding wage assessments, or are those exactly the ones you sell compliance services to?
Step 1: New registrations as a feed
The NY registry has a lot of fields. For prospecting, the useful ones are the certificate dates, location, and the compliance flags. The examples below use the input format of a ready-made Apify Actor for this registry (linked at the end). You could also query data.ny.gov's API directly.
Input: new electrical contractors in Western New York (ZIP codes starting with 14) over the last 90 days, active certificates only, debarred businesses skipped:
{
"issuedWithinDays": 90,
"keywords": ["electric"],
"zipCodes": ["14"],
"activeOnly": true,
"excludeDebarred": true,
"cleanRecordOnly": false,
"includeContactDetails": true,
"maxResults": 500
}
What the filters do:
-
issuedWithinDaysis the certificate issue date window (0= no limit). For a fixed range, useissuedFrom/issuedTo(YYYY-MM-DD) instead. -
keywordsmatches words in the legal or DBA business name only, not the trade a business registered for. "Electric" finds Example Electric LLC but not Example Holdings LLC doing electrical work. Use several synonyms. -
citiesis an exact match (case-insensitive).zipCodesmatches prefixes (112= Brooklyn,14= Western NY). -
businessTypescan be Corporation, LLC, LLP, Partnership, Association or Government. -
excludeDebarred(defaulttrue) skips businesses that report a state or federal debarment.cleanRecordOnlygoes further and keeps only businesses with no outstanding wage assessments, debarments, labor/tax-law determinations or safety violations. -
mwbeOnlykeeps only businesses that report being minority- or women-owned. This is useful for primes that need MWBE subs.
One output row. The field names are exact, but the values here are made up:
{
"certificateNumber": "26-XXXXX-CR",
"businessName": "EXAMPLE ELECTRIC LLC",
"businessType": "LLC",
"status": "Active",
"issuedDate": "2026-10-16",
"expirationDate": "2028-12-29",
"businessAddress": "123 EXAMPLE STREET",
"city": "Buffalo",
"state": "NY",
"zipCode": "14201",
"businessPhone": "(555) 555-0100",
"mwbeOwned": false,
"hasWorkersCompInsurance": true,
"hasOutstandingWageAssessments": false,
"debarred": false,
"hasSafetyStandardViolation": false,
"apprenticeshipAssociated": true,
"latitude": 42.89,
"longitude": -78.87,
"sourceUrl": "https://data.ny.gov/d/i4jv-zkey"
}
The full record also includes dbaName, hasLaborOrTaxLawViolation, workersCompExempt, debarment details (debarmentLaw, debarmentStartDate, debarmentEndDate) and apprenticeship fields. There are two ready-made dataset views: New contractors and Compliance flags.
Ways the flags turn into pitches:
-
hasWorkersCompInsurance: falseorworkersCompExempt: true: a lead for insurance and bonding. -
apprenticeshipAssociated: false: a lead for apprenticeship programs and training providers. -
hasOutstandingWageAssessments: true: a lead for certified-payroll and compliance tools. For primes vetting subs, it's a reason to ask questions first.
Privacy behavior, and why some businesses are missing on purpose. The feed is business-only. Officer names are never requested from the source. Sole proprietorships are always excluded. A row is kept only if the name carries a legal-entity designator (LLC, Inc., Corp., Co., Company, Ltd., LP, LLP, PC, PLLC, DPC, Joint Venture/JV). DBA names are kept only when they carry one too. The address and phone are the registered business address and phone, and includeContactDetails: false removes them. Each run's RUN_SUMMARY record shows how many non-entity rows were dropped. Keep in mind that the registry covers businesses registered for public-work and prevailing-wage projects. It is not a list of every contractor in New York, and it doesn't include NYC home-improvement licenses or local municipal licenses.
Step 2: New permits as a feed
City permit data differs a lot from city to city: field names, refresh schedules, and whether contractors are named at all. A unified schema saves you from writing nine parsers. Here's what the nine cities in the Actor below publish:
| City | Refresh | Contractor business name |
|---|---|---|
| Chicago, IL | daily | yes |
| New York City, NY (DOB NOW) | daily | yes (permittee business) |
| Los Angeles, CA | weekly | not published |
| San Francisco, CA | daily | not published |
| Seattle, WA | daily | sometimes |
| Austin, TX | daily | yes |
| Boston, MA | daily | rarely (applicant companies only) |
| Cincinnati, OH | daily | yes |
| Baton Rouge, LA | daily | yes |
Input: roofing and solar permits from the last 2 days in cities that name contractors, only permits that list a contractor business:
{
"cities": ["chicago", "nyc", "austin", "cincinnati", "baton_rouge"],
"issuedWithinDays": 2,
"workCategories": ["Roofing", "Solar"],
"onlyWithContractor": true,
"maxResults": 500,
"maxResultsPerCity": 150
}
-
workCategoriesis a normalized category (New construction, Addition / alteration, Demolition, Roofing, Solar, Electrical, Plumbing, Mechanical / HVAC, Fire protection, Sign, Scaffold / shed / fence, Structural / foundation / earthwork, Other). Categories are assigned by fixed keyword rules from each city's permit type and work class, with the description as a fallback. To filter on a city's own wording, usepermitTypes(e.g."Sidewalk Shed","Commercial"). -
keywordsmatches the description, type or work class, e.g.restaurant,ADU,pool,battery. -
minValuationkeeps permits at or above a declared job value. Permits with no valuation are excluded when this is set, and NYC trade permits often have none. -
maxResultsis a total cap across cities, newest first.maxResultsPerCitystops one busy city from filling the whole list.
One output row. The field names are exact, but the values here are made up:
{
"city": "Austin",
"state": "TX",
"permitNumber": "2026-XXXXXX RP",
"permitType": "Building Permit - Residential",
"workClass": "Repair",
"workCategory": "Roofing",
"description": "Reroof, tear off and replace shingles ...",
"status": "Active",
"appliedDate": "2026-10-10",
"issuedDate": "2026-10-17",
"valuation": null,
"address": "100 EXAMPLE DR",
"zipCode": "78701",
"latitude": 30.27,
"longitude": -97.74,
"housingUnitsChange": null,
"squareFeet": null,
"contractorBusinessName": "Example Roofing LLC",
"contractorRole": "General Contractor",
"contractors": [{ "name": "Example Roofing LLC", "role": "General Contractor" }],
"permitUrl": "https://...",
"sourceUrl": "https://data.austintexas.gov/d/3syk-w9eu"
}
Cities publish different fields, and missing values are null, not guesses. The Contractor leads dataset view gives one line per contractor-permit pair. RUN_SUMMARY gives per-city counts and lists any city that was temporarily unavailable. One city being down doesn't fail the run.
Privacy behavior. Owner, applicant and individual-contractor name columns are never requested from the city APIs. Where a city mixes people and companies in one column (Chicago contacts, Boston applicants, Cincinnati company), a name is kept only if it has an entity designator. A string like ACME ELECTRIC CO / JOHN DOE is split and only the company part is kept. Emails and phone numbers in descriptions are masked. The address is the project site, never an owner's mailing address.
Step 3: Join the two on business name (NYC example)
NYC permits name the permittee business, and the NY registry lists businesses registered for public work. If you match the two, you get businesses with active private projects that are also set up for public work, which is a strong signal for suppliers. Names are never typed the same way twice, so normalize them before matching:
import re, json
DESIGNATORS = r"\b(LLC|L L C|INC|INCORPORATED|CORP|CORPORATION|CO|COMPANY|LTD|LP|LLP|PC|PLLC|DPC|JV|JOINT VENTURE)\b"
def norm(name: str | None) -> str:
if not name:
return ""
s = name.upper().replace("&", " AND ")
s = re.sub(r"[^\w\s]", " ", s) # drop punctuation
s = re.sub(DESIGNATORS, " ", s) # drop LLC, INC, ...
return re.sub(r"\s+", " ", s).strip()
registry = json.load(open("ny_registry.json")) # exported dataset (JSON)
permits = json.load(open("nyc_permits.json"))
by_name = {norm(r["businessName"]): r for r in registry}
for r in registry: # also match on DBA names
if r.get("dbaName"):
by_name.setdefault(norm(r["dbaName"]), r)
matches = []
for p in permits:
reg = by_name.get(norm(p.get("contractorBusinessName")))
if reg:
matches.append({
"business": reg["businessName"],
"certificateNumber": reg["certificateNumber"],
"registeredOn": reg["issuedDate"],
"permitNumber": p["permitNumber"],
"workCategory": p["workCategory"],
"projectAddress": p["address"],
"permitIssued": p["issuedDate"],
"permitUrl": p["permitUrl"],
})
print(len(matches), "matched leads")
Exact matching after normalization misses some real matches and almost never makes a wrong one. If you need more recall, add a fuzzy matcher, and review its matches by hand before they go to sales. For the registry side of a join, use a wider window (issuedWithinDays: 0 or a year of issuedFrom), because a business may have registered long before its next permit.
Step 4: Run it daily without duplicates
- Save each input as an Actor task in Apify Console, for example
ny-new-electrical-wnyandpermits-roofing-solar. - Under Schedules → Create new, set a cron such as
0 7 * * *, pick your timezone, add both tasks, and enable the schedule. New schedules start disabled. - Use a 2-day window (
issuedWithinDays: 2) so nothing falls between runs, then de-duplicate in your destination:- registry key:
certificateNumber - permit key:
city+permitNumber
- registry key:
-
Los Angeles refreshes weekly. If you include
los_angeles, put it in a separate task withissuedWithinDays: 14or more, run it weekly, and de-duplicate the same way. Otherwise LA will look empty most days.
To send rows to your tools, use each task's Integrations tab: Gmail (results attached as CSV/Excel), Google Drive file upload, Slack, webhooks, or Make/Zapier with a Google Sheets or CRM step. Daily SAM.gov Alerts in Your NAICS Codes (No API Key), the first post in this series, walks through each of these step by step.
Step 5: Do outreach like a professional
- Lead with the signal. "Saw you just registered for public work in Erie County. Here's how we help prevailing-wage contractors with X" beats a generic pitch.
- The data is business-only, so keep outreach business-only. Follow the email and calling rules that apply to you (for US businesses, at least CAN-SPAM for email and the TCPA and do-not-call rules for phone). Check with counsel if you're unsure.
- Respect the sources' terms. The NY data is published under the OPEN-NY Terms of Use. If you republish Chicago data, Chicago's Data Terms of Use require a specific notice.
-
Don't treat a flag as a verdict.
hasOutstandingWageAssessmentsanddebarredare what the registry reports. Check the official record before you act on it.
The data gaps, all in one place
- The NY registry covers public-work/prevailing-wage registrants only. Sole proprietors, and registrations whose name lacks an entity designator, are left out on purpose.
- Registry
keywordsmatch names only. - LA permits refresh weekly. SF and LA don't publish contractor names. Seattle sometimes does, Boston rarely does.
-
NYC trade permits often lack a valuation, and
minValuationdrops permits without one. -
workCategoryis rule-based. Spot-check the categories you rely on, and usepermitTypeswhen a city's own wording is more precise.
Disclosure: we built the two Actors used here, the New York Contractor Registry Feed ($3 per 1,000 results) and the US Building Permits Feed ($2 per 1,000 results), plus Apify's small Actor-start fee. Both read the official open-data APIs listed above, which you can also query yourself.
Update (Oct 2026): a data-backed follow-up for one state, New Washington contractor licenses in October 2026.
Top comments (0)