# Contractor License Lookup and Leads (`zaiq/contractor-license-lookup`) Actor

Verify US contractor licenses and find licensed contractors by trade and area. Get a clear status verdict, board classifications, dates, published bonds and insurance, source freshness and scheduled change monitoring.

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

## Pricing

from $2.40 / 1,000 license records

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## Contractor License Lookup and Leads

![Contractor License Lookup and Leads: Verify US contractor licenses and find licensed contractors by trade and area](https://api.apify.com/v2/key-value-stores/4VJNANhwKgqPpXtAi/records/hero-contractor-license-lookup.jpg)

Get one **sourced verdict per US contractor licence**: the published status, trade, expiry date, and available bond or insurance information. Verify licence numbers or business names, find contractors by trade and area, and schedule checks that return new or changed records.

**$0.003 per delivered licence record ($3 per 1,000), the same price on every Apify plan.** Failure notes and unchanged monitored records have no licence-record charge. Start with `{"states":["WA"],"city":"Seattle","trades":["general-building"],"maxResults":10}`.

**Coverage is specific to each official source.** California, Washington, Oregon, Connecticut and Virginia cover broad contractor categories. Colorado covers electrical and plumbing contractor registrations; New York covers statewide mold assessment and remediation contractor licences. This is not an all-state or all-trade directory.

| State | Records available | Important limits |
|---|---|---|
| California | CSLB master: current and expired but renewable licences, classifications, bonds and workers’ compensation | Cancelled, revoked and expired non-renewable licences are excluded. Bulk snapshot; a download can be incomplete. |
| Washington | L\&I construction, electrical and plumbing registrations; separate bond and liability-insurance feeds | Original issue date, county, workers’ compensation and discipline are not published in these feeds. |
| Oregon | Active CCB licences, endorsements, bonds, insurance and workers’ compensation exemption classification | Active only; broad endorsements do not identify every specialty trade. |
| Connecticut | Construction-related contractor credentials, including inactive statuses | No bond, insurance or county fields in this source. |
| Colorado | Electrical and plumbing contractor registrations and published disciplinary actions | No general-contractor coverage, street address, county, bonds or insurance. |
| New York | Statewide mold assessment and remediation contractor licences | No general-contractor or local home-improvement coverage. |
| Virginia | Current Class A, B and C contractor rosters plus legacy Class A | No suspended, revoked or inactive records, bonds, insurance or county. |

### What you get

| Field | What it holds |
|---|---|
| Verdict first | A readable line such as active licence, classification, published bond information and expiry date. `riskFlags` highlight date contradictions, stale sources and other published concerns. |
| Identity and status | `state`, `licenseNumber`, `businessName`, `licenseeName` where published, `status`, `statusRaw`, `statusReason` and `matchType`. |
| Trades | Normalized `trades` plus each board’s original `classifications` code and description. Multiple endorsements are combined into one licence row. |
| Dates | `issuedOn`, `effectiveOn`, `expiresOn` and `recordUpdatedOn` where the source identifies them. Missing dates remain null. |
| Bond and insurance evidence | Published `bonds`, `insurance` and `workersComp`, including company, amount, policy dates and exemption classification where available. Washington join availability is explicit. |
| Discipline | Colorado’s published action rows and California’s disciplinary-bond fields. An empty list is not proof of a clean disciplinary history. |
| Published address and phone | Business or mailing address, city, ZIP, county and phone where the board publishes them. No external personal-contact enrichment. |
| Evidence and freshness | `recordUrl`, `evidenceUrl`, `sourceUrl`, `sourceUpdatedAt`, `checkedAt`, `freshness` and `sourceAgeDays`. |
| Monitoring changes | `changeType` is baseline, new or changed. `changes` gives before/after values. Unchanged records are omitted. |
| SUMMARY | Per-state source, scope, freshness, scanned and delivered counts, completeness, skipped malformed records, route failures and charged events. |

### What people use it for

- **Contractor lead lists:** Find licensed businesses in a published trade and mailing area for suppliers or home-service marketing.
- **Marketplace and property-management checks:** Look up a supplied licence number and retain its source record and dated status.
- **Bond and insurance review:** Collect the policy information a board publishes, including expiry or cancellation dates, for further review.
- **Recurring portfolio monitoring:** Schedule the same watchlist and receive changed statuses and new matching licences without paying again for unchanged records.

### How to use it

1. Run the prefilled Washington leads query to see ten real records. Open the Licenses table for verdicts and the Summary link for coverage.
2. For verification, set `mode` to `verify` and enter licence numbers as `STATE:number`. Business-name searches return candidates; compare their identifiers before selecting a business.
3. For leads, choose states, trades or original classification codes, and optional city, county or ZIP filters. Raise `maxResults` to collect a larger list.
4. For scheduled comparisons, add a named `monitorStore`, keep the same filters, and schedule one run at a time. The first successful deliveries form the baseline.
5. Export the dataset or read it by API. Use `kind=license` for licence records and `kind=note` for free explanations. Check `SUMMARY.coverage[].complete` before treating a scan as exhaustive.

### Input

| Field | Example | Notes |
|---|---|---|
| mode | `leads` or `verify` | Default: leads. Verification returns the actual status regardless of the leads status filter. |
| states | `["WA", "OR"]` | CA, WA, OR, CT, CO, NY or VA; omitted or empty defaults to WA. |
| licenseNumbers | `["CA:123456"]` | Verify mode; prefix required when more than one state is selected. Keep board prefixes and leading zeros. Up to 500 entries. |
| businessNames | `["WA:Example Building Company"]` | Verify mode. With no state prefix, searches the selected states. Up to 500 entries. |
| nameMatch | `contains` | Or exact, ignoring case. A matching name is a candidate, not proof of identity. |
| trades | `["roofing", "electrical"]` | Any selected normalized trade. Empty means all; classifications come from the board, never from a business-name guess. |
| classifications | `["CA:C-39", "VA:ELE"]` | Any selected original classification. If trades are also supplied, both filters must match. |
| city / county / zip | `Seattle` / empty / empty | Exact city or county ignoring case; ZIP or ZIP+4. County is supported in CA and OR only. Addresses do not establish a service area. |
| status | `active` | Also all, expired, suspended, revoked, inactive, cancelled, pending or unknown. Active-only sources cannot answer other statuses. |
| newlyLicensedDays | `30` | Uses the published original issue date. Zero disables the filter; unavailable in WA. |
| maxResults | `10` | Total delivered records across selected states in input order; maximum 250,000. Unchanged records do not consume this limit. |
| includeInsurance | on | Joins Washington bond and liability-insurance feeds. Other states retain policy fields already in their primary record. |
| monitorStore | `contractor-watchlist` | Optional named key-value store. Same name and filters reuse the baseline. Different filters create a separate scope. |

### Output

Each result is one row in the run's dataset (download it as JSON, CSV, Excel, XML or HTML, or read it through the API). An example row (values shortened):

```json
{
  "verdict": "Active WA licence · 01 GENERAL · bond $30,000 on file · insurance $1,000,000 on file · expires 15 Aug 2027",
  "kind": "license",
  "state": "WA",
  "status": "active",
  "statusRaw": "ACTIVE",
  "classifications": [
    {
      "code": "01",
      "description": "GENERAL",
      "trade": "general-building"
    }
  ],
  "trades": [
    "general-building"
  ],
  "effectiveOn": "2025-08-15",
  "expiresOn": "2027-08-15",
  "workersComp": {
    "status": "not_published"
  },
  "freshness": "recent",
  "sourceUpdatedAt": "2026-10-26",
  "licenseNumber": "EXAMPLE123AB",
  "businessName": "Example Building Company",
  "address": "100 Example Street",
  "city": "Example City",
  "zip": "00000",
  "phone": "000-555-0100",
  "recordUrl": "https://example.invalid/official-license/EXAMPLE123AB",
  "bonds": [
    {
      "status": "on_file",
      "company": "Example Surety",
      "amountUsd": 30000.0,
      "number": "EXAMPLE-BOND",
      "effectiveOn": "2026-08-20",
      "expiresOn": null,
      "cancelsOn": null,
      "expirationText": "Until Canceled"
    }
  ],
  "insurance": [
    {
      "status": "on_file",
      "company": "Example Insurer",
      "amountUsd": 1000000.0,
      "number": "EXAMPLE-POLICY",
      "effectiveOn": "2026-06-12",
      "expiresOn": "2027-06-12",
      "cancelsOn": null,
      "expirationText": "2027-06-12T00:00:00.000"
    }
  ]
}
```

This example is shortened from a real test on 26 October 2026. Business and insurer names, licence and policy identifiers, phone, address, city, ZIP and site addresses have been replaced. Status, classifications, policy amounts and dates retain the test values. The SUMMARY record in the run’s key-value store carries the coverage and completeness table, source freshness and charges.

### Pricing

- **$0.003 per delivered licence record.** The same price on every Apify plan.
- Only a record that arrives is charged. Missing results, unsupported inputs and failure notes have no licence-record charge.
- Monitoring charges the first baseline and then only new or changed licence records. Unchanged records are omitted.
- Apify’s **$0.00005 start charge** counts towards Maximum cost per run, including runs with no delivered licence records.
- No AI model or separate AI fee is used.

What typical jobs cost at the list price:

| Job | Cost |
|---|---|
| 1 delivered licence | $0.00305 including start |
| 10 delivered licences | $0.03005 including start |
| 1,000 delivered licences | $3.00005 including start |
| 50,000 delivered licences | $150.00005 including start |
| Unchanged monitoring run | $0.00005 start charge; no licence-record charge |

Set Maximum cost per run before starting. A limit of $0.00605 covers two licence records and the start charge. The Actor checks available room before requesting paid results, stops when the next record cannot fit, and still writes free notes. A result limit or spending limit can leave a state scan incomplete.

### Tips

- Use exact licence numbers for a known watchlist. Use trade and area filters for discovery, then retain the state and licence number as the identity.
- For Washington, turn off includeInsurance when only the licence status and classification are needed. This avoids two supplemental API lookups per batch.
- For Oregon, search a broad endorsement or original classification. Do not infer a roofing or electrical specialty from a general CCB registration.
- For recurring checks, keep one named store per watchlist, maintain the same filters, and avoid overlapping runs.
- Use the official record URL and source date when reviewing a result. The source’s omissions and incomplete flags are part of the result.

### Use it through the API

**Python**

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("zaiq/contractor-license-lookup").call(run_input={"states": ["WA"], "city": "Seattle", "trades": ["general-building"], "maxResults": 10})
for row in client.dataset(run.default_dataset_id).iterate_items():
    print(row)
```

**JavaScript**

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

const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const run = await client.actor('zaiq/contractor-license-lookup').call({"states": ["WA"], "city": "Seattle", "trades": ["general-building"], "maxResults": 10});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

**HTTP (one call, results back)**

```bash
curl -X POST 'https://api.apify.com/v2/acts/zaiq~contractor-license-lookup/run-sync-get-dataset-items?token=<YOUR_APIFY_TOKEN>' \
  -H 'Content-Type: application/json' \
  -d '{"states": ["WA"], "city": "Seattle", "trades": ["general-building"], "maxResults": 10}'
```

### Integrations and schedules

- **Schedules:** run it every hour, day or week from the Apify Console (Schedules) and keep a history of results.
- **Make, Zapier, n8n, Google Sheets, Slack, Airtable:** connect it through Apify's integrations, or send each finished run to a **webhook**.
- **Datasets:** results stay in the run's dataset; export them or read them by API whenever you need them.

### Use it with AI agents (MCP)

Add Apify's MCP server (`https://mcp.apify.com`) to Claude, ChatGPT, Cursor, VS Code or any MCP client. The agent can find this Actor and run it for you: ask it for "find active roofing contractor licences in this state and city, with their official record links". AI agents can also pay for runs directly through Apify's agentic payments.

### Limits

- Only the seven sources described above are supported. Statewide coverage does not imply every trade or every historical status.
- California and Virginia require bulk downloads even for a small query. They can take several minutes. California can return a truncated file; received rows remain useful, but coverage is marked incomplete and absent records are not interpreted.
- Bulk files use direct or datacenter connections. Large residential downloads are refused to keep a small query from consuming disproportionate resources. API calls can try direct, datacenter and fresh residential routes when available; failures are recorded.
- Official data can lag a live board record. The verdict reflects the source snapshot, not a guarantee of present eligibility, insurance coverage or permission for a particular job. Sources older than 14 days receive a review flag.
- “On file” describes dated published policy evidence, not confirmation from an insurer. Oregon’s nonexempt workers’ compensation classification does not establish current coverage.
- No record does not establish that a contractor is unlicensed. A missing record in an active-only source is reported as unknown, never as a guessed suspension or revocation.
- Licence holders may be people or businesses. Output is limited to their official published licence record; no email discovery or outside personal-contact research is performed.
- Monitoring retains delivered checkpoints on incomplete runs. Missing records are flagged only after a complete state scan. A failed optional policy lookup preserves the previous policy checkpoint rather than inventing a removal.
- Run a monitoring scope sequentially. A changed filter starts a different scope. A rolling newly-licensed window may stop including older records; a missing-source note in that scope does not establish a licence-status change.
- Source request limits and up to 250,000 delivered records apply. Incomplete, malformed, repeated or unexpectedly short feeds are reported. Raising the result limit does not remove source access restrictions.

### FAQ

**Does this cover all US contractor licences?**

No. It covers the listed official sources and their published scope. Colorado and New York have limited trade coverage; Oregon and Virginia publish current/active records only.

**Can I look up a business without its licence number?**

Yes. Use verify mode and businessNames. Contains is the default; exact is available. A name match is a candidate, so use its state, licence number and address to distinguish businesses.

**Can I export every matching contractor?**

Set maxResults high enough and give the run enough spending room. Check the per-state completeness flags. Records beyond either limit, inaccessible sources and malformed records prevent an exhaustive claim.

**How do trades work across states?**

The Actor maps published classifications to a common list such as electrical, plumbing, roofing and general-building. It keeps each board’s original code and label. Broad registrations do not prove an unlisted specialty.

**Will monitoring catch active-to-suspended changes?**

For sources that publish both statuses, monitoring scans all statuses within the selected trade and area and returns changes to watched licences. Active-only sources can only report that a record disappeared, with status unknown.

**What happens after an Actor restart?**

Rows already acknowledged in the same run’s dataset are recognized by a stable resultId and are not delivered or charged twice. Monitoring can recover their checkpoints from that dataset.

**Do I need a board account or an AI key?**

No. The Actor reads public, signed-out records and computes deterministic verdicts. It does not solve challenges or sign into licensing sites.

**Why can a verdict say review needed while status says active?**

The board may publish an active status alongside an expiry date in the past, an expired policy, or an old source snapshot. The original status is preserved and the conflicting evidence is flagged.

**Why is a run incomplete if it delivered the requested number?**

Reaching maxResults or the spending limit stops collection before exhaustion. Complete means the source/query was exhausted without a known data issue, not merely that a requested batch was delivered.

### Related Actors from Zaiq

- [government-tenders-scraper](https://apify.com/zaiq/government-tenders-scraper): public tenders and contract awards
- [contact-form-finder](https://apify.com/zaiq/contact-form-finder): public business contact pages and enquiry forms

### Support

Questions, a site that changed, or a field you need? Open an issue on this Actor's **Issues** tab: we answer there, usually within a day.

***

Also found as: contractor license lookup, contractor license verification, licensed contractor leads, CSLB license lookup, contractor license monitoring, contractor directory by trade.

# Actor input Schema

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

Find licenses by trade and area, or verify exact license numbers and business-name candidates.

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

Supported sources: California CSLB, Washington L\&I, Oregon CCB, Connecticut contractor credentials, Colorado electrical/plumbing, New York mold contractors, Virginia current A/B/C. Empty defaults to Washington.

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

Verify mode. Use STATE:number, such as CA:123456. With one state selected, its prefix is optional. Keep leading zeros and board prefixes. Up to 500 inputs.

## `businessNames` (type: `array`):

Verify mode. Board-record name matches only, with no external enrichment. Prefix with STATE: to choose a state. A name match is a candidate, not proof of identity.

## `nameMatch` (type: `string`):

Contains returns all matching board records up to the result limit. Exact matches the full name ignoring case.

## `classifications` (type: `array`):

Leads mode. Original state codes, optionally prefixed by state, for example CA:C-39 or VA:ELE. Separate values are alternatives; trades and area filters must also match.

## `city` (type: `string`):

Leads mode. Exact city in the published business/mailing address, ignoring case. Leave empty for all cities. This is an address filter, not a service-area radius.

## `county` (type: `string`):

Leads mode. Published county, supported in California and Oregon. Other states return a free unsupported-filter note.

## `zip` (type: `string`):

Leads mode. Five-digit US ZIP or ZIP+4 from the published address. Leave empty for all ZIP codes.

## `status` (type: `string`):

Leads filter. Verify mode returns the actual status regardless of this setting. Monitoring also returns changes to previously watched licenses when their status leaves this filter. Active-only sources cannot report revoked or expired licenses.

## `newlyLicensedDays` (type: `integer`):

Leads mode. Uses a published original issue date, from today back through N days. Zero disables it. Washington has no separately identified original issue date and returns a free unsupported-filter note.

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

Total across selected states, in their input order. A capped scan is marked incomplete. Increase this to retrieve all matching records; maximum 250,000. Unchanged monitored records do not consume this limit.

## `includeInsurance` (type: `boolean`):

Fetch the separate Washington public bond and insurance feeds. Other states retain fields already in their primary record. On-file policies are not a guarantee of coverage; dates and limitations are included.

## `monitorStore` (type: `string`):

Optional named store for scheduled comparisons. First run returns the baseline; later runs charge only new or changed records. Use the same input filters and one run at a time. Missing active-only records produce a free unknown-status note, never a guessed revocation.

## `trades` (type: `array`):

Leads mode. Match any selected trade based only on published classifications; empty means all. Oregon uses broad endorsements; Colorado and New York have restricted trade coverage.

## Actor input object example

```json
{
  "mode": "leads",
  "states": [
    "WA"
  ],
  "licenseNumbers": [],
  "businessNames": [],
  "nameMatch": "contains",
  "classifications": [],
  "city": "Seattle",
  "status": "active",
  "newlyLicensedDays": 0,
  "maxResults": 10,
  "includeInsurance": true,
  "trades": [
    "general-building"
  ]
}
```

# Actor output Schema

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

Dataset: the results.

## `summary` (type: `string`):

What each input returned, and notes.

# 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 = {
    "mode": "leads",
    "states": [
        "WA"
    ],
    "city": "Seattle",
    "maxResults": 10,
    "trades": [
        "general-building"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("zaiq/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 = {
    "mode": "leads",
    "states": ["WA"],
    "city": "Seattle",
    "maxResults": 10,
    "trades": ["general-building"],
}

# Run the Actor and wait for it to finish
run = client.actor("zaiq/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 '{
  "mode": "leads",
  "states": [
    "WA"
  ],
  "city": "Seattle",
  "maxResults": 10,
  "trades": [
    "general-building"
  ]
}' |
apify call zaiq/contractor-license-lookup --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,zaiq/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/URS2g4j24y2AY6RhI/builds/vRIlvlUupj3cra7NO/openapi.json
