DEV Community

lulzasaur
lulzasaur

Posted on

Verify Contractor Licenses in 11 US States with One API Call

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();
Enter fullscreen mode Exit fullscreen mode
{
  "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"
      }
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

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`);
Enter fullscreen mode Exit fullscreen mode

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`
      );
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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)