# WA Contractor License Scraper (L\&I Status Lookup) (`steadfetch/wa-contractor-license-status`) Actor

Washington State L\&I contractor license database: JSON records with number, business name (registered entities), type, class, specialty, status, dates, city, UBI. Filter by number or status. Lookup, bulk export or incremental runs. For contractor vetting and compliance. No personal contact fields.

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

## Pricing

from $1.60 / 1,000 licence delivereds

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

## WA Contractor License Scraper (L\&I Status Lookup)

Tested daily. Last verified: 2026-10-30

Look up contractor, liquor and professional licence records from official US state open-data sources. Built by [Steadfetch](https://apify.com/steadfetch): web data that does not break.

### What it does

The Actor reads three keyless public Socrata datasets and returns one JSON item per row in a **unified multi-state schema**:

- **Washington contractor licences** - the L\&I Contractor License Data - General dataset (`data.wa.gov`, resource `m8qx-ubtq`), unchanged since v1.
- **New York liquor licences** - the Current Liquor Authority Active Licenses dataset (`data.ny.gov`, resource `9s3h-dpkz`, about 60,000 active licences, refreshed daily).
- **Washington professional licence transactions** - the DOL Professional License Transactions dataset (`data.wa.gov`, resource `ixni-jq78`), aggregate counts of original, renewal and reinstatement transactions by programme, licence type, county and date.

Every item carries `state` (source jurisdiction: `wa` or `ny`) and `family` (`contractor`, `liquor` or `professional`), so results from all sources can be mixed, filtered and joined downstream.

- **Filters:** state and family selection, text search on holder or licence-type names, date range, and the v1 WA filters (exact licence number, status).
- **Incremental delta runs:** the default mode delivers only new or changed rows for the same filter scope. Changed rows keep their original `firstSeen` and get `lastChanged`, so you can detect licence updates daily.
- **Full bulk export:** `fetchAll: true` re-reads the selected datasets.
- **Normalized output:** dates become `YYYY-MM-DD`, missing values become `null`, and the WA specialty is a nested `{ code, name }` object.
- **Schema validation:** every item is validated against `.actor/dataset_schema.json` before it reaches the dataset. An invalid item fails the run.
- **No charge for noise:** invalid rows, duplicates, personal-data-excluded rows and rows already delivered unchanged by a previous incremental run are never pushed, so they are never billed.

#### Privacy: business records only

The sources contain personal columns. This Actor **never selects them from the APIs**, so they are never downloaded, stored or published.

- WA contractor: `businessName` is published **only** for registered entity types (LLC, corporation, partnership, LLP, joint venture, association, trust); for individual licences it is `null`. The principal name, phone, street address and ZIP columns are never requested.
- NY liquor: the dataset has no entity-type flag, so the Actor publishes a row **only when the licence holder is confirmed as an organization** by a corporate marker in the legal name. Sole-proprietor rows (about 3% of the dataset, including chains without a legal suffix) are excluded entirely instead of risking a natural person's data. ZIP and georeference columns are never selected.
- WA professional: the source is aggregate counts only - no licence holder exists in it.
- The output schema is closed (`additionalProperties: false`): a row with any extra field, including a personal one, is rejected before it can be pushed.

#### Source, licence and access

- **WA contractor:** [L\&I Contractor License Data - General](https://data.wa.gov/d/m8qx-ubtq), Open Data Commons PDDL (public domain).
- **NY liquor:** [Current Liquor Authority Active Licenses](https://data.ny.gov/d/9s3h-dpkz), published by the New York State Liquor Authority on the official state open-data portal (Open NY program).
- **WA professional:** [Professional License Transactions](https://data.wa.gov/d/ixni-jq78), Open Data Commons ODbL. The Actor names the Washington State Department of Licensing as the source and ships the source's own records, so the license terms are honoured with attribution and licensing notice.
- **robots.txt:** read on 2026-10-25 for both portals. `User-agent: *` declares `Crawl-delay: 1` on both hosts; filter URLs under `/browse` and `/catalog` are disallowed. The Actor only calls the exact `/resource/<id>.json` endpoints, waits at least 1000 ms between requests and revalidates every redirect destination against the same allowlist. Full details in the repository's `docs/legal-gate.md`.
- **Not included:** the NMLS registry (`nmlsconsumeraccess.org`) blocks automated access with 403 + CAPTCHA and is excluded on purpose.

### Input

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `states` | array | all | Source states to query: `ny`, `wa`. Empty means all sources. |
| `families` | array | all | Families to query: `contractor`, `liquor`, `professional`. Empty means all. |
| `licenseNumber` | string | - | Optional exact WA contractor licence number. When set (or when `status` is set), only the WA contractor source is queried, as in v1. |
| `status` | enum | - | Optional WA contractor status: `ACTIVE`, `INACTIVE`, `EXPIRED`, `SUSPENDED`. Other source statuses are returned when no filter is set. |
| `q` | string | - | Optional case-insensitive text filter on holder or licence-type names (never on personal columns). |
| `dateFrom` / `dateTo` | date | - | Optional range on the source's effective/transaction date (`dateTo` is exclusive). |
| `maxResults` | integer 1-5000 | - | Total row cap across all selected sources, split evenly between them (the first sources take the remainder). Overrides `maxRows` when set. |
| `maxRows` | integer 1-5000 | `100` | Legacy v1 total row cap (default 100, split evenly across sources). Ignored when `maxResults` is set. |
| `fetchAll` | boolean | `false` | `true` fetches the full selected datasets, ignoring row caps and the delta state. |
| `strictSchema` | boolean | `true` | Fail the run when an item fails schema validation. Keep enabled. |

#### Minimum input

Every field is optional. This is the smallest real input that runs:

```json
{}
```

With `{}` the Actor returns up to 100 rows split evenly across the three sources and bills about 0.20 USD. Raise `maxResults` for larger extracts.

A single-licence WA lookup (v1 compatible):

```json
{
    "licenseNumber": "WHIDBIT745PO"
}
```

New York liquor licences only:

```json
{
    "states": ["ny"],
    "families": ["liquor"],
    "maxResults": 1000
}
```

### Example output

A New York liquor licence:

```json
{
    "url": "https://data.ny.gov/resource/9s3h-dpkz.json?licensepermitid=0032-23-139868",
    "title": "OSPREYS DOMINION VINEYARDS LTD",
    "scrapedAt": "2026-10-25T02:00:00.000Z",
    "state": "ny",
    "family": "liquor",
    "licenseNumber": "0032-23-139868",
    "businessName": "OSPREYS DOMINION VINEYARDS LTD",
    "businessType": null,
    "licenseType": "Farm winery",
    "licenseClass": "0032",
    "specialty": null,
    "status": "active",
    "issuedAt": "2023-05-01",
    "firstSeen": "2026-10-25T02:00:00.000Z",
    "lastChanged": null,
    "premisesAddress": "44075 MAIN RD, PECONIC",
    "county": "Suffolk",
    "businessState": null,
    "effectiveDate": "2023-05-01",
    "expirationDate": "2026-04-30",
    "city": "PECONIC",
    "ubi": null,
    "programType": null,
    "transactionType": null,
    "transactionCount": null,
    "transactionDate": null,
    "sourceUrl": "https://data.ny.gov/d/9s3h-dpkz"
}
```

A WA contractor licence keeps the v1 fields (`licenseNumber`, `businessName`, `specialty`, `effectiveDate`, `ubi`, ...) and adds `state: "wa"`, `family: "contractor"` and the delta fields. A WA professional aggregate row has `licenseNumber: null`, `status: null` and carries `programType`, `transactionType`, `transactionCount` and `transactionDate`.

**Schema note (v0.2):** the `state` field now holds the source jurisdiction (`wa`/`ny`). The WA licence holder's address state, previously published in `state`, moved to `businessState` so the unified schema stays closed and explicit.

### Output fields

| Field | Type | Source |
| --- | --- | --- |
| `url` | URI | public Socrata API URL for this record (envelope) |
| `title` | string or null | business name for entities; licence number; record label for aggregates |
| `scrapedAt` | date-time | run timestamp (envelope) |
| `state` | string | source jurisdiction: `wa` or `ny` |
| `family` | string | `contractor`, `liquor` or `professional` |
| `licenseNumber` | string or null | licence number; null for aggregate rows |
| `businessName` | string or null | holder name, only for confirmed organizations |
| `businessType` | string or null | WA contractor `businesstypecodedesc` |
| `licenseType` | string or null | licence type or class description |
| `licenseClass` | string or null | class code |
| `specialty` | object or null | WA contractor `specialtycode1` + `specialtycode1desc` as `{ code, name }` |
| `status` | string or null | licence status; null for aggregates |
| `issuedAt` | date or null | original issue date (NY) |
| `firstSeen` | date-time | first run that delivered the row |
| `lastChanged` | date-time or null | run in which the row content changed |
| `premisesAddress` | string or null | licensed premises, address and city (NY) |
| `county` | string or null | premises county (NY) or aggregate county (WA professional) |
| `businessState` | string or null | WA holder address state (v1 `state` semantics); licensees' state for WA professional aggregates |
| `effectiveDate` / `expirationDate` | date or null | licence dates |
| `city` | string or null | holder or premises city |
| `ubi` | string or null | WA Unified Business Identifier |
| `programType` / `transactionType` / `transactionCount` / `transactionDate` | see dataset schema | WA professional aggregate dimensions and count |

### Incremental runs and delta detection

- **`fetchAll: false` (default):** each source persists what it has delivered for the current filter scope. A later run delivers only rows that are **new** or whose content **changed** (row content hash). New rows get `firstSeen = this run`, `lastChanged = null`; changed rows keep the original `firstSeen` and get `lastChanged = this run`. Unchanged rows are skipped and billed nothing.
- **`fetchAll: true`:** a full export. It ignores the persisted state and returns the complete current selection with `firstSeen = this run` and `lastChanged = null`.

The scope is the pair of filters you use (v1: `licenseNumber` + `status`; multi-state: `states` + `families` + `q` + dates). Change any filter to start a separate incremental series.

**Limitation, stated honestly:** for the WA contractor source the dedupe key is still licence number + effective date, as in v1: a status-only change of an existing licence is not re-emitted incrementally - use `fetchAll: true` to re-read it. The NY and WA professional sources use a full row-content hash, so any change in the source row is detected.

### Pricing

Pay-per-event, with the two synthetic platform events only:

| Event | When it fires | Price |
| --- | --- | --- |
| `apify-actor-start` | Once per run start (includes the first 5 s of compute for free) | 0.00005 USD |
| `apify-default-dataset-item` | One row saved to the dataset: a WA contractor licence, a NY liquor licence or a WA professional transaction record | 0.002 USD = **2.00 USD per 1,000 results** |

One row = one charged result, never a page or a retry. The price is unchanged from v0.1.

**Works on the Apify free plan** - about 2,499 rows for the $5 monthly credit ((5.00 USD - 0.00005 USD run-start) / 0.002 USD per row).

### Cost per 1,000 results

Measured locally on 2026-10-22 over 5,000 real WA contractor rows: extraction plus schema validation took **151 ms (30.3 ms per 1,000 rows)** with zero schema rejections. The two new sources use the same JSON + validation pipeline, so the per-row cost stays in the same range (see `docs/measurements.md`).

The real cost driver is the robots-mandated 1 s delay between page requests: a full 161,000-row WA scan makes about 33 requests, so roughly 33 s of wall time. Even with a deliberately conservative allowance for compute, dataset writes and data transfer, the total stays **below 0.001 USD per 1,000 results** - a gross margin above 99% at the 2.00 USD price. The first paid run will confirm the billed figure.

### FAQ

**Do I need an API key?** No. All three Socrata resources are public and keyless.

**Does it scrape the website?** No. It calls the official JSON APIs, which is faster and cheaper than rendering the web front ends.

**Can I get personal contact data?** No. The Actor never requests principal name, phone, street address or ZIP columns for WA contractor licences; it excludes every NY row whose holder is not a confirmed organization; and the WA professional source is aggregate counts only. The goal is business licence data, not people.

**Which fields exist per family?** WA contractor rows fill the v1 fields (licence number, business name, specialty, UBI, dates). NY liquor rows fill `licenseNumber`, `businessName`, `premisesAddress`, `county`, `licenseType` and the licence dates. WA professional rows carry `programType`, `transactionType`, `transactionCount` and `transactionDate`, with `licenseNumber`, `businessName` and `status` null. All rows always carry `state`, `family`, `firstSeen` and `lastChanged`.

**How do I monitor new licences every day?** Run incrementally (default) with your filters. You pay only for rows that are new or changed; unchanged days cost nothing.

**Why did my NY run return fewer rows than the source has?** The Actor excludes rows whose holder cannot be confirmed as an organization (sole proprietors) to keep natural persons' data out of the output. That is about 3% of the NY dataset.

**How do I re-extract everything?** Run with `fetchAll: true`. This is a paid bulk export of the selected datasets.

**Why did my second incremental run return zero rows?** Because the scope was already fully delivered and nothing changed. That is the intended behaviour and it costs nothing.

### Maintenance

**tested daily.** Steadfetch runs a catalog test every day against the live APIs, validates the output against the published schema and turns failures into issues in the repository. The sources are stable government Socrata datasets, and the Actor has no browser, proxy or anti-bot dependency.

# Actor input Schema

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

Source states to query. Empty means all sources. Contractor licence number or status filters always restrict the run to the Washington contractor source (v1 behaviour).

## `families` (type: `array`):

Licensing families to query. Empty means all families.

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

Optional exact Washington contractor licence number. When set (or when a status filter is set), only that WA contractor source is queried.

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

Optional Washington contractor licence status filter, as published by L\&I.

## `q` (type: `string`):

Optional case-insensitive text filter on holder or licence-type names (never on personal contact columns).

## `dateFrom` (type: `string`):

Optional inclusive lower bound (YYYY-MM-DD) on the source's effective/transaction date.

## `dateTo` (type: `string`):

Optional exclusive upper bound (YYYY-MM-DD) on the source's effective/transaction date.

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

Maximum number of validated rows pushed to the dataset across all selected sources. When set it overrides maxRows.

## `maxRows` (type: `integer`):

Legacy v1 total row cap. Ignored when maxResults is set.

## `fetchAll` (type: `boolean`):

Fetch the full selected datasets, ignoring row caps, and skip the incremental delta state. With false (default) the run is incremental: only new or changed rows are delivered (changed rows keep their original firstSeen and get lastChanged).

## `strictSchema` (type: `boolean`):

Fail the run when an item does not match the output schema. Keep enabled: Steadfetch never publishes Actors without output-schema validation.

## Actor input object example

```json
{
  "states": [
    "ny",
    "wa"
  ],
  "families": [
    "contractor",
    "liquor",
    "professional"
  ],
  "licenseNumber": "WHIDBIT745PO",
  "status": "ACTIVE",
  "q": "vineyards",
  "dateFrom": "2026-01-01",
  "dateTo": "2026-02-01",
  "maxResults": 1000,
  "maxRows": 100,
  "fetchAll": true,
  "strictSchema": true
}
```

# Actor output Schema

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

Dataset items validated against the item schema in dataset_schema.json (fields).

# 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 = {
    "licenseNumber": "WHIDBIT745PO",
    "maxResults": 50,
    "maxRows": 10,
    "fetchAll": true,
    "strictSchema": true
};

// Run the Actor and wait for it to finish
const run = await client.actor("steadfetch/wa-contractor-license-status").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 = {
    "licenseNumber": "WHIDBIT745PO",
    "maxResults": 50,
    "maxRows": 10,
    "fetchAll": True,
    "strictSchema": True,
}

# Run the Actor and wait for it to finish
run = client.actor("steadfetch/wa-contractor-license-status").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 '{
  "licenseNumber": "WHIDBIT745PO",
  "maxResults": 50,
  "maxRows": 10,
  "fetchAll": true,
  "strictSchema": true
}' |
apify call steadfetch/wa-contractor-license-status --silent --output-dataset

```

## MCP server setup

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

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/JfgATZ66O7dNDNM1H/builds/DL4UiWGMt68b4aiyW/openapi.json
