# Us State Contractor License Scraper (`arezdev/us-state-contractor-license-scraper`) Actor

Look up official California CSLB, Florida DBPR and Texas TDLR records. Extract license status, expiration dates, public contacts and renewal flags, plus available California bond and workers' compensation details. Export data for verification and prospecting.

- **URL**: https://apify.com/arezdev/us-state-contractor-license-scraper.md
- **Developed by:** [Arezdev](https://apify.com/arezdev) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.50 / 1,000 contractor license extracteds

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

**Find licensed contractors and renewal opportunities using official California, Florida, and Texas licensing records.**

Extract contractor identities, classifications, published contact details, license dates, and 60-day renewal flags into a flat Apify dataset. California records also include published contractor bond and Workers' Compensation details. Download JSON, CSV, Excel, or connect the dataset to your workflow.

Best for license verification workflows and renewal lead research. California requires exact license numbers. Texas discovery covers supported HVAC and electrical datasets; Florida coverage follows supported DBPR searches. Verify trade and location coverage before scaling.

### Ready-to-run example

The Input form includes public example values. A fresh `apify run` uses the input schema defaults. To explicitly select the small sample, including when input was saved by an earlier local run, use:

```sh
apify run --input-file .actor/input.example.json
```

You can also paste [.actor/input.example.json](.actor/input.example.json) into the JSON input editor in Apify Console. Replace the example sources with your own targets and increase the limits when you are ready.

### How to use

1. Choose Texas, California, or Florida.
2. Enter license numbers for exact lookup, or select a trade and city for Texas or Florida discovery.
3. Set the maximum number of saved records and optional expiration window.
4. Run the Actor and open the dataset. Filter `status = SUCCESS` before using the records downstream.

The form starts with a small Texas HVAC sample, an empty city, and 3 records. Its request uses the public Socrata API directly. The schema default is also `city: ""` for statewide extraction, with `maxItems: 50` for API or CLI inputs that omit the item limit. Source availability and network latency can change; successful extraction within five seconds cannot be guaranteed.

Quick Texas sample:

```json
{
  "state": "TX",
  "tradeCategory": "HVAC_ACR",
  "city": "",
  "onlyActive": true,
  "maxItems": 3
}
```

Florida HVAC discovery:

```json
{
  "state": "FL",
  "tradeCategory": "HVAC_ACR",
  "city": "MIAMI",
  "onlyActive": true,
  "maxItems": 50
}
```

California exact lookup:

```json
{
  "state": "CA",
  "licenseNumbers": ["1013845"],
  "onlyActive": false,
  "maxItems": 3
}
```

Exact license lookup ignores the discovery city and trade category. Active-status and expiration filters still apply. Supply only the numeric license portion for Texas and California; Florida numbers include their prefix. A missing exact license produces an `ERROR` record. A valid license excluded by a filter produces no record. An empty discovery search produces no records.

### What data can you extract?

| Data | California CSLB | Florida DBPR | Texas TDLR |
| --- | --- | --- | --- |
| License number, business or licensee, classification | Yes | Yes | Yes |
| Owner or qualifying individual | When published | Primary licensee | When published |
| Published license status | Yes | Yes | Not in this dataset |
| Expiration and renewal flags | When published | When published | When published |
| Issue date | When published | When published | Not in this dataset |
| Address, city, county, ZIP, phone | When published | When published | Frequently omitted by the source |
| Contractor bond company and amount | When published | Not on the supported detail page | Not in this dataset |
| Workers' Compensation carrier, policy, expiration | When published | Not on the supported detail page | Not in this dataset |

Unavailable fields are `null`. They are never filled with guesses or unrelated state agency contact information. California insurance exemptions leave policy fields `null`. `bondAmount` refers to the contractor's bond, rather than additional LLC or qualifying-individual bonds. Multiple California classifications share one record. Distinct Texas license classifications or subtypes can produce separate records for the same numeric license.

Texas uses the [TDLR All Licenses dataset](https://data.texas.gov/7358-krk7). Its published schema has no license-status column. `onlyActive` uses an unexpired date as a proxy, and `licenseStatus` explicitly says that TDLR does not publish status here. An unexpired date does not independently confirm that a license is in good standing. The dataset is a periodic extract and may lag behind a license's latest renewal.

Texas supports electrical and HVAC discovery. `ALL` includes those construction trades and excludes unrelated professions. [Plumbing licenses are overseen by a separate Texas board](https://www.texas.gov/occupational-professional-licenses-in-texas/index.html); plumbing, roofing, and general-building discovery are unavailable through this TDLR endpoint. Unsupported Texas trade discovery is rejected before any network requests. Published Texas HVAC records inspected on October 19, 2026 had no business or mailing city values. Such records cannot match a city filter; clear the city to retrieve them.

California uses [CSLB license detail lookups](https://www.cslb.ca.gov/OnlineServices/CheckLicenseII/LicenseDetail.aspx?LicNum=1013845) and requires license numbers. Florida submits the [official DBPR search form](https://www.myfloridalicense.com/wl11.asp), follows its pagination, and supports both legacy and current portal detail layouts. An empty Florida city searches the 67 Florida counties in turn; this is slower than searching one city. Records are checked against the exact requested city after detail extraction.

### Input settings

| Setting | Default | Accepted values |
| --- | --- | --- |
| `state` | `TX` | `TX`, `CA`, `FL` |
| `licenseNumbers` | None | Up to 5,000 exact numbers; required for CA |
| `tradeCategory` | `HVAC_ACR` | `ALL`, `ELECTRICAL`, `HVAC_ACR`, `PLUMBING`, `ROOFING`, `GENERAL_BUILDING` |
| `city` | `""` (statewide) | Optional city; empty string for statewide discovery |
| `onlyActive` | `true` | Exclude inactive or expired licenses; TX uses date inference |
| `expiringWithinDays` | None | Integer from 1 through 365, inclusive |
| `maxItems` | `50` | 1 through 5,000; form sample uses 3 |
| `timeoutSecs` | `15` | 3 through 60 seconds per HTTP attempt |
| `maxConcurrency` | `25` | 1 through 100; HTML portals use at most 3 concurrent lookups |
| `proxyConfiguration` | Automatic direct-first policy | Apify proxy editor configuration |

`daysUntilExpiration` compares calendar dates in UTC. Today is 0; yesterday is -1. `isRenewalLead` is true from 0 through 60 days, inclusive. Missing or invalid expiration dates yield `daysUntilExpiration: null` and `isRenewalLead: false`. Expiration-window filtering excludes missing dates and already expired licenses.

Texas always connects directly. California and Florida first connect directly, then can try SHADER and account datacenter proxies for HTTP 403, 429, or 503. Configured or residential fallback is enabled only when `useApifyProxy: true` is explicitly supplied. `useApifyProxy: false` forces direct requests. Custom `proxyUrls` are used as supplied, rotating across requests without automatic Apify fallback. HTTP and HTTPS proxies must resolve to public addresses.

Automatic Apify proxy use requires `APIFY_PROXY_PASSWORD` in the run environment. The Actor never retrieves an account-wide proxy password through a user-profile API. If credentials or proxy access are unavailable, source failures remain visible as uncharged errors. No credentials are included in logs or output.

### Output example

Illustrative data:

```json
{
  "state": "CA",
  "licenseNumber": "123456",
  "status": "SUCCESS",
  "businessName": "EXAMPLE ELECTRIC LLC",
  "ownerOrQualifierName": "JANE DOE",
  "tradeClassification": "C-10 - ELECTRICAL",
  "licenseStatus": "This license is current and active.",
  "issueDate": "2020-03-10",
  "expirationDate": "2026-11-01",
  "daysUntilExpiration": 13,
  "isRenewalLead": true,
  "phone": "+1 (213) 555-0100",
  "streetAddress": "100 MAIN ST",
  "city": "LOS ANGELES",
  "county": null,
  "zipCode": "90001",
  "workersCompCarrier": "EXAMPLE INSURANCE",
  "workersCompPolicyNumber": "WC123",
  "workersCompExpirationDate": "2026-12-31",
  "bondCompany": "EXAMPLE SURETY",
  "bondAmount": 25000,
  "sourceUrl": "https://www.cslb.ca.gov/OnlineServices/CheckLicenseII/LicenseDetail.aspx?LicNum=123456",
  "scrapedAt": "2026-10-19T12:00:00.000Z",
  "errorMessage": null
}
```

Every record uses the same 24 flat fields. An error retains `state`, the requested `licenseNumber` when known, `sourceUrl`, and `scrapedAt`; it sets `status: "ERROR"`, a safe `errorMessage`, nullable extracted fields to `null`, and `isRenewalLead: false`.

### Use the results in Clay, n8n, Make, or your API

- **Clay:** Import the dataset's CSV export, deduplicate by state, license number, and classification, then filter renewal leads.
- **n8n:** Use the Apify integration or an HTTP Request node to start a run, wait for completion, and fetch the default dataset. Route only `SUCCESS` records into your CRM.
- **Make:** Connect an Apify run to a dataset-record iterator, then send selected fields to your existing workflow.
- **API:** Use the Actor's API tab to obtain its run endpoint. After completion, request `GET https://api.apify.com/v2/datasets/{defaultDatasetId}/items?format=json&clean=true` with the authorization method provided by your Apify account.

Schedule repeated runs in Apify to track upcoming renewals. Match on state, license number, and classification because numeric license numbers are not globally unique. Dataset strings are protected against spreadsheet formula injection. Valid phone numbers retain their leading plus sign. Render values as text when displaying them in an HTML application.

### Pricing and limits

The custom pay-per-event name is **`license-scraped`**. Only saved `SUCCESS` records are submitted with that event. **`ERROR` rows cost $0 in custom event fees.** Platform compute, storage, transfer, and proxy usage may still apply to a run.

Use the current price shown in the Store or run pricing configuration. For a concrete example, at an event rate of **$0.002 per successful record**, 1,000 successful records cost **$2.00** in event fees; 900 successful records and 100 errors cost **$1.80** in event fees. This is an example calculation, not an advertised configured price. Pricing is configured on Apify, outside this repository.

Free Apify plans can save at most 25 records per run, including errors. Paid and local runs honor `maxItems` up to 5,000. A charge budget can reduce the number of successful records saved. When the budget is exhausted, the Actor flushes allowed records, stops scheduling requests, and finishes with exit code 0.

Records are written in serialized batches of up to 100, on a five-second flush interval, at limits, and during orderly shutdown. A platform abort can retain already saved records; a forced process kill cannot flush in-memory records.

### FAQ and troubleshooting

**Why did Texas return zero records for Austin?**

The source may omit every city in the selected trade. Clear `city`, use exact license numbers, or choose Florida city discovery. The Actor does not silently drop a user-supplied city filter. Empty discovery results have no custom event fee.

**Why are phone, address, bond, or insurance fields null?**

The selected official source did not publish them. These fields are not equally available across states. The DBPR agency phone in a page footer is never treated as a contractor phone.

**Why did California return `SOURCE_UNAVAILABLE`?**

CSLB sometimes redirects license detail requests to a maintenance page. This is detected as an `ERROR`, and that row has no custom event charge. Retry when the official portal is available. California detail requests on October 19, 2026 returned scheduled-maintenance pages during verification.

**Why was an exact number omitted?**

It may exist but fail `onlyActive` or `expiringWithinDays`. Disable the status filter and remove the expiration window to inspect it. A missing license or failed request is recorded separately as an error.

**What does a partial-response warning mean?**

HTML is limited to 4 MiB after decompression. A completed license record may be retained from a larger page, or after a timeout/reset when at least 16 KiB of a successful HTML response arrived. Missing fields remain null, and a page without the required license identity is rejected. JSON responses are limited to 2 MiB and are never partially parsed.

**Can I use limited permissions?**

Yes. The Actor uses run environment metadata for the free-plan gate and does not call `/v2/users/me`. Automatic proxy setup skips account-profile discovery. No browser or browser engine is installed or launched.

### Development and deployment

Requires Node.js 22 or later. All source, logs, schemas, and documentation are in English.

```sh
npm ci --ignore-scripts
npm run build
npm test
npm run smoke
```

Unit tests use `node:test` and deterministic HTTP fixtures or injected streams. `npm run smoke` performs direct live-source checks; it reports California source failures separately because maintenance cannot be predicted. Every child test or smoke process sets `UV_THREADPOOL_SIZE=64`, production mode, and strict TLS verification.

The Dockerfile uses a Node.js 22 Alpine build stage and a non-root production stage with a 160 MiB JavaScript heap limit. Actor run defaults are 256 MiB RAM and a 3,600-second timeout. Network protection validates public DNS addresses on every redirect, pins direct DNS lookups without replacing the hostname, and tunnels proxy connections to validated destination IPs while keeping TLS verification and the original TLS server name.

Before publication, configure **Pay per event** in Apify Console with the event **`license-scraped`** and your chosen price. Do not add an automatic default-dataset-item charge; only the explicit custom event should have a price. Keep the Actor's run permission mode at **Limited permissions**. The repository does not publish, set a Store price, or modify your account automatically.

Initialized by Arezgit.

# Actor input Schema

## `state` (type: `string`):

Choose the official state licensing database.

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

Optional exact numbers. Required for California. Exact lookup ignores city and trade category; status and expiration filters still apply. Texas requires the numeric portion only.

## `tradeCategory` (type: `string`):

Discovery filter. Texas supports electrical and HVAC through TDLR; ALL includes only supported construction trades.

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

Optional discovery filter. Leave empty for statewide search. Texas records without a published city cannot match a city filter. Enter a city such as MIAMI to narrow Florida discovery.

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

Exclude inactive or expired licenses. Texas open data has no status field: unexpired dates are used as a proxy and labeled explicitly.

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

Include licenses expiring today through this many days from today, inclusive.

## `maxItems` (type: `integer`):

SUCCESS and ERROR rows both count. Free Apify plans are capped at 25 records per run.

## `timeoutSecs` (type: `integer`):

Timeout for each HTTP attempt, including DNS validation.

## `maxConcurrency` (type: `integer`):

Parallel exact lookups. HTML government portals are additionally limited to 3 concurrent lookups.

## `proxyConfiguration` (type: `object`):

Texas always uses direct connections. Other states start direct and may use datacenter fallback. Residential fallback requires explicit enablement. Disable Apify proxy to force direct requests.

## Actor input object example

```json
{
  "state": "TX",
  "tradeCategory": "HVAC_ACR",
  "city": "",
  "onlyActive": true,
  "maxItems": 3,
  "timeoutSecs": 15,
  "maxConcurrency": 25
}
```

# Actor output Schema

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

Default dataset items. Review each record status before using extracted data downstream.

# 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 = {
    "state": "TX",
    "tradeCategory": "HVAC_ACR",
    "city": "",
    "maxItems": 3
};

// Run the Actor and wait for it to finish
const run = await client.actor("arezdev/us-state-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 = {
    "state": "TX",
    "tradeCategory": "HVAC_ACR",
    "city": "",
    "maxItems": 3,
}

# Run the Actor and wait for it to finish
run = client.actor("arezdev/us-state-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 '{
  "state": "TX",
  "tradeCategory": "HVAC_ACR",
  "city": "",
  "maxItems": 3
}' |
apify call arezdev/us-state-contractor-license-scraper --silent --output-dataset

```

## MCP server setup

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