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:
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=${co ntractor.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:
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.
More...
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:
| 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=${co ntractor.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.
More...