← Guides

How-to

How to run California Construction Trades Directory & Leads (CSLB)

Pick your trades, pick your counties, get one deduplicated contractor list — the exact input, what each field does, and what comes back.

You want every licensed building trade across a region — not one trade in one county, but the whole sweep: electricians, plumbers, HVAC, roofers, framers, painters, across four counties, as one list you can filter and load somewhere.

CSLB publishes that data for free. What it will not do is give you all of it at once. Its export form takes one classification at a time, so a thirteen-trade sweep across four counties is fifty-two downloads and a manual merge — and the merge is the annoying part, because contractors hold several classifications and turn up in more than one file.

This actor does the sweep in one run and hands you a deduplicated list.

What you’ll need

Nothing but an Apify account. No API key, no CAPTCHA credentials, no proxy — CSLB serves this data to ordinary datacenter IPs, so proxyConfiguration is off by default and you should leave it that way. Turning it on only adds latency.

The input

{
  "licenseClasses": ["B", "C-10", "C-20", "C-27", "C-36", "C-39"],
  "counties": ["Sonoma", "Napa", "Solano", "Contra Costa"],
  "maxResults": 500
}

licenseClasses — the trades to sweep. It ships prefilled with the thirteen building trades most lead lists want (general building, framing, concrete, drywall, electrical, flooring, HVAC, landscaping, painting, plastering, plumbing, roofing, sheet metal), so the actor does something useful before you touch anything. Replace them with whatever you actually want: any CSLB code works — A, B, B-2, the C-2C-60 specialties, and the C-61/D-xx limited specialties like C-61/D-49 tree service.

counties — California counties. Case, spacing and a trailing “County” don’t matter; sonoma, Sonoma and Sonoma County all resolve. Adding counties is free: every county you ask for is fetched in the same request, so a four-county sweep costs exactly as many requests as a one-county sweep.

maxResults — a cap per trade, not per run. That distinction matters: if it capped the run, the trades at the end of your list would come back empty and look like they don’t exist in your counties. Leave it empty to get everything.

listCountiesOnly — returns every valid county name and charges nothing. Use it if you’re unsure of a spelling.

What comes back

One row per contractor, deduplicated across your whole sweep:

{
  "license_number": "1070189",
  "business_name": "AMY'S ROOFING AND SOLAR",
  "matched_trade": "C-39",
  "classifications": ["B", "C10", "C39"],
  "county": "Sonoma",
  "city": "SANTA ROSA",
  "phone": "(707) 555 0142",
  "status": "CLEAR",
  "expiration_date": "08/31/2028",
  "bond_number": "100201394",
  "workers_comp_type": "Workers' Compensation Insurance"
}

Two fields are worth understanding together. matched_trade is which of your requested trades found this contractor. classifications is everything they actually hold. The example above was found under roofing but also holds a general building licence and an electrical one — which is useful, because it tells you the shop is bigger than “a roofer”.

Deduplication, and what you’re charged for

Contractors routinely hold several classifications. Ask for both roofing and solar and a shop holding both appears in both downloads.

You get one row and one charge. Deduplication happens by licence number before anything is billed. A real run makes this visible in the log:

Sweeping 2 trades across 2 counties (Napa, Sonoma) — 2 requests, one per trade.
C-39: 40 rows, 40 new after dedup.
C-46: 37 rows, 35 new after dedup.
Parsed 75 rows.

Seventy-seven rows fetched, seventy-five billed — two contractors held both trades.

Sizing a run before you start it

Rows scale with trades × county size, and California counties vary enormously. One trade across four North Bay counties returned 1,096 rows. The default thirteen trades across those same four counties returned 12,894 — roughly $25.79. Swap those counties for Los Angeles alone and the same thirteen trades returned 39,558, about $79. (Both measured 2026-08-23.)

If you want a bounded bill on a wide sweep, set maxResults — but read its title, because it is a cap per trade, not per run. Setting 500 against the default thirteen trades allows up to 6,500 rows, not 500. If you want the true count first, run one trade and multiply.

Reading the log

Every trade logs its own outcome, which matters in a sweep: without it, a trade with no contractors in your counties is invisible and you can’t tell whether you asked wrongly or the answer is genuinely zero. C-57: CSLB has no licences in the requested counties; 0 rows. is a real answer.

When CSLB itself is down

CSLB takes its online services offline for maintenance, and it does it by redirecting every request to a Department of Consumer Affairs notice page. The CSLB website stays up while this is happening, so the only way to tell from the outside is to load the data portal itself.

The run doesn’t fail and it doesn’t hand you an empty file. You get exactly one row saying what happened, and it isn’t charged:

{
  "notice": "CSLB is not answering: ...bounced to www.dca.ca.gov/maintenance/cslb_outage.html",
  "notice_type": "blocked"
}

That distinction is the point. “No C-39 contractors in Napa” and “the state portal is switched off” produce the same empty spreadsheet otherwise, and only one of them means your territory is empty. If you schedule this actor, check for notice_type before treating a run as a real result — and just retry later. Outages have been short, and nothing about your input needs changing.

Try it

Run California Construction Trades Directory & Leads with the input above, or read Build a regional construction lead list in one run for a worked territory example.

When to use a different actor

Last updated 2026-08-19