Verify Contractor Licenses in 11 US States with One API Call
If you build anything in insurance tech, background checks, credentialing, or construction management, you've probably hit this wall: verifying a contractor's license means scraping 11 different state websites, each with different formats, search flows, and data schemas.
California uses CSLB. Texas has TDLR. Florida runs DBPR. Washington has L&I with 159K+ records. New York uses municipal open data. None of them agree on what a "license record" looks like.
I needed this for a compliance project, so I built a single API that normalizes all of it.
The Problem
Here's what you're dealing with state by state:
| State | Agency | Records | Format |
|---|---|---|---|
| CA | CSLB | 300K+ | HTML scraping |
| TX | TDLR | 200K+ | Search form |
| FL | DBPR | 500K+ | HTML table |
| NY | NYC Open Data | 80K+ | JSON API |
| WA | L&I (Socrata) | 159K | JSON API |
| CT | DCP | 133K | Socrata |
| OR | CCB | 56K | Includes bond/insurance |
| CO | DORA | 70K | Trade licenses |
| IL | IDFPR | 400K+ | Statewide |
| IA | DWD | 17K | Socrata |
| DE | DPOS | 31K | Socrata |
Each state returns different fields. Some include bond info, some include insurance, some just give you name + license number + status. Building scrapers for each one is a multi-week project.
One Endpoint, Normalized Response
const response = await fetch(
'https://marketplace-price-api-production.up.railway.app/license/search?' +
new URLSearchParams({
state: 'CA',
name: 'Johnson Electric',
type: 'contractor'
}),
{ headers: { 'X-Api-Key': 'your-key' } }
);
const data = await response.json();
{
"success": true,
"total": 3,
"licenses": [
{
"name": "JOHNSON ELECTRIC INC",
"licenseNumber": "C10-987654",
"state": "CA",
"status": "Active",
"type": "C-10 Electrical",
"issueDate": "2015-03-12",
"expirationDate": "2027-03-31",
"bond": {
"amount": 25000,
"company": "Hartford Fire Insurance"
}
}
]
}
Every state returns the same shape: name, license number, status, type, dates. States that have bond/insurance data include it. The normalization layer handles the differences so your code doesn't have to.
Building a Bulk Verification Pipeline
The real use case is batch verification. Here's a Node.js script that checks a CSV of contractors against all supported states:
import { parse } from 'csv-parse/sync';
import { readFileSync, writeFileSync } from 'fs';
const API_URL = 'https://marketplace-price-api-production.up.railway.app/license/search';
const API_KEY = 'your-key';
const contractors = parse(readFileSync('contractors.csv'), {
columns: true
});
const results = [];
for (const row of contractors) {
const resp = await fetch(
`${API_URL}?${new URLSearchParams({
state: row.state,
name: row.company_name,
type: 'contractor'
})}`,
{ headers: { 'X-Api-Key': API_KEY } }
);
const data = await resp.json();
const match = data.licenses?.[0];
results.push({
company: row.company_name,
state: row.state,
verified: match?.status === 'Active',
licenseNumber: match?.licenseNumber || 'NOT FOUND',
expiration: match?.expirationDate || 'N/A'
});
// Respect rate limits
await new Promise(r => setTimeout(r, 200));
}
writeFileSync('verification-results.json', JSON.stringify(results, null, 2));
console.log(`Verified ${results.length} contractors`);
Feed it a CSV with company_name and state columns, get back verification status for each one.
Expiration Monitoring
Set up a daily cron to catch licenses about to expire:
const WATCHLIST = [
{ name: 'ABC Plumbing', state: 'FL', license: 'CFC1234567' },
{ name: 'XYZ Electric', state: 'CA', license: 'C10-987654' },
];
for (const contractor of WATCHLIST) {
const resp = await fetch(
`${API_URL}?state=${contractor.state}&license=${contractor.license}`,
{ headers: { 'X-Api-Key': API_KEY } }
);
const data = await resp.json();
const lic = data.licenses?.[0];
if (lic) {
const daysLeft = Math.floor(
(new Date(lic.expirationDate) - Date.now()) / 86400000
);
if (daysLeft < 30) {
console.log(
`⚠️ ${contractor.name} (${contractor.state}) ` +
`expires in ${daysLeft} days`
);
}
}
}
Why Not Just Scrape?
You could build scrapers for each state. I did — that's how this API started. Here's why you probably don't want to maintain them:
- California CSLB changes their HTML layout every few months
- Florida DBPR rate-limits aggressively and blocks automated UA strings
- Oregon CCB includes bond/insurance data nested in secondary pages
- Washington L&I uses Socrata with non-obvious dataset IDs
Each state breaks differently, at different times. I handle the maintenance so you don't have to.
I built this because I kept needing it across projects. It's on RapidAPI — free tier gives you enough calls to evaluate, paid tiers for production volume.
Top comments (0)