# GovData Labs — Multi-State Contractor License Verifier (`govdatalabs/govdata-labs-contractor-license-verifier`) Actor

GovData Labs verifies contractor license status, insurance, and bonds from official Washington, Oregon, New York, and Texas open data.

- **URL**: https://apify.com/govdatalabs/govdata-labs-contractor-license-verifier.md
- **Developed by:** [GovData Labs](https://apify.com/govdatalabs) (community)
- **Categories:** Business, Lead generation, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

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

## GovData Labs — Multi-State Contractor License Verifier

Look up contractor licenses in bulk from the official open-data APIs of Washington, Oregon, New York, and Texas. Each result is one flat record: status, dates, insurance, bond, address, and principals when the state publishes them, plus expiry flags.

Built for general contractors, suppliers, and compliance teams who need to check a bid list without opening four state portals.

### What you get

For every matched license:

- `state`, `license_number`, `business_name`, `normalized_name`
- `status` and `license_type` / `license_classification`
- `issued_on` and `expires_on` (`YYYY-MM-DD`)
- insurance carrier, amount, and expiry when the state publishes them
- bond company, amount, and expiry when the state publishes them
- `address`, `city`, `postal_code`, `phone`, `principals`
- flags: `expired`, `expiring_within_30_days`, `expiring_within_60_days`, `expiring_within_90_days`, `insurance_lapsed`, `bond_lapsed`
- `source_url` and `fetched_at`

The 60-day and 90-day flags include shorter windows. A license due in 10 days has all three set. An already expired license has `expired` set and the "within N days" flags clear.

`insurance_lapsed` and `bond_lapsed` are true only when the source shows missing coverage or a past expiry. If the state does not publish that field, the flag stays false. A Washington bond that says "Until Canceled" is not treated as lapsed; the original text is kept in `bond_expiration_raw`.

### Who it is for

- Pre-qualifying subcontractors before a bid
- Watching a vendor list for expirations, lapsed insurance, or lapsed bonds
- Appending a normalized license record to a CRM

### How a run works

1. Pass license numbers, business names, or both. Prefix a value with a state (`WA:ECOSTSC758NN` or `TX:Bee Cave Electric`) to search one state. Without a prefix, every selected state is searched.
2. License numbers are exact. Names are probed with a SoQL `LIKE` on the most distinctive word, then ranked with a fuzzy score that ignores punctuation and suffixes such as LLC, Inc, and Co. The default threshold is 0.84. Exact number hits have confidence 1.
3. Washington joins the license to the insurance and bond datasets on `contractorlicensenumber`. Principals come from `primaryprincipalname` on the license file. There is no fourth principals dataset.
4. Oregon has no status column. Status is Active or Expired from `lic_exp_date`. The responsible managing individual is stored as `principals`.
5. New York does not publish a carrier or a bond. `insurance_lapsed` is true when the registry says workers' compensation is missing and the business is not exempt.
6. Texas TDLR's file is every profession, not just contractors. Name search defaults to construction trades (electrical, air conditioning, elevator, appliance installation, water well, and related). A lookup by license number returns every profession that uses that number, because TDLR numbers are not unique across professions. Set `includeAllTexasLicenseTypes` to search the full file by name.
7. Rows are deduped on state, license number, and license type. Optional change tracking stores a snapshot in a named key-value store.

### Pricing

Pay per event. You pay only for what a run returns:

| Event | Price | When |
| --- | ---: | --- |
| `actor-start` | $0.010 | Once per run, after the input validates |
| `license-result` | $0.010 | Each output row |
| `change-detected` | $0.005 | Each new or changed row after a baseline snapshot |

One hundred licenses cost about $1.01. The first "only changes" run is the baseline and does not charge `change-detected`.

Platform compute is billed separately by Apify and is small: 512 MB memory is enough for typical runs (the Actor refuses more than 2048 MB).

### Example input

```json
{
  "licenseNumbers": ["WA:ECOSTSC758NN", "OR:213645", "NY:26-639SQ-CR", "TX:36159"],
  "businessNames": ["Reliable Construction"],
  "states": ["WA", "OR", "NY", "TX"],
  "maxResults": 50,
  "onlyChangesSinceLastRun": false
}
```

### Example output

Example data (fictional business and address) showing the shape of a Washington row. Real rows come from the live state datasets.

```json
{
  "state": "WA",
  "license_number": "EXAMPCL000AA",
  "business_name": "EXAMPLE CONSTRUCTION LLC",
  "normalized_name": "EXAMPLE CONSTRUCTION",
  "status": "Active",
  "license_type": "CONSTRUCTION CONTRACTOR",
  "issued_on": "2025-08-15",
  "expires_on": "2027-08-15",
  "insurance_carrier": "State National Ins Co Inc",
  "insurance_amount": 1000000,
  "insurance_expires_on": "2027-06-12",
  "bond_company": "Merchants Bonding Co (Mutual)",
  "bond_amount": 30000,
  "bond_expires_on": null,
  "bond_expiration_raw": "Until Canceled",
  "address": "123 Main St, Anytown, WA, 98000",
  "principals": "J. SMITH",
  "expired": false,
  "expiring_within_90_days": false,
  "insurance_lapsed": false,
  "bond_lapsed": false,
  "match_confidence": 1,
  "source_url": "https://data.wa.gov/d/m8qx-ubtq",
  "fetched_at": "2026-10-31T00:00:00.000Z"
}
```

### Limits

- 5,000 output rows per run, 5,000 queries per run.
- Name search ranks at most 40 candidates per state (configurable up to 100).
- Requests to one host are spaced 200 ms apart by default, with retries and backoff on HTTP 429 and 5xx.
- The snapshot keeps the newest 12,000 hashes so it stays under the key-value record size limit.
- An optional Socrata app token (`socrataAppToken` or `SOCRATA_APP_TOKEN`) raises the portal rate limit. Anonymous access is enough for short runs.

### Data sources

Verified against the live SODA APIs on 31 October 2026. All four requested states responded. None were dropped.

| State | Portal | Dataset | What it contributes |
| --- | --- | --- | --- |
| Washington | [data.wa.gov](https://data.wa.gov/d/m8qx-ubtq) | `m8qx-ubtq` licenses, `ciwg-agsx` insurance, `bzff-4fmt` bonds | Status, dates, address, primary principal, carrier, bond. Join key `contractorlicensenumber`. |
| Oregon | [data.oregon.gov](https://data.oregon.gov/d/g77e-6bhs) | `g77e-6bhs` | CCB license, bond, and insurance on one row. No status column. |
| New York | [data.ny.gov](https://data.ny.gov/d/i4jv-zkey) | `i4jv-zkey` | Contractor registry. Workers-compensation flag, no carrier or bond. |
| Texas | [data.texas.gov](https://data.texas.gov/d/7358-krk7) | `7358-krk7` | TDLR all-licenses file. Expiration and owner. No street address, insurance, or bond. |

Washington principals are the `primaryprincipalname` column on the license dataset, not a separate file. The three published ids are licenses, insurance, and bonds.

Published by the Washington State Department of Labor & Industries, the Oregon Construction Contractors Board (via Oregon open data), New York State, and the Texas Department of Licensing and Regulation. These are public open-data portals. Confirm each portal's terms before you redistribute an extract.

### Disclaimer

Data comes from official public sources and may lag the licensing board. It is not a legal determination of license status, insurance, or bond sufficiency. Confirm a result with the state before you rely on it for a bid, a claim, or an enforcement decision.

# Changelog

This Actor's version history is a separate document: https://apify.com/govdatalabs/govdata-labs-contractor-license-verifier/changelog.md

# Actor input Schema

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

License numbers to verify. Prefix a state code to search one state, for example WA:ECOSTSC758NN. Unprefixed numbers are searched in every state selected below.

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

Business names to match. The same STATE:name prefix works. Matching is fuzzy after stripping legal suffixes such as LLC and Inc.

## `queries` (type: `array`):

Optional rows of { state, licenseNumber, businessName }. A row with both a number and a name is looked up by number.

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

States to search when a value has no state prefix.

## `nameMatchThreshold` (type: `number`):

Minimum fuzzy-match score from 0.5 to 1. Exact license-number hits are always confidence 1.

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

Cap on output rows. Each row is billed as license-result. Default 100.

## `onlyChangesSinceLastRun` (type: `boolean`):

Compare this run with the snapshot in the named key-value store. The first run stores a baseline and bills only license-result. Later runs emit new or changed rows and also bill change-detected.

## `snapshotStoreName` (type: `string`):

Named key-value store that holds the last license snapshot.

## `includeAllTexasLicenseTypes` (type: `boolean`):

Texas TDLR publishes every profession in one dataset. Name search defaults to construction trades (electrical, air conditioning, elevator, appliance, water well). Turn this on to search barber, cosmetology, and the rest too. A lookup by license number always returns every profession that uses that number.

## `texasLicenseTypes` (type: `array`):

Optional override for the Texas name-search allow list. Leave empty to use the built-in construction list.

## `nameSearchLimit` (type: `integer`):

How many SoQL candidates to rank per name query before fuzzy matching.

## `requestSpacingMs` (type: `integer`):

Minimum delay between requests to the same open-data host. Default 200. Lower it only with a Socrata app token.

## `socrataAppToken` (type: `string`):

Optional Socrata app token. Anonymous access works for light runs. You can also set the SOCRATA_APP_TOKEN environment variable.

## Actor input object example

```json
{
  "licenseNumbers": [
    "WA:ECOSTSC758NN"
  ],
  "queries": [],
  "states": [
    "WA",
    "OR",
    "NY",
    "TX"
  ],
  "nameMatchThreshold": 0.84,
  "maxResults": 100,
  "onlyChangesSinceLastRun": false,
  "snapshotStoreName": "contractor-license-snapshots",
  "includeAllTexasLicenseTypes": false,
  "nameSearchLimit": 40,
  "requestSpacingMs": 200
}
```

# Actor output Schema

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

License rows written to the default dataset.

## `resultCount` (type: `string`):

Number of license rows saved.

## `changedCount` (type: `string`):

Rows whose tracked fields differed from the previous snapshot.

## `unmatchedCount` (type: `string`):

Lookups that returned no row above the name threshold.

# 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 = {
    "licenseNumbers": [
        "WA:ECOSTSC758NN"
    ],
    "queries": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("govdatalabs/govdata-labs-contractor-license-verifier").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 = {
    "licenseNumbers": ["WA:ECOSTSC758NN"],
    "queries": [],
}

# Run the Actor and wait for it to finish
run = client.actor("govdatalabs/govdata-labs-contractor-license-verifier").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 '{
  "licenseNumbers": [
    "WA:ECOSTSC758NN"
  ],
  "queries": []
}' |
apify call govdatalabs/govdata-labs-contractor-license-verifier --silent --output-dataset

```

## MCP server setup

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

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/9R9SzY0ugfz6UiZ6S/builds/SUqi1Z5fseGE1jhBz/openapi.json
