← Guides

How-to

How to run Contractor License Verify - AI Agent Tool (17 States)

Step-by-step: the exact input, what each field does, and the rows that come back from AL LBGC, AR ACLB, CSLB, CT DCP, FL DBPR, MA CSL, MI LARA, MN DLI, NCLBGC, NM CID, NV NSCB, OR CCB, SC LLR, TN DOCI, TX TDLR, VA DPOR, WA L&I.

You are building something — an agent, a vendor-onboarding flow, a CRM intake step — that needs to answer one question: is this contractor actually licensed in this state? Today your options are to send a human to a state portal, or to skip the check. There is no public API: California’s CSLB is an ASP.NET WebForms portal, South Carolina’s board puts a reCAPTCHA on the search itself, New Mexico an image CAPTCHA, Virginia publishes a bulk roster with no search at all. Seventeen states, seventeen different shapes, none of them callable by a model. This actor is one call in, one flat verdict out, with the board that answered cited on the row.

What you’ll need

Nothing but an Apify account. The actor talks to AL LBGC, AR ACLB, CSLB, CT DCP, FL DBPR, MA CSL, MI LARA, MN DLI, NCLBGC, NM CID, NV NSCB, OR CCB, SC LLR, TN DOCI, TX TDLR, VA DPOR, WA L&I directly.

Nothing else. No API key, no proxy configuration, no CAPTCHA-solving account. The two boards that CAPTCHA their search (South Carolina and New Mexico) are solved inside the per-state lookup actors, and that cost is already in the price — it is why those two states cost more per check.

Step 1 — Open the actor

Open Contractor License Verify - AI Agent Tool (17 States) on the Apify Store and press Try for free. The actor’s own page on this site lists its live health and pricing.

The Contractor License Verify - AI Agent Tool (17 States) listing on the Apify Store
The listing on the Apify Store. Pricing and the nightly health record are both public.

Step 2 — Fill in the input

The input form for Contractor License Verify - AI Agent Tool (17 States) in the Apify Console
The input form, as it appears in the Apify Console.
FieldTypeRequiredWhat it does
statestringnoREQUIRED. The 2-letter US state code of the licensing board to check, e.g. ‘TX’. Supported: AL, AR, CA, CT, FL, MA, MI, MN, NC, NM, NV, OR, SC, TN, TX, VA, WA. Pass any other…
companystringnoThe business or licensee name to look up, e.g. ‘Ransome Company’. Use this when you do not have a licence number. Matching is EXACT on the full name once case, punctuation and…
licenseNumberstringnoThe contractor’s licence number, e.g. ‘22726’. This is the most reliable input - prefer it over the company name whenever the user has given you one. Punctuation and leading…
companiesarraynoBatch form of ‘company’ - check many names in ONE run instead of starting a run per name. All entries must be in the same state. You are billed per verdict returned, not per… (default [])
licenseNumbersarraynoBatch form of ‘licenseNumber’ - check many licence numbers in ONE run. All entries must be in the same state. Billing is per verdict returned, so batching costs the same as… (default [])

state is mandatory and it is not a filter — it is the router. Contractor licensing is per-state, so without it there is nothing to check. If your user has not said which state, ask them rather than guessing; guessing produces a confident answer about the wrong registry.

Send licenseNumber whenever you have one. It is exact. company is matched on the whole normalized business name (case, punctuation and LLC/Inc suffixes ignored), so “ABC Roofing” will not match “ABC Roofing & Siding” — deliberately. If a name matches two licences you get check_failed rather than a coin flip, because handing back the wrong contractor’s licence is worse than handing back nothing.

One exception worth knowing: Virginia cannot be searched by licence number at all. DPOR publishes a name-filtered roster with no number axis, so a number-only request there returns check_failed telling you to supply the name.

A working input:

{
  "state": "CA",
  "licenseNumber": "22726"
}

Step 3 — Run it

Press Start. Rows are pushed to the dataset as they’re found, so the run log fills in as it works.

One request in, exactly one row out — always. This actor cannot return zero rows for a request you made: every outcome, including “we could not check”, is a row with a verdict.

Measured on 2026-08-06, one licence-number lookup per state: median 13.3 seconds, fastest 10.6s (Florida, California, Washington, Michigan, North Carolina), slowest 30.8s for South Carolina, whose board CAPTCHAs the search. New Mexico is 19.6s for the same reason. If you are calling this from an agent loop, set your timeout above 35 seconds and expect ~13s typical.

Step 4 — Read the output

Each run returns a labelled table, not raw JSON:

The output fields returned by Contractor License Verify - AI Agent Tool (17 States)
The output view — every field the actor returns, named and typed.

Every row looks like this — a real row from a real run:

{
  "verdict": "active",
  "verdict_reason": "status 'This license is current and active.', expires 2027-10-31",
  "verdict_basis": "status+expiry_date",
  "requested_company": null,
  "requested_license_number": "22726",
  "requested_state": "CA",
  "entity_name": "RANSOME COMPANY",
  "license_number": "22726",
  "license_type": null,
  "status_raw": "This license is current and active.",
  "expiry_date": "10/31/2027",
  "days_to_expiry": 451,
  "source_portal": "CSLB",
  "checked_at": "2026-08-06T19:20:11+00:00",
  "source_url": "https://www.cslb.ca.gov/OnlineServices/CheckLicenseII/LicenseDetail.aspx?LicNum=22726"
}
FieldExample valueMeaning
verdictactiveactive
verdict_reasonstatus 'This license is current and active.', expires 2027-10-31Plain-language explanation, safe to quote to an end user
verdict_basisstatus+expiry_dateWhich evidence the verdict rests on: status, expiry_date, status+expiry_date, or none
requested_companyThe company name that was searched, echoed back
requested_license_number22726The license number that was searched, echoed back
requested_stateCAThe state that was searched
entity_nameRANSOME COMPANYThe licensed business or person as the board publishes it
license_number22726License number as published by the board

verdict is the field, and the discipline is in what it refuses to say.

not_found does not mean unlicensed. It means the registry returned no match for what you searched. A typo, a DBA that differs from the licensed entity, a licence held under a parent company — all produce not_found. If your agent turns that into “this contractor is unlicensed” in front of an end user, it has made a false and damaging claim about a real business. Branch it to “could not verify, check manually”.

inactive is not revoked. The boards publish “Lapsed”, “Voluntary Surrender”, “Archived”, “Voluntary Termination” — all real strings, none of them a disciplinary action. Those map to inactive. revoked and suspended are reserved for what the board actually calls a revocation or suspension. Do not paraphrase one as the other.

check_failed is never a pass. Board down, ambiguous name, a record missing a field that board normally publishes — all fail closed.

verdict_basis tells you what the answer rests on, because the boards differ: Alabama, Texas and Virginia publish no status field, so those verdicts stand on the expiry date alone; North Carolina and South Carolina publish no expiration date, so those stand on status alone. Everywhere else you get status+expiry_date. status_raw always carries the board’s own wording if you want to quote it verbatim.

Step 5 — Export it

Open the Dataset tab and export to CSV, JSON, or Excel — or pull the same rows from the API, which is what you want if this is going to run on a schedule.

What it costs

$0.004 per verdict resolved on a board. Verdicts of not_found, unverifiable_state_not_covered and check_failed are free — they describe the check, not the contractor, and you should not pay for an answer we could not get.

There is a second line: this tool runs the per-state lookup actors on your behalf and those bill their own per-match fee to your account. Budget roughly $0.008 per verdict in the fifteen open-data states, and about $0.034 in New Mexico and South Carolina, where every check costs a CAPTCHA solve. A thousand verifications spread like the measured mix is a few dollars.

Where the data comes from

This reads AL LBGC, AR ACLB, CSLB, CT DCP, FL DBPR, MA CSL, MI LARA, MN DLI, NCLBGC, NM CID, NV NSCB, OR CCB, SC LLR, TN DOCI, TX TDLR, VA DPOR, WA L&I directly. The target is Apify actor-to-actor routing over 17 state-board actors (ASP.NET WebForms, Socrata open data, PSI portal, bulk rosters). Every night a canary runs this actor against that live source and diffs the result against a frozen fixture — what “verified” means.

No two of these boards agree on anything. Some are open-data endpoints answering in under a second; CSLB is a WebForms portal behind a WAF that will hand you an ordinary-looking 200 that is really a redirect; South Carolina and New Mexico gate the search itself behind a CAPTCHA. They disagree on date formats, on whether a status field exists at all, on whether an expiry date exists, and on what their own status words mean.

That last one is the real work. On 2026-08-06 Florida’s DBPR went into scheduled maintenance and served a perfectly normal page saying so — which parsed to zero results and would have reported every Florida contractor as not_found. That is now caught and returned as check_failed. The value here is not fetching pages; it is refusing to turn a broken check into a confident wrong answer.

See it used

Watching a whole roster rather than answering one question? Use Contractor License Expiration Monitor instead — same boards, but it re-checks a watchlist on a schedule and alerts you on changes.

Last updated 2026-08-06