# Contractor License Scraper - Texas TDLR, Washington, Oregon (`neverempty/contractor-license-scraper`) Actor

For contractor lead lists, insurance and supplier sales teams: licensed trade contractors in Texas, Washington and Oregon in one schema. Texas alone is 996,129 official rows, Washington 45,508 licence numbers, all read live on every run. Filter by city, trade or expiring soon. No API key.

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

## Pricing

from $7.30 / 1,000 license returneds

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?

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 Scraper - Texas TDLR, Washington, Oregon

**For license verification before hiring a contractor, lead lists of licensed trades in an area, and renewal or compliance tracking**: contractor and trade licenses from three official state sources at once — Texas TDLR, Washington L\&I and Oregon CCB — returned in one schema, one row per license, every row linking back to the state's own dataset.

**Measured against the live sources on 2026-09-11.** Texas alone is a **187 MB file with 996,129 rows** and no status column, read whole on every run (about **10 seconds** on Apify) so a result is never a stale copy. Oregon's **56,171** rows cover **45,508** license numbers — one row per endorsement, not per license.

**Three ways a naive reader gets this wrong, all fixed here.** Texas lists every Class A Barber twice: **30,881** rows carry the type and the same **30,881** licenses appear again with a blank type — you get one row per license. Oregon stores dates as text, so "expires before 10/15/2026" compared as text matched **46,036 of 56,171** rows; expiration searches here compare real dates. Washington spells cities in mixed case, so an exact match on "Seattle" finds **616** contractors while the upper-case form finds **8,173** — city, county and name searches ignore case.

**"Active" is checked against the expiration date, not just the flag.** Washington has **34** contractors marked ACTIVE whose registration is already past its expiration date, and Oregon's "active licenses" file contains licenses that expired earlier in 2026. Every row carries `isExpired` and `daysUntilExpiration`.

The three sources, in full:

- **Texas** - the Texas Department of Licensing and Regulation (**TDLR**) license file: electrical contractors, master and journeyman electricians, A/C contractors and technicians, elevator contractors, appliance installers, water well drillers, cosmetology and barber licenses and establishments, tow truck operators and the rest of TDLR's 80+ license types.
- **Washington** - the Department of Labor & Industries (**L\&I**) contractor registration: construction, electrical, plumbing and elevator contractors with their specialties, UBI number, status, suspension date, current surety bonds and current liability insurance.
- **Oregon** - the Construction Contractors Board (**CCB**) active licenses: residential and commercial general and specialty contractors, lead-based paint renovation contractors, home inspectors and locksmiths, with bond and insurance.

Filter by name, license number, city, county, ZIP, license type, active status or "expiring within N days" (up to **10,000** licenses per state). No API key, no browser, no proxy. Export as JSON, CSV or Excel.

### What the sources get wrong, and what this Actor does about it

All numbers below were measured against the live sources on 2026-09-11 between 00:30 and 01:10 JST (2026-09-10, 15:30-16:10 UTC). The state files are updated daily, so the same counts taken later differ slightly.

| The source's behaviour | What you get here |
|---|---|
| **Texas lists every Class A Barber twice.** The TDLR file has 30,881 Class A Barber rows, and the same 30,881 licenses appear again as rows with a blank license type (same number, subtype, name and expiration date) | **One row per license.** The blank-type copy is dropped when the same license is on a typed row, so a license is never returned or charged twice. Blank-type rows with no typed twin (18,509 rows have neither a type nor a subtype, and barber shops are listed only this way) are returned with `licenseType: null` rather than a guessed type |
| **The Texas file is 187 MB (996,129 rows)** and has no status column | The whole file is read on every run (about 10 seconds on Apify), so a result is never a stale copy. "Active" in Texas means "not past the expiration date", and `licenseStatus` stays `null` instead of an invented value |
| **Oregon lists one row per endorsement.** 56,171 rows cover 45,508 license numbers; license 100283 is two rows (Commercial Specialty Level 2 and Residential Specialty) with two different bond amounts | One row per license with every endorsement in `licenseTypes` and every bond in `bonds`. A license-type filter keeps the whole license, not only the matching endorsement |
| **Oregon endorsements run on their own terms.** 4,251 Oregon licenses have endorsement rows with different expiration dates - license 3550's general contractor endorsements expire 2027-10-08, its lead-based paint renovation endorsement 2026-10-08 | The license's `expirationDate` is the contractor endorsements' date (they never disagree); every endorsement's own dates are in `endorsements`, so a license is not reported as expiring because only its lead-paint certification does |
| **Oregon home inspector and locksmith certificates reuse contractor license numbers** (for example 1022 is both a contractor and a home inspector) | They are kept apart, so looking up a number never merges two different licensees |
| **Oregon stores dates as text** ("02/06/2028"). Compared as text, "expires before 10/15/2026" matched 46,036 of 56,171 rows | Expiration searches compare real dates |
| **Washington's bonds and insurance live in separate files that keep every past policy** | Only current ones are attached: not cancelled, and not past their expiration date ("Until Canceled" bonds count as current). A bond L\&I has marked impaired carries `impaired: true` and its `impairedDate`; Oregon does not publish this, so both are `null` there |
| **Washington spells cities in mixed case** ("SPOKANE", "Spokane"): an exact match on "Seattle" finds 616 contractors, the upper-case form 8,173 | City, county and name searches ignore case |
| **Washington has 34 contractors marked ACTIVE whose registration is past its expiration date**, and Oregon's "active licenses" file contains licenses that expired earlier in 2026 | "Only active" also checks the expiration date. `isExpired` and `daysUntilExpiration` are on every row, computed from the published date |
| A search with no matches, a rejected search and a source that could not be reached look the same in most tools | Each gets its own row with the reason (`no-licenses`, `bad-input`, `failed`, `incomplete`, `state-skipped`). None of them is charged |

### Personal information

License holder names are returned because they are what each state's public license lookup shows. Contact details follow one rule: **a street address, ZIP code and phone number are returned only for business licenses.** For a license held by an individual, only the city (where published), county and state are returned.

- **Texas**: business license types (contractors, establishments, schools, education providers and similar) come with the business address and business phone, including sole proprietors registered under their own name. Two kinds of Texas rows get only the city and county: business licenses whose licensee name TDLR writes in its personal "LAST, FIRST" form, and licenses held by individuals (electricians, technicians, barbers, cosmetologists, well drillers, accessibility specialists...), even where the file has an address or phone for them. For those rows the city comes from the mailing address when TDLR lists one; the street, ZIP and phone are never returned. Texas mailing addresses and mailing phone numbers are never returned for any row.
- **Washington**: every registration is a contractor business registration, including sole proprietors (`businessType: "Individual"`); the registered business address and phone are returned as L\&I publishes them.
- **Oregon**: contractor licenses are business licenses and come with their address and phone. Home inspector (OCHI) and locksmith (OCLS) certificates belong to individuals: no address, ZIP or phone.

No email addresses are returned from any state.

### What each state publishes

| Field | Texas (TDLR) | Washington (L\&I) | Oregon (CCB) |
|---|---|---|---|
| License status | not published (use `isExpired`) | yes (ACTIVE, EXPIRED, SUSPENDED, ...) | the file lists active licenses only |
| Expiration date | yes | yes | yes |
| Issue / registration date | not published | `effectiveDate` | `originalRegistrationDate` |
| Suspension | not published | `suspendDate` | not published |
| Bonds and insurance | not published (`null`) | yes, current only | yes |
| County | yes | not published | yes |
| Principal / responsible person | not published | primary principal | responsible managing individual |

### Input

| Field | Default | What it does |
|---|---|---|
| `states` | all three | Any of `tx`, `wa`, `or` |
| `name` | - | Licensee, business or principal name contains this text |
| `licenseNumber` | - | Exact license number. Texas numbers repeat across license types, so a Texas number can return several licenses |
| `city` | - | Exact city, any case. Texas business licenses use the business city; Texas individual licenses have a city only when TDLR lists a mailing address, and the street is never returned |
| `county` | - | Exact county, any case. Washington publishes no county, so it is skipped (with a row saying so) |
| `zip` | - | Business ZIP code starts with these 3 to 5 digits |
| `licenseType` | - | Type, endorsement, specialty or subtype contains this text (electrical, plumbing, roofing, A/C, residential, elevator...) |
| `onlyActive` | `true` | Only licenses not past their expiration date (and ACTIVE in Washington). Turn off to look up any license, including expired and suspended ones |
| `expiringWithinDays` | - | Only licenses expiring between today and N days from today (0-365), soonest first |
| `maxResultsPerState` | `100` | Up to 10,000 licenses per state |
| `includeBondsAndInsurance` | `true` | Attach Washington's current bonds and insurance (one extra request per 100 licenses) |

If no name, number, city, county, ZIP, type or expiry window is given, `licenseType` = `contractor` is used, and every row says so in its `query` column.

Dates such as "today" are computed in UTC.

Runs use 1024 MB of memory by default. Only the requested number of licenses is kept while the 187 MB Texas file is read, so memory does not grow with the number of matching rows. Measured on Apify on 2026-09-11 (10:45-10:52 JST) with 1024 MB: the default input finished in 14 seconds using 62 MB; 10,000 licenses per state from all three states, every status, expiring within 365 days (540,431 matching Texas rows) finished in 105 seconds using 266 MB; the same for Texas alone in 59 seconds using 116 MB. An earlier version that kept every matching row ran out of memory at 512 MB on that input; lowering the memory mainly makes Texas slower, because Apify gives smaller runs less CPU.

Examples:

```json
{ "states": ["wa", "or"], "city": "Spokane", "licenseType": "roofing" }
```

```json
{ "states": ["tx"], "licenseType": "electrical contractor", "city": "Houston", "expiringWithinDays": 90 }
```

```json
{ "states": ["or"], "county": "Multnomah", "expiringWithinDays": 30, "maxResultsPerState": 500 }
```

### Output

Real rows from 2026-09-11 (trimmed):

```json
{
  "state": "WA", "licenseNumber": "ALLTETR835BL", "licenseType": "CONSTRUCTION CONTRACTOR",
  "specialties": ["ROOFING"], "licenseStatus": "ACTIVE",
  "expirationDate": "2027-01-13", "daysUntilExpiration": 125, "isExpired": false, "effectiveDate": "2017-01-13",
  "businessName": "ALL TERRAIN ROOFING LLC", "principalName": "BROWN, ANTHONY MICHAEL",
  "licenseeKind": "business", "businessType": "Limited Liability Company", "ubi": "604068244",
  "address": "7517 N FIVE MILE RD", "city": "SPOKANE", "zip": "99208", "phone": "5094994643",
  "bonds": [{ "company": "Ohio Cas Ins Co", "amountUsd": 30000, "effectiveDate": "2024-07-01", "expirationDate": null, "untilCanceled": true, "forLicenseType": "CONSTRUCTION CONTRACTOR" }],
  "insurance": [{ "company": "United National Ins Co", "amountUsd": 1000000, "effectiveDate": "2026-01-07", "expirationDate": "2027-01-07" }],
  "sourceDataset": "https://data.wa.gov/d/m8qx-ubtq"
}
```

```json
{
  "state": "TX", "licenseNumber": "17000", "licenseType": "Electrical Contractor", "licenseSubtype": "EC",
  "licenseStatus": null, "expirationDate": "2027-07-31", "isExpired": false,
  "licenseeName": "TORRES ELECTRICAL SERVICE", "businessName": "TORRES ELECTRICAL SERVICE", "licenseeKind": "business",
  "address": "11910 DOVER STREET", "city": "HOUSTON", "county": "HARRIS", "zip": "77031", "phone": "7135622862",
  "bonds": null, "insurance": null, "bondsAndInsurancePublished": false,
  "sourceDataset": "https://www.tdlr.texas.gov/LicenseSearch/licfile.asp", "sourceUpdatedAt": "2026-09-10T11:19:27.000Z"
}
```

Text is returned as each state published it. A value a state does not publish is `null`, never a guess; `bonds: null` means "not published or could not be read", `bonds: []` means "published, and there are none".

Rows whose `status` is not `ok` explain themselves in `note` and are never charged.

### Pricing

Pay per license returned. An Oregon license spread over several endorsement rows is one license and one charge. Rows that only explain an empty, rejected, skipped or failed search are free.

### Sources

Texas Department of Licensing and Regulation license file (updated daily), Washington State Department of Labor & Industries contractor license data on data.wa.gov (general, bond and insurance files), Oregon Construction Contractors Board active licenses on data.oregon.gov. This Actor is not affiliated with any of these agencies. A license lookup here does not replace the state's own verification before you sign a contract.

### Support

Found a problem or need another state? Open an issue in the Issues tab.

# Actor input Schema

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

Which state license files to search. Texas is the TDLR file (electricians, A/C, elevators, cosmetology, barbers, tow operators and more); Washington is L\&I contractor registration (construction, electrical, plumbing, elevator); Oregon is the Construction Contractors Board. None chosen means all three.

## `name` (type: `string`):

Keep licenses whose licensee, business or principal name contains this text. Case does not matter.

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

Exact license number, for example 17000 (Texas), ECOSTSC758NN (Washington) or 100215 (Oregon). Texas numbers repeat across license types, so a number alone can return several Texas licenses.

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

Exact city name, such as Houston, Spokane or Portland. Case does not matter. Texas business licenses use the business city; Texas individual licenses have a city only when TDLR lists a mailing address.

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

Exact county name, such as Harris or Multnomah. Washington does not publish a county, so Washington is left out (with a row saying so) when this is set.

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

Keep licenses whose business ZIP code starts with these 3 to 5 digits. Individual licensees carry no ZIP code here.

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

Keep licenses whose type, endorsement, specialty or subtype contains this text, such as electrical, plumbing, roofing, A/C, residential or elevator. If no name, number, city, county, ZIP, type or expiry window is given, CONTRACTOR is used and every row says so in its query column.

## `onlyActive` (type: `boolean`):

Keep only licenses that are not past their expiration date (and, in Washington, whose status is ACTIVE). Turn off to include expired, suspended and closed licenses - for example when checking one license by number.

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

Keep licenses whose expiration date is between today and N days from today (0-365), soonest first. Leave empty to ignore expiration.

## `maxResultsPerState` (type: `integer`):

The most licenses returned for each state. A license that the source lists on several records (one per Oregon endorsement) counts once.

## `includeBondsAndInsurance` (type: `boolean`):

Look up each Washington contractor's current surety bonds and liability insurance in L\&I's bond and insurance files (one extra request per 100 licenses). Oregon's bond and insurance come with the license; Texas does not publish them.

## Actor input object example

```json
{
  "states": [
    "tx",
    "wa",
    "or"
  ],
  "licenseType": "contractor",
  "onlyActive": true,
  "expiringWithinDays": 60,
  "maxResultsPerState": 100,
  "includeBondsAndInsurance": true
}
```

# Actor output Schema

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

One row per license, plus one row when a state's data rejected the search, matched nothing, was skipped, or could not be read.

# 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 = {
    "states": [
        "tx",
        "wa",
        "or"
    ],
    "licenseType": "contractor",
    "expiringWithinDays": 60
};

// Run the Actor and wait for it to finish
const run = await client.actor("neverempty/contractor-license-scraper").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 = {
    "states": [
        "tx",
        "wa",
        "or",
    ],
    "licenseType": "contractor",
    "expiringWithinDays": 60,
}

# Run the Actor and wait for it to finish
run = client.actor("neverempty/contractor-license-scraper").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 '{
  "states": [
    "tx",
    "wa",
    "or"
  ],
  "licenseType": "contractor",
  "expiringWithinDays": 60
}' |
apify call neverempty/contractor-license-scraper --silent --output-dataset

```

## MCP server setup

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

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/Nt6l0TB3KcC69HOBW/builds/fo0WJf6A7cfeEuzkl/openapi.json
