# US Contractor License Lookup & List Scraper (State Boards) (`scrapebench/us-contractor-license-lookup`) Actor

Verify contractor licenses or export whole-state contractor lists across 36 US jurisdictions in one run - enumerated verdicts, the board cited on every row.

- **URL**: https://apify.com/scrapebench/us-contractor-license-lookup.md
- **Developed by:** [ScrapeBench](https://apify.com/scrapebench) (community)
- **Categories:** Lead generation
- **Stats:** 11 total users, 5 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$0.004 / license verified

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## US Contractor License Lookup & List Scraper (State Boards)

### Pain points

- A vendor list spans a dozen states, and every state board has its own portal, search form and status vocabulary.
- Checking 300 subcontractors means 300 manual searches, or a different scraper and a different output shape per state.
- Bulk lists live on some boards as open data, on others behind a search form, and nobody hands you one schema for all of them.

### What we solve

- One input, many states: list the states, then either the names / licence numbers to verify or the filters to export.
- Verify mode returns one flat verdict per contractor per state (active, expired, revoked, suspended, inactive, not_found, unverifiable_state_not_covered, check_failed).
- Export mode delivers each state's full contractor roster, or a filtered slice, from that state's own scrapebench actor.
- A state that fails or is refused returns one free notice; the other states still deliver.

### Summary

A multi-state front door to the scrapebench state contractor actors. Pick the states, then pick the job. Verify: give company names or licence numbers and get one verdict per contractor per state, with the reason in plain language, the board's own status string, the expiry date and which board answered. Export: get every licensed contractor in each state, or a slice of it using that state's own filters. Each state is answered by a real run of that state's actor under your account, so you get the same data as running the states one by one, in one call. The design rule that matters most is what the verify verdicts refuse to say: 'not_found' means the registry returned no match for that search and is NOT a finding that anybody is unlicensed; an uncovered state returns 'unverifiable_state_not_covered', never silence; any error, timeout or ambiguous name match returns 'check_failed', never a pass; a licence that ended voluntarily is 'inactive', never 'revoked'.

### Who it's for

- Procurement and vendor-onboarding teams checking subcontractor lists across several states
- Insurers, lenders and marketplaces verifying trade licences at signup, by API
- Suppliers and sales teams building contractor lists in several states at once
- Developers who want one integration instead of one per state board

### How to use

This actor does two jobs. Run it, then collect results from the run's dataset (export to JSON/CSV/Excel, or pull via the Apify API).

**Get a list (export):** no names, just filters; returns the whole matching list.

```json
{
  "mode": "export",
  "states": [
    "TX",
    "CA"
  ],
  "stateFilters": {
    "TX": {
      "licenseType": "Electrical Contractor",
      "county": "Travis"
    },
    "CA": {
      "licenseClass": "C-10",
      "county": "Sacramento"
    }
  },
  "maxResults": 500
}
```

**Check specific firms (lookup):** put company names or licence numbers in `mode`, `states`, `licenseNumbers`, `companies`; **Inputs** below says how each is matched.

```json
{
  "mode": "verify",
  "states": [
    "CA",
    "TX",
    "FL"
  ],
  "licenseNumbers": [
    "22726"
  ],
  "companies": [
    "Ransome Company"
  ]
}
```

See **Inputs** below for every available field.

### What you get

One row per record:

| Field | Description |
|---|---|
| `verdict` | Verify mode. active | expired | revoked | suspended | inactive | not_found | unverifiable_state_not_covered | check_failed. The last three describe the CHECK, not the contractor |
| `verdict_reason` | Verify mode. Plain-language explanation, safe to quote to an end user |
| `verdict_basis` | Verify mode. Which evidence the verdict rests on: status, expiry_date, status+expiry_date, or none |
| `requested_company / requested_license_number / requested_state` | Verify mode. What was searched, echoed back |
| `entity_name / license_number / license_type` | Verify mode. The licence as the board publishes it |
| `status_raw / expiry_date / days_to_expiry` | Verify mode. The board's own status string, its expiry date, and days left (negative when expired) |
| `source_portal` | Verify mode. Which board answered (CSLB, TX TDLR, SC LLR...) - cite this |
| `source_state` | Export mode. The state the row came from; every other field is that state actor's own row, unchanged |
| `source_url` | Where the answer came from |
| `notice / notice_type` | Free row explaining a gap: a state not covered, a state that failed, a spend cap reached |

Sample:

```json
{
  "verdict": "active",
  "verdict_reason": "status 'This license is current and active.', expires 2027-10-31",
  "verdict_basis": "status+expiry_date",
  "requested_company": null,
  "requested_license_number": "22726",
  "requested_state": "CA",
  "entity_name": "RANSOME COMPANY",
  "license_number": "22726",
  "license_type": null,
  "status_raw": "This license is current and active.",
  "expiry_date": "10/31/2027",
  "days_to_expiry": 451,
  "source_portal": "CSLB",
  "source_url": "https://www.cslb.ca.gov/OnlineServices/CheckLicenseII/LicenseDetail.aspx?LicNum=22726"
}
```

### Inputs

| Field | Required | Type | Default | Description |
|---|---|---|---|---|
| `mode` | no | string | `"verify"` | lookup / verify: mode 'verify' - give company names or licence numbers and get one verdict per contractor per state (active, expired, revoked, suspended, inactive, not_found...). export: mode 'export' - download every licensed contractor in each state you pick, or a slice of it via 'stateFilters'. Each state is answered by that state's own scrapebench actor, run under your account. |
| `states` | no | array | `["CA", "TX"]` | 2-letter codes. Verify covers AL, AR, AZ, CA, CO, CT, DC, DE, FL, IA, ID, MA, MD, MI, MN, MS, NC, ND, NE, NJ, NM, NV, NY, OH, OR, PA, RI, SC, TN, TX, VA, WA, WV. Export covers AL, AR, AZ, CA, CO, CT, DC, DE, FL, IA, ID, IL, LA, MD, MN, MS, ND, NE, NJ, NM, NV, NY, OH, OR, PA, RI, SC, TN, TX, UT, VA, WA, WV. In verify mode every name and number is checked in every state listed. A state outside the list returns 'unverifiable_state_not_covered' - the check was NOT performed, which is not a finding that anybody is unlicensed. |
| `state` | no | string | — | Single-state form of 'states', e.g. 'TX'. Kept for saved tasks and agents; both may be set. |
| `companies` | no | array | `["Ransome Company"]` | Business or licensee names to verify. Matching is EXACT on the full name once case, punctuation and company suffixes (LLC, Inc, Corp) are ignored - 'ABC Roofing' will NOT match 'ABC Roofing & Siding', by design. A name matching more than one licence returns 'check_failed' rather than a guess. Use licence numbers whenever you have them. |
| `licenseNumbers` | no | array | `["22726"]` | Licence numbers to verify - the most reliable input. Punctuation and leading zeros are ignored. DE, IA, MS, OH and VA cannot be searched by number (their boards only take names) and return 'check_failed' asking for the name. |
| `company` | no | string | — | Single-value form of 'companies'. |
| `licenseNumber` | no | string | — | Single-value form of 'licenseNumbers'. |
| `stateFilters` | no | object | `{"TX": {"licenseType": "Electrical Contractor", "county": "Travis"}}` | Export mode only. Keyed by state code; each value is passed verbatim as that state's actor input, so use that actor's own field names (linked in the README), e.g. {"TX": {"county": "Travis"}, "CA": {"licenseClass": "C-10", "county": "Sacramento"}}. A state with no entry exports with no filter. |
| `maxResults` | no | integer | `500` | Export mode only: the most rows delivered per state. Billing is per row delivered, so this is also your spend limit per state. |

### Pricing (Pay Per Event)

You pay per result (`license-verified`) — **no charge for empty runs**. Example: **1000 rows delivered** at *$0.001/result* ≈ **$1.00**.

**$0.001 per row from 2026-11-14, on top of the state actor's own fee**, and export mode opens that day. Until then each resolved verdict is $0.004 and export returns a free notice.

Each state is answered by a real run of that state's scrapebench actor under your account, and that run bills its own per-row fee (most states $0.002-$0.004 per row; South Carolina and other CAPTCHA boards add a per-search fee). In verify mode the state actor bills each candidate it returns: one for a licence number, up to 20 for a common name - another reason to send numbers. So this tool never costs less than running the states yourself - it costs $0.001 more per row, for one call instead of many. Your maximum cost per run is split across the state runs, so it still caps the total.

Apify platform usage (compute) is billed separately per your plan.

### Use cases

- Subcontractor vetting - a GC with crews in five states checks every sub's licence in one run before a job starts.
- Vendor onboarding - a marketplace verifies a trade licence at signup, whichever state the contractor works in.
- Multi-state lead lists - a supplier exports electrical contractors in Texas and California in one call.
- Agent tool call - an LLM answers 'is this contractor licensed in Texas?' with a citable board.

### Why this actor

- One call, many states: the same verdict shape and the same notices whichever board answered.
- Fails closed: an error, a timeout or a name matching two licences returns check_failed, never a pass; not_found is never 'unlicensed'.
- Backed by maintained single-state actors: each state's scraper is watched and fixed on its own, and this tool routes to it.

### Limitations & updates

Each state adds a cold start (10-30 s) and a big statewide export runs for minutes; at most three states run at once. Louisiana is export-only - its board publishes a roster with no per-contractor search. Michigan, Massachusetts and North Carolina are verify-only - their boards offer capped searches, not rosters. Some boards are partial by source: AL, DE, TX, VA and WV do not reliably publish a licence status (the verdict rests on the expiry date), and NC and SC publish no expiration date while AZ, MS and NJ often omit it (the verdict rests on status); 'verdict_basis' says which on every answer. Name matching is deliberately strict, so a licence number is always the more reliable input. Export rows are each state actor's own rows, so their fields differ from state to state; 'source_state' tells you which state a row came from.

### FAQ

**Which states are covered?**

Verify: AL, AR, AZ, CA, CO, CT, DC, DE, FL, IA, ID, MA, MD, MI, MN, MS, NC, ND, NE, NJ, NM, NV, NY, OH, OR, PA, RI, SC, TN, TX, VA, WA and WV. Export: AL, AR, AZ, CA, CO, CT, DC, DE, FL, IA, ID, IL, LA, MD, MN, MS, ND, NE, NJ, NM, NV, NY, OH, OR, PA, RI, SC, TN, TX, UT, VA, WA and WV. Any other state returns a free row saying it is not covered - never a finding about the contractor.

**Does 'not_found' mean the contractor is unlicensed?**

No. It means the state registry returned no match for what was searched. A typo, a DBA that differs from the licensed entity, or a licence held under a parent company all produce 'not_found'. Treat it as 'go and check', never as a finding.

**What does it cost?**

$0.001 per row on top of the state actor's own per-row fee (from 2026-11-14). Each state runs as its own actor run under your account and bills there, so a verified California licence costs about $0.005 and an exported Texas row about $0.005. Free rows (not_found, check_failed, notices) cost nothing here.

**How do I filter an export?**

Put that state's own input fields under its code in 'stateFilters', e.g. {"TX": {"county": "Travis"}}. Each state actor's page lists its filters; this tool passes them through unchanged. A state with no entry exports with no filter, up to 'maxResults' rows.

**Can I search every state by licence number?**

No. Delaware, Iowa, Mississippi, Ohio and Virginia boards only search by name, so a number-only request there returns 'check_failed' asking for the company name. Reporting 'not_found' would imply the licence does not exist, which we cannot know from a search that was never possible.

**Is this personal data?**

It is public professional-licence data as the state boards publish it. Some boards license individuals, so a person's name can appear where the board publishes it that way. No enrichment is added.

### Which actor to choose

Part of the contractor-license suite - pick the shape that fits how you are calling it:

- **Contractor License Expiration Monitor & Alerts (Notion)** — You want a watchlist re-checked on a schedule with alerts, rather than one answer on demand.
- **California Contractor License Lookup & Verify (CSLB)** — You need California only, including owner/personnel name search.
- **Texas Contractor & Trade License Lookup & Export (TDLR)** — You need Texas only, with every TDLR filter.
- **Virginia Contractor License Lookup (DPOR Bulk)** — You need the Virginia DPOR roster only.

### Guides & use cases

Written up on **[scrapebench.dev](https://scrapebench.dev)** — the bench that runs and verifies this actor against the live source every night:

- **How-to:** [How to run US Contractor License Lookup & List Scraper (State Boards)](https://scrapebench.dev/guides/how-to-us-contractor-license-lookup/)
- **Use case:** [Give an AI agent the ability to verify a contractor licence](https://scrapebench.dev/use-cases/agent-verify-contractor-licence/)

More actors, coverage and nightly verification results: **[scrapebench.dev](https://scrapebench.dev)**

### Works with AI assistants (MCP)

Callable as an MCP tool, so Claude, Cursor, VS Code Copilot and other MCP clients can run it directly. Grab the config from the [MCP tab](https://apify.com/scrapebench/us-contractor-license-lookup/api/mcp) on this page — Apify hosts the server and keeps that snippet current, and OAuth signs you in on first connect, so no API token goes in your config file.

Then just ask:

> "Verify California contractor licence 22726 and tell me whether it is active."

It returns a verdict with the reason behind it, and says explicitly when a state is not covered rather than implying the contractor is unlicensed. Runs started this way bill exactly like any other run.

### Data & lawful use

This actor collects publicly available information that the state contractor licensing boards it reads publishes for public
lookup — licence records, business listings and the contact details professionals list there.
Some of it identifies individuals (sole proprietors, licence holders, agents), so treat any
names, phone numbers and emails in your results as personal data.

When you run this actor, you decide what is collected and what it is used for, so you are the
controller of that data and we act only as your processor (Apify Standard Actor Contract).
You need a lawful basis for your use, must honour opt-out and deletion requests, and must
follow the marketing rules that apply to you (CAN-SPAM, TCPA, GDPR, CCPA and similar).
Don't use the output for unsolicited bulk messaging, for screening covered by the FCRA, or
for anything the source's own terms forbid.

Your results are stored in your own Apify account. We don't keep a copy of your results and
don't sell or share them.

### Disclaimer

This is an independent tool. It is not affiliated with, endorsed by or sponsored by
the state contractor licensing boards it reads. Names and marks are used only to describe the data source. Records are
provided as published by the source and may be out of date or incomplete. For anything
consequential (hiring, a permit, a legal decision), confirm the record with the issuing
agency. You are responsible for making sure your use complies with applicable law and the
source's terms.

### Need help?

Open an issue on this actor's **Issues** tab and we'll respond there. Bug reports, missing
fields and feature requests are all welcome. Guides and examples: [scrapebench.dev](https://scrapebench.dev).

If you are a person named in this data and want your record removed from our copy, email
apify-help@scrapebench.dev; the source agency remains the official record.

Your run may be used to improve our services. If something looks off, or you want this actor to fit your use better, please share your run data with us: [Apify Console → Settings → Security → Share run data with developers](https://console.apify.com/settings/security). It lets us see exactly what went wrong and keep improving the actor.

# Actor input Schema

## `mode` (type: `string`):

lookup / verify: mode 'verify' - give company names or licence numbers and get one verdict per contractor per state (active, expired, revoked, suspended, inactive, not_found...).
export: mode 'export' - download every licensed contractor in each state you pick, or a slice of it via 'stateFilters'.
Each state is answered by that state's own scrapebench actor, run under your account.

## `states` (type: `array`):

2-letter codes. Verify covers AL, AR, AZ, CA, CO, CT, DC, DE, FL, IA, ID, MA, MD, MI, MN, MS, NC, ND, NE, NJ, NM, NV, NY, OH, OR, PA, RI, SC, TN, TX, VA, WA, WV. Export covers AL, AR, AZ, CA, CO, CT, DC, DE, FL, IA, ID, IL, LA, MD, MN, MS, ND, NE, NJ, NM, NV, NY, OH, OR, PA, RI, SC, TN, TX, UT, VA, WA, WV. In verify mode every name and number is checked in every state listed. A state outside the list returns 'unverifiable_state_not_covered' - the check was NOT performed, which is not a finding that anybody is unlicensed.

## `state` (type: `string`):

Single-state form of 'states', e.g. 'TX'. Kept for saved tasks and agents; both may be set.

## `companies` (type: `array`):

Business or licensee names to verify. Matching is EXACT on the full name once case, punctuation and company suffixes (LLC, Inc, Corp) are ignored - 'ABC Roofing' will NOT match 'ABC Roofing & Siding', by design. A name matching more than one licence returns 'check_failed' rather than a guess. Use licence numbers whenever you have them.

## `licenseNumbers` (type: `array`):

Licence numbers to verify - the most reliable input. Punctuation and leading zeros are ignored. DE, IA, MS, OH and VA cannot be searched by number (their boards only take names) and return 'check_failed' asking for the name.

## `company` (type: `string`):

Single-value form of 'companies'.

## `licenseNumber` (type: `string`):

Single-value form of 'licenseNumbers'.

## `stateFilters` (type: `object`):

Export mode only. Keyed by state code; each value is passed verbatim as that state's actor input, so use that actor's own field names (linked in the README), e.g. {"TX": {"county": "Travis"}, "CA": {"licenseClass": "C-10", "county": "Sacramento"}}. A state with no entry exports with no filter.

## `maxResults` (type: `integer`):

Export mode only: the most rows delivered per state. Billing is per row delivered, so this is also your spend limit per state.

## Actor input object example

```json
{
  "mode": "verify",
  "states": [
    "CA",
    "TX"
  ],
  "companies": [
    "Ransome Company"
  ],
  "licenseNumbers": [
    "22726"
  ],
  "stateFilters": {
    "TX": {
      "licenseType": "Electrical Contractor",
      "county": "Travis"
    }
  },
  "maxResults": 500
}
```

# Actor output Schema

## `results` (type: `string`):

One verdict per contractor checked.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "states": [
        "CA",
        "TX"
    ],
    "companies": [
        "Ransome Company"
    ],
    "licenseNumbers": [
        "22726"
    ],
    "stateFilters": {
        "TX": {
            "licenseType": "Electrical Contractor",
            "county": "Travis"
        }
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("scrapebench/us-contractor-license-lookup").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "states": [
        "CA",
        "TX",
    ],
    "companies": ["Ransome Company"],
    "licenseNumbers": ["22726"],
    "stateFilters": { "TX": {
            "licenseType": "Electrical Contractor",
            "county": "Travis",
        } },
}

# Run the Actor and wait for it to finish
run = client.actor("scrapebench/us-contractor-license-lookup").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "states": [
    "CA",
    "TX"
  ],
  "companies": [
    "Ransome Company"
  ],
  "licenseNumbers": [
    "22726"
  ],
  "stateFilters": {
    "TX": {
      "licenseType": "Electrical Contractor",
      "county": "Travis"
    }
  }
}' |
apify call scrapebench/us-contractor-license-lookup --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,scrapebench/us-contractor-license-lookup"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/eesmN8mL336FTZZ0G/builds/Hbch0FqriVZFBlvwA/openapi.json
