New York State requires every contractor bidding on public work covered by prevailing-wage law to register with the Department of Labor, and it publishes the whole registry as open data. Each record shows the business type, MWBE status and compliance flags: debarment, outstanding wage assessments and workers' comp insurance.
That makes it a useful list for:
- General contractors looking for qualified, compliant subs, especially MWBE firms to meet participation goals.
- Insurers, bonding agents, payroll and certified-payroll software vendors, since a new registrant is about to start public work.
- Suppliers looking for newly active contractors.
This post shows how to pull every contractor business registered in the last 30 days, narrow it to New York City, and keep only firms with a clean record, all with one short Python script.
Run on Oct 22, 2026:
- 1,129 new registrations from businesses in 30 days: 821 corporations, 294 LLCs, 10 LLPs and 4 partnerships.
- 170 were based in New York City (the five boroughs), and 165 of those had no debarment, no outstanding wage assessments and workers' comp in place.
- 72 of the NYC firms were MWBE-owned.
The data source
NYS Contractor Registry on data.ny.gov (Socrata), maintained by the NYS Department of Labor and updated as registrations come in.
The script
import requests
from collections import Counter
from datetime import date, timedelta
since = (date.today() - timedelta(days=30)).isoformat()
NYC_ZIP_PREFIXES = ("100", "101", "102", "103", "104", "111", "112", "113", "114", "116") # 5 boroughs
rows = requests.get("https://data.ny.gov/resource/i4jv-zkey.json", params={
"$select": "certificate_number,business_name,business_type,business_is_mwbe_owned,city,zip_code,issued_date,"
"business_has_been_debarred,business_has_outstanding_wage_assessments,business_has_workers_compensation_insurance",
"$where": f"issued_date >= '{since}' AND status = 'Active' AND business_type != 'Sole Proprietorship'",
"$order": "issued_date DESC", "$limit": 10000}, timeout=120).json()
print(len(rows), "new NY contractor registrations (businesses) since", since)
print(Counter(r.get("business_type") for r in rows).most_common())
nyc = [r for r in rows if (r.get("zip_code") or "")[:3] in NYC_ZIP_PREFIXES]
clean = [r for r in nyc if r.get("business_has_been_debarred") == "No"
and r.get("business_has_outstanding_wage_assessments") == "No"
and r.get("business_has_workers_compensation_insurance") == "Yes"]
print(len(nyc), "in New York City;", len(clean), "with a clean record and workers' comp;",
sum(r.get("business_is_mwbe_owned") == "Yes" for r in nyc), "MWBE-owned")
Three things that will trip you up
-
Skip sole proprietors. A few hundred registrations are sole proprietorships, whose business name is often a person's name. Filtering
business_type != 'Sole Proprietorship'keeps the list to companies, which is better for privacy and more useful for B2B. - City names are messy, ZIP codes aren't. Records say "Brooklyn", "BROOKLYN", "Bklyn" or a neighborhood like "Astoria". Filtering on the first three ZIP digits is far more reliable. The script uses the NYC ZIP prefixes.
-
Flags are text, not booleans. Columns like
business_has_been_debarredhold"Yes"or"No"strings. Compare to the string.
Ideas
- Swap the ZIP prefixes to cover Long Island (
110,115,117,118,119) or upstate regions. - Run it weekly with a 7-day window and de-duplicate on
certificate_numberto get a steady new-registrant feed. - Track the
expiration_datecolumn to time renewal-related outreach.
Optional: I run this as a hosted, scheduled search: New NYC Contractors (ready-made search) (paid per result). Disclosure: it's my tool. The script above does the same job for free.
Top comments (0)