# CSLB California Contractor Scraper (`deadwood_data_solutions/california-contractor-directory-leads-scraper`) Actor

233K California contractors with phone numbers, filtered by when their workers-comp policy expires. Timed insurance leads: find contractors whose coverage renews in the next 30/60/90 days. No login, no API key. Optionally sync new leads to Notion, HubSpot, Airtable or Supabase — free.

- **URL**: https://apify.com/deadwood\_data\_solutions/california-contractor-directory-leads-scraper.md
- **Developed by:** [K O](https://apify.com/deadwood_data_solutions) (community)
- **Categories:** Lead generation, Real estate, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 leads

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

## California Contractor Insurance Leads — sorted by Workers-Comp renewal date

**Reach contractors *when their insurance is about to renew* — not at random.**

Built from **CSLB public license records** (California Contractors State License Board): every active California contractor with a phone number and a workers'-comp policy expiration date, sorted so the ones renewing soonest come out first.

***

### Pain points

- Generic "contractor leads" lists give you a name and maybe an email. They can't tell you **when** a contractor is actually in-market for a new policy — you're cold-calling on a schedule that has nothing to do with theirs.
- CSLB's own public license lookup is a one-license-at-a-time web form. There is no bulk export, no phone-number guarantee, and no way to sort by "who renews soonest."
- Buying a generic California contractor list gets you the same static roster everyone else already called. By the time you reach someone, their policy renewed months ago and they already re-upped with their current carrier.

### What we solve

- **The renewal date is the product.** Every record carries the contractor's exact workers'-comp policy expiration date, computed days-to-renewal, and current carrier — the buying-window signal no generic list has.
- **Sorted soonest-first.** Results come back ranked by renewal urgency, so the hottest leads are always at the top even when you cap the run with `maxResults`.
- **99.9% phone fill** on active, dated records — this is a call list, not a spreadsheet of names.
- **You are billed once per contractor, per run — never twice.** A contractor that matches more than one county or license class you requested still appears once and is charged once. Filtering narrows the list; it never multiplies the bill.
- **Free discovery before you spend anything.** Set `listCountiesOnly: true` to get back the exact list of California county names in the dataset — spelled exactly as the `counties` filter expects — at zero cost, no leads charged. Get the filter right before your first billed run.

**Live inventory (California, active licenses with phone + WC date), refreshed continuously:**

- **~8,300** contractors renewing in the next **30 days**
- **~20,400** in the next **60 days**
- **~28,300** in the next **90 days**
- **117,000** total dated contractors across all **58 California counties**

### Who buys this

- **Workers-comp & contractor insurance agents** — call contractors right before renewal, when they'll actually take the quote.
- **Insurance agencies & wholesalers** building a California contractor pipeline.
- **B2B sellers** targeting contractors by trade and location.

***

### Input

```json
{
  "renewalWindowDays": 60,
  "counties": ["Riverside", "Orange"],
  "licenseClasses": ["C-10", "C-33"],
  "activeOnly": true,
  "maxResults": 1000
}
```

| Field | What it does |
|---|---|
| `listCountiesOnly` | **Free.** Skips the paid query and returns every valid California county name in the dataset. No leads charged. |
| `renewalWindowDays` | Only contractors whose WC policy expires within this many days. `30` = hottest, `0` = ignore timing. |
| `counties` | Limit to California counties. Empty = all of CA. Run `listCountiesOnly` first to get exact spellings. |
| `cities` | Limit to specific cities. |
| `licenseClasses` | CSLB class prefixes, e.g. `B` (general building), `C-10` (electrical), `C-33` (painting). |
| `activeOnly` | Active licenses only (default true). |
| `maxResults` | Hard cap. **You are billed per lead returned**, so this bounds your cost. |
| `webhookUrl` | Optional — the result batch is POSTed here as JSON on finish. |
| `syncEnabled` | Optional, off by default. Also send each matched lead to a connected app — see [Sync](#sync-to-your-crm-notion-hubspot-airtable-or-supabase-optional) below. |
| `syncDestination`, `syncWriteTool`, `syncFieldMap`, `syncExtraArgs` | Only used when `syncEnabled` is on. |

### Output

One record per lead, **sorted soonest-renewal first**. This is a real record from the current dataset — not a placeholder:

```json
{
  "company": "VALLEY PREMIUM PAINTING",
  "license": "1001033",
  "licenseType": "B General Building",
  "classifications": "B General Building; C-33 Painting & Decorating",
  "phone": "(760) 702-7557",
  "address": "47-412 LAGOON CT, INDIO, CA, 92201 (Riverside County)",
  "city": "Indio",
  "county": "Riverside",
  "zip": "92201",
  "state": "CA",
  "status": "Active",
  "wcExpire": "2026-08-22",
  "daysToRenewal": 12,
  "wcCarrier": "HARTFORD CASUALTY INSURANCE COMPANY",
  "wcPriorCarrier": ""
}
```

| Field | Description |
|---|---|
| `company` | Licensed business name. |
| `license` | CSLB license number. |
| `licenseType` / `classifications` / `classDesc` | Trade classification(s), CSLB class codes and descriptions. |
| `phone` | Business/license contact phone. |
| `address`, `city`, `county`, `zip`, `state` | License-of-record location. |
| `status` | CSLB license status (e.g. Active). |
| `wcExpire` | Workers'-comp policy expiration date (ISO). **The buying signal.** |
| `daysToRenewal` | Computed days from today to `wcExpire`. Determines sort order. |
| `wcCarrier` | Current workers'-comp carrier on file. |
| `wcPriorCarrier` | Prior carrier, when CSLB records a change. |

***

### Sync to your CRM, Notion, HubSpot, Airtable or Supabase (optional)

Turn on `syncEnabled` to also send each matched lead straight to a connected
app — free, with no extra charge. Connect the app under **Integrations** in
Apify Console, pick it as `syncDestination`, and set `syncWriteTool` to the
name of the tool that creates one record there (run the
[dataset-sync-connector](https://apify.com/deadwood_data_solutions/dataset-sync-connector)
Actor in `list-tools` mode against the same connector if you don't know the
name). A sync failure is logged as a warning and never blocks the dataset —
your leads always land here first regardless of what the destination does.

### Pricing

**Pay per lead returned.** You set `maxResults`, so you always control the bill. No subscription, no minimum. A run that returns 500 leads charges for 500 leads — nothing more. A contractor matching several of your filters is still one lead, billed once. `listCountiesOnly` runs charge nothing.

- **Sync to a connected app (optional)** — free. No event is charged for records sent to a destination; it's an added convenience on top of the leads you already paid for.

### Notes & compliance

- Source: **CSLB public contractor license data** (public record).
- Phone numbers are business/license contact numbers from the public register. **You are responsible for TCPA / DNC compliance** when calling — scrub against the National Do Not Call Registry as required.
- Coverage is **California** (plus a small Arizona set). Records without a valid WC expiration date are excluded, because timing is the point.

### Run frequency

Schedule it **weekly**. Each run surfaces the contractors newly entering your chosen renewal window, so your call list stays current with almost no overlap.

### FAQ

**How am I charged?**

Pay-per-lead. Each contractor returned by your filters and `maxResults` cap is one billed "lead" event. Matching multiple counties, cities, or license classes you specified does not multiply the charge — one contractor, one charge, per run. `listCountiesOnly: true` runs are entirely free.

**Where does the data come from?**

The California Contractors State License Board (CSLB) public license register — the same records CSLB itself publishes, extracted, normalized, and paired with each license's on-file workers'-comp policy expiration date so you can filter and sort by renewal timing.

**How fresh is it?**

The bundled dataset is refreshed on an ongoing basis as CSLB updates licenses and policy records. Run the actor on a weekly schedule to catch newly entering renewal windows.

**I ran it and got 0 results — why?**

Usually the county or city spelling didn't match. Run with `listCountiesOnly: true` first (free) to confirm the exact county names, or widen `renewalWindowDays`.

**Do you have contractors outside California?**

This actor is California-focused (plus a small Arizona subset already in the dataset). For dedicated Arizona coverage with owner names and license issue dates, see [az-contractor-leads](https://apify.com/deadwood_data_solutions/az-contractor-leads). For Texas tow companies and vehicle storage facilities with current insurance carrier on file, see [texas-tow-vsf-insurance-leads](https://apify.com/deadwood_data_solutions/texas-tow-vsf-insurance-leads).

***

### Need the rest of the record on these companies?

This actor returns one source. **[KYB Company Data Enrichment](https://apify.com/deadwood_data_solutions/company-intel-enrichment)** takes a company name and returns what the public record says about it across up to nine sources — state business registries, UCC lien filings, tax warrants and WARN layoff notices, the FMCSA motor-carrier census, the CMS NPI Registry, USAspending federal awards, EPA ECHO compliance, SEC EDGAR filings and CourtListener federal dockets.

Every enriched field carries its source and its match confidence. Only matches above the accept threshold are merged — anything weaker is surfaced as a suggestion and never folded into the record, because a false join is worse than returning nothing.

# Actor input Schema

## `listCountiesOnly` (type: `boolean`):

Skip the paid query entirely and return the exact list of California county names present in the dataset, so you can get the `counties` filter spelled right before running a billed query. No leads are charged in this mode.

## `renewalWindowDays` (type: `integer`):

Return only contractors whose workers-comp policy expires within this many days. This is the timing signal: 30 = hottest leads renewing now, 90 = fuller pipeline. Set 0 to ignore renewal timing and return all matching contractors.

## `counties` (type: `array`):

Limit to these California counties (e.g. Los Angeles, Orange, San Diego). Leave empty for all of California.

## `cities` (type: `array`):

Limit to these cities. Leave empty for all.

## `licenseClasses` (type: `array`):

Filter by CSLB class code prefix, e.g. "B" (general building), "C-10" (electrical), "C-36" (plumbing). Matches the contractor's classification. Leave empty for all trades.

## `activeOnly` (type: `boolean`):

Only include contractors with an active CSLB license.

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

Hard cap on returned leads. You are charged per lead returned, so this bounds your cost.

## `webhookUrl` (type: `string`):

If set, the full result batch is POSTed here as JSON when the run finishes.

## `syncEnabled` (type: `boolean`):

Send each matched lead to a connected destination (Notion, HubSpot, Airtable, Supabase, or any Apify MCP connector) in addition to the dataset. Free — this never triggers a billable event. A sync failure never blocks or fails the run.

## `syncDestination` (type: `string`):

Connect the app under Integrations in Apify Console first, then pick it here. Required only if 'Sync new records' is on.

## `syncWriteTool` (type: `string`):

Name of the destination's MCP tool that creates one record, e.g. 'create\_page' for Notion, 'insert' for Supabase. Run the dataset-sync-connector Actor in 'list-tools' mode against the same destination if you don't know it.

## `syncFieldMap` (type: `object`):

Maps destination argument names to this Actor's output field names, e.g. {"title": "company", "phone": "phone"}. Leave empty to pass each record through unchanged.

## `syncExtraArgs` (type: `object`):

Fixed arguments merged into every sync write call, e.g. {"database\_id": "abc123"} for Notion.

## Actor input object example

```json
{
  "listCountiesOnly": false,
  "renewalWindowDays": 60,
  "activeOnly": true,
  "maxResults": 1000,
  "syncEnabled": false,
  "syncFieldMap": {},
  "syncExtraArgs": {}
}
```

# 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 = {
    "syncFieldMap": {},
    "syncExtraArgs": {}
};

// Run the Actor and wait for it to finish
const run = await client.actor("deadwood_data_solutions/california-contractor-directory-leads-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 = {
    "syncFieldMap": {},
    "syncExtraArgs": {},
}

# Run the Actor and wait for it to finish
run = client.actor("deadwood_data_solutions/california-contractor-directory-leads-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 '{
  "syncFieldMap": {},
  "syncExtraArgs": {}
}' |
apify call deadwood_data_solutions/california-contractor-directory-leads-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,deadwood_data_solutions/california-contractor-directory-leads-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/V3peBiL5imG0hsihW/builds/I7Jjo90lqq4RfoCyO/openapi.json
