# Contractor License Change Monitor — new, expiring, lost (US) (`handa-labs/contractor-license-change-monitor`) Actor

Watch US state contractor registries (WA, OR, TX, NY) and get only what changed since your last run: new licenses, lapsed ones, status changes, renewals and licenses entering your expiry window. Official open data, no scraping.

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

## Pricing

from $5.00 / 1,000 license changes

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?

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 Change Monitor (WA/OR/TX/NY) — new, expiring, lost

**Know the moment a contractor's license changes.** This Actor watches the official contractor registries of Washington, Oregon, Texas and New York and returns **only what changed since your last run**:

| `changeType` | Meaning | Who cares |
|---|---|---|
| `new` | a license appeared in the registry (a new business, or a newly licensed trade) | suppliers, insurers, bonding agents, software vendors — first to call wins |
| `expiring-soon` | a license entered your expiry window (default 60 days) | renewal marketing: continuing-education, bonds, insurance |
| `renewed` | the expiration date moved forward | confirms the business is active |
| `status-changed` | e.g. ACTIVE → SUSPENDED (WA, NY expose status) | risk & compliance, prime contractors vetting subs |
| `removed` | the license no longer appears (lapsed / inactive / revoked) | stop selling to them; risk flag |

The first run stores a compressed **baseline snapshot** of your scope and (optionally) returns every license already inside the expiry window, so you have a usable list on day one. Every later run compares against the snapshot.

### Sample output

```json
{
  "type": "change",
  "changeType": "expiring-soon",
  "state": "WA",
  "licenseNumber": "ACMERRL123AB",
  "businessName": "ACME ROOFING LLC",
  "licenseType": "CONSTRUCTION CONTRACTOR",
  "status": "ACTIVE",
  "expirationDate": "2026-10-28",
  "daysToExpiry": 53,
  "city": "TACOMA",
  "zip": "98402",
  "phone": "2535550100",
  "sourceDataset": "https://data.wa.gov/d/m8qx-ubtq",
  "checkedAt": "2026-09-05T08:00:00.000Z",
  "llmSummary": "ACME ROOFING LLC (WA CONSTRUCTION CONTRACTOR, #ACMERRL123AB) expires 2026-10-28 — in 53 days."
}
```

A `summary` record per run gives counts per state and change type.

### Sources

| State | Registry (open data, refreshed daily) | Status field | Expiry field |
|---|---|---|---|
| WA | Department of Labor & Industries — Contractor License Data | yes | yes |
| OR | Construction Contractors Board — Active Licenses (removal = no longer active) | active-only | yes |
| TX | TDLR — All Licenses (contractor & electrician types) | no | yes |
| NY | Department of Labor — Contractor Registry Certificates | yes | yes |

No scraping, no login, no proxies.

### Input

```json
{
  "states": ["WA", "OR"],
  "licenseType": "ROOF",
  "zip": "98",
  "expiringWithinDays": 60,
  "emitExpiringOnBaseline": true
}
```

| Field | Default | Meaning |
|---|---|---|
| `states` | WA | registries to watch |
| `licenseType` / `city` / `county` / `zip` | — | watch scope (strongly recommended; a whole state can be 160,000 licenses) |
| `expiringWithinDays` | 60 | emit `expiring-soon` when a license enters this window |
| `emitExpiringOnBaseline` | true | on the first run, return all licenses already in the window |
| `maxRecordsPerState` | 20000 | snapshot cap per state |
| `stateStoreName` | `license-change-state` | where snapshots live; one per watchlist |
| `socrataAppToken` | — | optional, lifts rate limits for large scopes |

Schedule it daily or weekly with the same input.

### Monitor mode, watchlists and the free first run

All handa-labs monitors share the same controls:

| Field | Default | Meaning |
|---|---|---|
| `mode` | `monitor` | `monitor` remembers what was returned and emits only changes on later runs; `snapshot` returns everything matching on every run |
| `watchlistId` | `default` | label for this watchlist; runs with the same ID share memory, different IDs are independent |
| `emitBaseline` | `false` | the first monitor run establishes a **free baseline** (only the Actor-start fee) and returns counts; set `true` to also receive, and pay for, every current item on that first run |

Schedule the Actor with the same input and you get a clean change feed. Nothing is charged for records on the first run unless you ask for them.

### Pricing

Pay-per-event: a small fee per run plus a fee per change record. A focused scope (one trade, one state) typically produces a few dozen changes per week.

### Notes

- `licenseType` matches the registry's own type / specialty / endorsement fields. Trades like ROOF, ELECTRICAL, PLUMBING, HVAC work in WA, TX and NY; Oregon licenses are endorsement *levels* (e.g. "Residential General Contractor", "Commercial General Contractor Level 2"), so use those words, or leave `licenseType` empty and scope Oregon by `county` / `zip` instead.
- Texas publishes no status, so only `new`, `removed`, `renewed` and `expiring-soon` apply there; it also publishes no address or phone.
- Oregon's dataset lists active licenses only, so a license leaving it is reported as `removed`.
- Snapshots are stored compressed in your key-value store; changing the scope starts a new baseline.
- Need the full current list instead of changes? Use the companion **US Contractor License Lookup & Lead Lists**.
- Public records for business use; not for FCRA-regulated decisions.

***

### 日本語

米国4州(WA/OR/TX/NY)の建設業者ライセンス登録簿を監視し、前回からの変化だけを返します: 新規取得、失効(登録簿から消えた)、状態変更(有効→停止など)、更新、そして「N日以内に期限切れ」になった業者。保険・ボンド・建材・SaaSの営業と、元請の下請け審査向け。初回は圧縮スナップショットを保存し、期限が近い業者の一覧も返します。州のオープンデータのみ使用、スクレイピングなし。

# Actor input Schema

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

Which state registries to watch. Each state keeps its own snapshot.

## `licenseType` (type: `string`):

Narrow the watch scope, e.g. ROOF, ELECTRICAL, PLUMBING, HVAC, GENERAL. Strongly recommended — a whole state is up to 160,000 licenses.

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

Registry city name, e.g. SEATTLE.

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

County (Oregon, Texas).

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

"monitor" (default): remember what was returned and emit only changes on later runs. "snapshot": return everything matching, every run, no memory.

## `watchlistId` (type: `string`):

Label for this watchlist (e.g. "roofing-wa"). Runs with the same ID share memory; use different IDs for independent watchlists.

## `emitBaseline` (type: `boolean`):

The first monitor run establishes a free baseline and returns only counts. Turn this on to also receive (and pay for) every current item on that first run.

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

ZIP prefix, e.g. 981.

## `expiringWithinDays` (type: `integer`):

Emit an `expiring-soon` change when a license enters this window before its expiration date. 0 disables.

## `emitExpiringOnBaseline` (type: `boolean`):

Legacy alias of "emitBaseline".

## `maxRecordsPerState` (type: `integer`):

Safety cap on the snapshot size per state.

## `stateStoreName` (type: `string`):

Named key-value store that holds the compressed snapshots. Use a different name per watchlist.

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

Free token from any Socrata portal to lift anonymous rate limits for large scopes.

## Actor input object example

```json
{
  "states": [
    "WA"
  ],
  "licenseType": "ROOFING",
  "mode": "snapshot",
  "watchlistId": "default",
  "emitBaseline": false,
  "expiringWithinDays": 60,
  "emitExpiringOnBaseline": false,
  "maxRecordsPerState": 20000,
  "stateStoreName": "license-change-state"
}
```

# Actor output Schema

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

No description

# 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 = {
    "licenseType": "ROOFING",
    "mode": "snapshot"
};

// Run the Actor and wait for it to finish
const run = await client.actor("handa-labs/contractor-license-change-monitor").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 = {
    "licenseType": "ROOFING",
    "mode": "snapshot",
}

# Run the Actor and wait for it to finish
run = client.actor("handa-labs/contractor-license-change-monitor").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 '{
  "licenseType": "ROOFING",
  "mode": "snapshot"
}' |
apify call handa-labs/contractor-license-change-monitor --silent --output-dataset

```

## MCP server setup

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

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/FAD1tOQAuVecTZeaF/builds/kTuIkeVTBFJS75WRr/openapi.json
