# Contractor License Verify - AI Agent Tool (17 States) (`scrapebench/contractor-license-verify`) Actor

Ask one question, get one structured verdict: is this contractor licensed in this state? Enumerated verdicts built for an AI agent to branch on - not prose.

- **URL**: https://apify.com/scrapebench/contractor-license-verify.md
- **Developed by:** [ScrapeBench](https://apify.com/scrapebench) (community)
- **Categories:** Lead generation
- **Stats:** 3 total users, 2 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/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Contractor License Verify - AI Agent Tool (17 States)

### Pain points

- An AI agent building a vendor-onboarding or CRM-intake flow cannot answer 'is this contractor licensed?' today - there is no public API for it.
- State licensing portals are ASP.NET WebForms, bulk rosters and CAPTCHA-gated search pages; none of them are callable by a model.
- Batch scrapers return a dataset. An agent needs one answer, in a shape it can branch on, with a citation.

### What we solve

- One contractor, one state, one flat verdict object - built for a single agent call rather than a bulk export.
- Enumerated verdicts (active, expired, revoked, suspended, inactive, not\_found, unverifiable\_state\_not\_covered, check\_failed) so an agent branches on a value instead of parsing prose.
- Every answer cites the state board it came from and when it was checked.

### Summary

A single-query license verification tool shaped for an AI agent. Give it a state and either a company name or a license number, and it routes to that state's official licensing board and returns one flat object: a verdict, the reason in plain language, the board's own raw status string, the expiry date, and which board answered. It covers 17 states - AL, AR, CA, CT, FL, MA, MI, MN, NC, NM, NV, OR, SC, TN, TX, VA, WA - including boards no agent can otherwise reach: CSLB is an ASP.NET WebForms portal, SC LLR sits behind a reCAPTCHA, New Mexico behind an image CAPTCHA. The design rule that matters most here is what the tool refuses to say. 'not\_found' means the registry returned no match for that search and is explicitly 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' - because an agent repeats whichever word you hand it, and one of those is a defamation-shaped claim about a real business.

### Who it's for

- Developers building AI agents for vendor onboarding, CRM intake or procurement
- Teams wiring contractor verification into an LLM tool-calling flow
- Marketplaces and platforms that must verify a trade licence at signup
- Anyone who needs one licence answer on demand rather than a bulk export

### How to use

Set the input, run the actor, and collect results from the run's dataset (export to JSON/CSV/Excel, or pull via the Apify API). Example input:

```json
{
  "state": "CA",
  "licenseNumber": "22726"
}
```

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

### What you get

One row per record:

| Field | Description |
|---|---|
| `verdict` | active | expired | revoked | suspended | inactive | not\_found | unverifiable\_state\_not\_covered | check\_failed. The last three describe the CHECK, not the contractor |
| `verdict_reason` | Plain-language explanation, safe to quote to an end user |
| `verdict_basis` | Which evidence the verdict rests on: status, expiry\_date, status+expiry\_date, or none |
| `requested_company` | The company name that was searched, echoed back |
| `requested_license_number` | The license number that was searched, echoed back |
| `requested_state` | The state that was searched |
| `entity_name` | The licensed business or person as the board publishes it |
| `license_number` | License number as published by the board |
| `license_type` | License type or classification, where published |
| `status_raw` | The board's own status string, unmodified (e.g. 'Voluntary Surrender') |
| `expiry_date` | Expiration date as published by the board |
| `days_to_expiry` | Days until expiry; negative when already expired |
| `source_portal` | Which board answered (CSLB, TX TDLR, SC LLR...) - cite this |
| `checked_at` | UTC timestamp of the check |
| `coverage_note` | Which states are covered and what an uncovered state means |
| `source_url` | Where the answer came from |

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",
  "checked_at": "2026-08-06T19:20:11+00:00",
  "source_url": "https://www.cslb.ca.gov/OnlineServices/CheckLicenseII/LicenseDetail.aspx?LicNum=22726"
}
```

### Inputs

| Field | Required | Type | Default | Description |
|---|---|---|---|---|
| `state` | no | string | `"CA"` | REQUIRED. The 2-letter US state code of the licensing board to check, e.g. 'TX'. Supported: AL, AR, CA, CT, FL, MA, MI, MN, NC, NM, NV, OR, SC, TN, TX, VA, WA. Pass any other state and every result comes back with verdict 'unverifiable\_state\_not\_covered', which means the check was NOT performed - do not report that as the contractor being unlicensed. Contractor licensing is per-state, so you must know the state before you can verify anything; if the user has not told you which state, ask them rather than guessing. |
| `company` | no | string | — | The business or licensee name to look up, e.g. 'Ransome Company'. Use this when you do not have a licence number. Matching is EXACT on the full name once case, punctuation and company suffixes (LLC, Inc, Corp) are ignored - so 'ABC Roofing' will NOT match 'ABC Roofing & Siding', by design. If a name matches more than one licence you get verdict 'check\_failed' rather than a guess, because returning the wrong contractor's licence is worse than returning nothing. Supply 'licenseNumber' instead whenever you have it: it is exact and cannot mis-match. |
| `licenseNumber` | no | string | — | The contractor's licence number, e.g. '22726'. This is the most reliable input - prefer it over the company name whenever the user has given you one. Punctuation and leading zeros are ignored, so 'CCB-123456' and '123456' both work. If the number is not in the state's registry you get verdict 'not\_found', which means the registry had no such record - it is NOT evidence that the contractor is unlicensed. |
| `companies` | no | array | `[]` | Batch form of 'company' - check many names in ONE run instead of starting a run per name. All entries must be in the same state. You are billed per verdict returned, not per run, so batching costs the same and is much faster. |
| `licenseNumbers` | no | array | `[]` | Batch form of 'licenseNumber' - check many licence numbers in ONE run. All entries must be in the same state. Billing is per verdict returned, so batching costs the same as separate runs and is much faster. |

### Pricing (Pay Per Event)

You pay per result (`license-verified`) — **no charge for empty runs**. Example: **100 verdicts resolved on a state board** at *$0.004/result* ≈ **$0.40**.

Charged only for a verdict actually resolved on a board. not\_found, unverifiable\_state\_not\_covered and check\_failed are free.

The per-state lookup Actors this tool calls on your behalf bill their own per-match fees to your account, so budget both lines: about **$0.008 per verdict** in the 15 open-data states, and about **$0.034** in New Mexico and South Carolina, where the board CAPTCHAs the search itself and every check costs a solve.

#### Measured per-state latency (2026-08-06, one licence-number lookup each)

| | Seconds |
| --- | --- |
| Median across all 17 states | **13.3s** |
| Fastest (FL, CA, WA, MI, NC) | 10.6 - 11.3s |
| Slowest: South Carolina (reCAPTCHA per check) | **30.8s** |
| New Mexico (image CAPTCHA per check) | 19.6s |
| Massachusetts / Alabama / Connecticut | 16 - 17s |

If you are calling this from an agent loop, set your timeout above 35 seconds and expect ~13s typical. The cost is the state portal, not this tool.

### Use cases

- Agent tool call - give an LLM the ability to answer 'is this contractor licensed in Texas?' with a citable source.
- Vendor onboarding - verify a trade licence at signup before a contractor is approved.
- CRM intake - enrich a new contractor record with licence status, type and expiry at the moment it is created.
- Spot check - one contractor, one state, one answer, without configuring a batch scraper.

### Why this actor

- Verdicts are enumerated, never prose - an agent branches on a value instead of interpreting a sentence.
- not\_found is its own verdict and the schema says in so many words that it does NOT mean unlicensed. That is the single most important line here: an agent that gets this wrong makes a defamation-shaped claim about a real business.
- Fails closed. An error, a timeout or a name matching two licences returns check\_failed, never a pass.
- A voluntarily ended licence is 'inactive', never 'revoked' - the boards publish both, and only one of them is a disciplinary finding.
- Reaches boards an agent cannot: CSLB's WebForms portal, SC LLR's reCAPTCHA, New Mexico's image CAPTCHA.

### Limitations & updates

Covers 17 states; anything else returns 'unverifiable\_state\_not\_covered'. Louisiana is deliberately excluded - its board publishes a bulk roster with no per-contractor search, so a single lookup would mean downloading the whole thing. Some boards are partial by source: AL, TX and VA publish no license status (verdict rests on the expiry date), and NC and SC publish no expiration date (verdict rests on status); 'verdict\_basis' reports which on every answer. Virginia cannot be searched by license number at all. Name matching is deliberately strict - an exact match on the full normalized business name or nothing - so a license number is always the more reliable input, and a name matching two licences returns 'check\_failed' rather than a guess. Data reflects what each board publishes at check time; 'checked\_at' is on every answer for that reason.

### FAQ

**Which states are covered?**

Seventeen: AL, AR, CA, CT, FL, MA, MI, MN, NC, NM, NV, OR, SC, TN, TX, VA and WA. Any other state returns verdict 'unverifiable\_state\_not\_covered', which means no check was performed - it is not a finding about the contractor.

**Does 'not\_found' mean the contractor is unlicensed?**

No, and this is the most important line in the whole listing. 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'. If your agent reports that as 'unlicensed' to an end user, it has made a false and damaging claim about a real business. Treat it as 'go and check', never as a finding.

**Why is there an 'inactive' verdict as well as 'revoked'?**

Because the boards publish both, and conflating them would be defamatory. 'Revoked' and 'suspended' are disciplinary actions. 'Lapsed', 'Voluntary Surrender', 'Archived' and 'Voluntary Termination' are administrative or the licensee's own choice - all real strings from real boards. Those map to 'inactive', and the reason field says so explicitly.

**How fast is it?**

Measured on 2026-08-06: median 13.3 seconds, fastest around 10.6s (Florida, California, Washington), slowest 30.8s for South Carolina, whose board puts a reCAPTCHA on the search itself and so costs a solve per check. New Mexico is 19.6s for the same reason. Set an agent timeout above 35 seconds.

**What is 'verdict\_basis' for?**

It tells you what the verdict is standing on, because the boards do not all publish the same fields. Alabama, Texas and Virginia publish no status field, so a verdict there rests on the expiration date alone. North Carolina and South Carolina publish no expiration date, so those rest on status alone. Everywhere else you get both.

**Can I check several contractors at once?**

Yes - use 'companies' or 'licenseNumbers' with a list, all in the same state. Billing is per verdict returned, so a batch costs the same as separate runs and is much faster. The single-value fields exist for the agent case, where one question is the whole point.

**Can I search Virginia by license number?**

No. Virginia's DPOR data is a name-filtered roster with no license-number search axis, so a number-only request there returns 'check\_failed' telling you to supply 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 rather than companies, so a licensed person's name can appear where the board publishes it that way. No personal or PII 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 specifically, including owner/personnel name search.
- **Multi-State Contractor & Trade License Lookup** — You want the same contractor checked across WA, OR, TX & CT in one run.
- **South Carolina Contractor License Lookup & Verify (SC LLR)** — You need South Carolina search by name, city or classification rather than a single verdict.
- **New Mexico Contractor License Lookup & Verify (NM CID)** — You need New Mexico search by business name, city or ZIP.

### 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 Contractor License Verify - AI Agent Tool (17 States)](https://scrapebench.dev/guides/how-to-contractor-license-verify/)
- **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)**

# Actor input Schema

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

REQUIRED. The 2-letter US state code of the licensing board to check, e.g. 'TX'. Supported: AL, AR, CA, CT, FL, MA, MI, MN, NC, NM, NV, OR, SC, TN, TX, VA, WA. Pass any other state and every result comes back with verdict 'unverifiable\_state\_not\_covered', which means the check was NOT performed - do not report that as the contractor being unlicensed. Contractor licensing is per-state, so you must know the state before you can verify anything; if the user has not told you which state, ask them rather than guessing.

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

The business or licensee name to look up, e.g. 'Ransome Company'. Use this when you do not have a licence number. Matching is EXACT on the full name once case, punctuation and company suffixes (LLC, Inc, Corp) are ignored - so 'ABC Roofing' will NOT match 'ABC Roofing & Siding', by design. If a name matches more than one licence you get verdict 'check\_failed' rather than a guess, because returning the wrong contractor's licence is worse than returning nothing. Supply 'licenseNumber' instead whenever you have it: it is exact and cannot mis-match.

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

The contractor's licence number, e.g. '22726'. This is the most reliable input - prefer it over the company name whenever the user has given you one. Punctuation and leading zeros are ignored, so 'CCB-123456' and '123456' both work. If the number is not in the state's registry you get verdict 'not\_found', which means the registry had no such record - it is NOT evidence that the contractor is unlicensed.

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

Batch form of 'company' - check many names in ONE run instead of starting a run per name. All entries must be in the same state. You are billed per verdict returned, not per run, so batching costs the same and is much faster.

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

Batch form of 'licenseNumber' - check many licence numbers in ONE run. All entries must be in the same state. Billing is per verdict returned, so batching costs the same as separate runs and is much faster.

## Actor input object example

```json
{
  "state": "CA",
  "companies": [],
  "licenseNumbers": []
}
```

# 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 = {
    "state": "CA"
};

// Run the Actor and wait for it to finish
const run = await client.actor("scrapebench/contractor-license-verify").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 = { "state": "CA" }

# Run the Actor and wait for it to finish
run = client.actor("scrapebench/contractor-license-verify").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 '{
  "state": "CA"
}' |
apify call scrapebench/contractor-license-verify --silent --output-dataset

```

## MCP server setup

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

```

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/vS9QgmLJGxuDlvy6s/openapi.json
