Washington L&I Contractor & License Scraper avatar

Washington L&I Contractor & License Scraper

Pricing

from $1.50 / 1,000 results

Go to Apify Store
Washington L&I Contractor & License Scraper

Washington L&I Contractor & License Scraper

Washington State L&I contractor license scraper & API: search registrations and export license number, status, business name, owner, bond, insurance and expiry. Contractor verification, compliance and B2B lead generation — fast, no login.

Pricing

from $1.50 / 1,000 results

Rating

0.0

(0)

Developer

Haketa

Haketa

Maintained by Community

Actor stats

0

Bookmarked

4

Total users

0

Monthly active users

a day ago

Last modified

Share

Washington L&I Contractor License Scraper — License, Insurance, Bond, Owner & UBI Data

The most complete Washington State Department of Labor & Industries (L&I) contractor data extraction tool on Apify. This Washington L&I scraper pulls every registered Construction, Electrical, Plumbing, and Elevator contractor in Washington — joined live with liability insurance, surety bond, and principal-owner data from data.wa.gov — into clean, structured records ready for compliance, bidding, lead-gen, underwriting, and risk-analytics. No login, no coding. Use it to scrape Washington L&I contractor data at scale in a single run.

Export to JSON, CSV, Excel, or API.

Built for construction sales & lead-gen teams, GCs vetting subcontractors, insurance/surety underwriters, compliance and procurement officers, construction-defect attorneys, and researchers who need verified Washington contractor data in bulk.

Apify Actor


🔎 Why scrape Washington L&I contractor data?

The Washington State Department of Labor & Industries (L&I) registers every Construction Contractor (CC), Electrical Contractor (EC), Plumbing Contractor (PC), and Elevator Contractor (LC) doing business in Washington. That registry is the single source of truth for who is legally allowed to contract in WA, what they are licensed to do, who insures and bonds them, who owns the company, and whether the registration is currently in good standing. As of the latest run there are roughly 76,000+ ACTIVE registrations and 160,000+ total records (active, expired, suspended, out-of-business) across the four license types.

This data is gold for sales, compliance, and risk teams — but collecting it manually is painful. L&I's consumer verify tool (secure.lni.wa.gov/verify) only does single-record, JavaScript-rendered lookups: no bulk export, insurance/bond/owner data split across separate sub-pages, specialty codes buried in tooltips, and search pagination that silently truncates past a few hundred hits. The official Socrata SODA API exists, but stitching four separate datasets together by license number is hours of SoQL plumbing.

This Washington L&I contractor license scraper does all of that for you. It talks directly to L&I's open-data API on data.wa.gov, joins four official datasets by contractorlicensenumber, normalises the cryptic field names, formats dates to ISO YYYY-MM-DD, de-duplicates, and returns one flat record per contractor.

Common reasons people scrape Washington L&I:

  • Build targeted B2B prospect lists of active contractors by city, trade, and license type
  • Verify a subcontractor's license, bond, and insurance before awarding a bid (RCW 18.27)
  • Feed insurance / surety underwriting with verified carrier, bond, UBI, and owner data
  • Run daily compliance monitoring — catch ACTIVE → SUSPENDED transitions and impaired bonds
  • Enrich a CRM with UBI, principal-owner names, and contact phone numbers
  • Support construction-defect litigation and investigative journalism with licensing evidence
  • Power market & workforce research — contractor density, new-entrant velocity, enforcement rates

✨ What this Washington L&I scraper does

  • ✅ Scrapes the full WA L&I contractor registry via the official Socrata SODA API on data.wa.gov — the same backend as the state Open Data portal
  • ✅ Joins four official datasets in one run by contractorlicensenumber: General license + Insurance + Bond + Principal/owner
  • ✅ Server-side filtering (SoQL pushdown) by license status, license type (CC/EC/PC/LC), trade specialty, city, and effective-date range — only matching rows cross the wire
  • ✅ Automatic pagination with count-then-page logic, returning hundreds to tens of thousands of de-duplicated records, capped by maxRecords
  • ✅ Rich captured fields — identity (businessName, licenseNumber, ubi, licenseTypeDesc, status), location (address, city, state, zip, phone), classification (specialty1Desc, businessType), dates (licenseEffectiveDate, licenseExpirationDate), insurance (insuranceCompany, insurancePolicyNo, insuranceAmount, insuranceExpiration), bond (bondFirm, bondAmount, bondExpiration, bondImpaired), and owners (primaryPrincipal, principals)
  • ✅ Latest-wins enrichment — the most recent insurance and bond record per contractor is selected automatically
  • ✅ De-duplicated output — each licenseNumber emitted at most once per run
  • ✅ ISO date normalisation to YYYY-MM-DD
  • ✅ Fast & reliable HTTP-only engine — no headless browser, no CAPTCHA, no login, no proxy. A broad 500-record statewide pull with full enrichment completes in ~20 seconds
  • ✅ Export to JSON, CSV, Excel, XML, HTML, or via the Apify API

Only features the code actually implements are claimed above.

🎯 Use cases

  • Construction lead generation — building-material suppliers, equipment dealers, and construction SaaS (Procore, Buildertrend, CompanyCam) build targeted lists of active GCs and specialty subs by metro and trade.
  • Subcontractor vetting & bid compliance — GCs and owner's reps verify a sub's license is ACTIVE, the bond is not impaired, and insurance is current before issuing an RFQ or subcontract.
  • Insurance underwriting & surety risk modeling — carriers pre-fill quotes with UBI, business type, prior carrier, and bond data; flag impaired bonds and bond churn.
  • Compliance, procurement & public works — agencies, school districts, and prime contractors verify prime/sub registration and keep audit-ready, timestamped snapshots.
  • Legal, litigation & investigation — construction-defect attorneys confirm a contractor was licensed on the contract date, identify the surety for bond claims, and map principal-owner overlap across entities.
  • Real-estate development & origination — developers profile contractor density and capacity by submarket before mobilizing on a project.
  • Recruiting & staffing — trade-staffing firms build employer lists by specialty and metro for outbound sourcing.
  • Market & academic research — labor economists study small-business formation, enforcement, and new-license velocity over time.

📥 Input

Pick from the dropdowns or just press Start — there are no codes to memorise. Every parameter is optional; an empty run returns a fast, diverse 100-record statewide sample.

ParameterTypeDescription
statusesarray<string>License status filter. Values: ACTIVE, EXPIRED, SUSPENDED, RE-LICENSED, OUT OF BUSINESS, INACTIVE, SUPERCEDED, PASSED AWAY. Empty = all statuses (case-insensitive). Prefilled with ["ACTIVE"].
licenseTypesarray<string>License type code: CC (Construction), EC (Electrical), PC (Plumbing), LC (Elevator). Empty = all.
specialtiesarray<string>Trade/specialty code filter on specialtycode1 (e.g. 01 General, 02 Residential, AD Plumbing, CD Roofing, SM/6A HVAC, CB Painting). The dropdown lists every trade with a friendly label. Empty = all trades.
citiesarray<string>City filter (case-insensitive) from a dropdown of major WA cities, e.g. ["SEATTLE","TACOMA","SPOKANE"]. Empty = whole state.
citiesCustomarray<string>Advanced free-text list for any WA city not in the dropdown. Merged with cities.
effectiveAfterstringOnly licenses with licenseeffectivedate >= YYYY-MM-DD. Great for finding newly licensed contractors. Empty = no lower bound.
effectiveBeforestringOnly licenses with licenseeffectivedate <= YYYY-MM-DD. Empty = no upper bound.
enrichInsurancebooleanJoin the Insurance dataset (carrier, policy no, amount, expiration). Default true.
enrichBondbooleanJoin the Bond dataset (firm, amount, impaired status, expiration). Default true.
enrichPrincipalbooleanJoin the Principal dataset (owners/officers). Default true.
maxRecordsintegerCap on total contractor records. 0 = unlimited (the full matching set). Prefilled 100.
pageSizeintegerRecords fetched per Socrata page. Min 100, max 50000, default 1000.
requestDelayintegerMilliseconds between API calls (0–5000, default 200).

Example input

{
"statuses": ["ACTIVE"],
"licenseTypes": ["CC"],
"specialties": ["01"],
"cities": ["SEATTLE", "BELLEVUE", "TACOMA"],
"effectiveAfter": "2024-01-01",
"enrichInsurance": true,
"enrichBond": true,
"enrichPrincipal": true,
"maxRecords": 2000,
"pageSize": 1000,
"requestDelay": 200
}

📤 Output

Each row is a flat JSON object with a stable shape regardless of enrichment toggles — fields you didn't request (or that have no source record) are simply null. This keeps downstream ingestion (BigQuery, Postgres, Snowflake, Salesforce upserts) branch-free.

The two examples below are real records from a fresh verification run (statewide ACTIVE, full enrichment):

{
"businessName": "WA MOBILE LOCKSMITH",
"licenseNumber": "WAMOBML743P9",
"licenseTypeCode": "CC",
"licenseTypeDesc": "CONSTRUCTION CONTRACTOR",
"status": "ACTIVE",
"statusCode": "A",
"address": "1901 Merrill Creek Pkwy",
"city": "EVERETT",
"state": "WA",
"zip": "98203",
"phone": "4253285921",
"ubi": "606306938",
"businessType": "Limited Liability Company",
"specialty1": "SQ",
"specialty1Desc": "Locks, Security and Alarm Equipment",
"specialty2": null,
"specialty2Desc": null,
"primaryPrincipal": "Artzi, Segev",
"licenseEffectiveDate": "2026-10-30",
"licenseExpirationDate": "2028-10-30",
"suspendDate": null,
"insuranceCompany": "HISCOX INSURANCE COMPANY INC",
"insurancePolicyNo": "P1074539261",
"insuranceAmount": "1000000.0000",
"insuranceExpiration": "2027-10-16",
"insuranceAgency": null,
"bondFirm": "Westfield Insurance Co",
"bondAmount": "15000.0000",
"bondExpiration": "Until Canc",
"bondImpaired": "No",
"principals": "Artzi, Segev",
"scrapedAt": "2026-11-02T00:14:25.397Z"
}
{
"businessName": "STILLWATER 2 LLC",
"licenseNumber": "STILL2L742PT",
"licenseTypeCode": "CC",
"licenseTypeDesc": "CONSTRUCTION CONTRACTOR",
"status": "ACTIVE",
"statusCode": "A",
"address": "7020 147TH AVE NE",
"city": "LAKE STEVENS",
"state": "WA",
"zip": "98258",
"phone": "4258700249",
"ubi": "603290508",
"businessType": "Limited Liability Company",
"specialty1": "01",
"specialty1Desc": "GENERAL",
"specialty2": null,
"specialty2Desc": null,
"primaryPrincipal": "TOUCHETTE, MICHAEL WYNN",
"licenseEffectiveDate": "2026-10-30",
"licenseExpirationDate": "2028-10-30",
"suspendDate": null,
"insuranceCompany": "Nautilus Ins Co",
"insurancePolicyNo": "NN1802678",
"insuranceAmount": "1000000.0000",
"insuranceExpiration": "2027-02-09",
"insuranceAgency": null,
"bondFirm": "Merchants Bonding Co (Mutual)",
"bondAmount": "30000.0000",
"bondExpiration": "Until Canc",
"bondImpaired": "No",
"principals": "TOUCHETTE, MICHAEL WYNN; TOUCHETTE, ROBERT LEWIS",
"scrapedAt": "2026-11-02T00:14:25.494Z"
}

Output field reference

FieldDescription
businessNameLegal business name as registered with L&I
licenseNumberL&I contractor license number — the primary join key across all four datasets
licenseTypeCodeCC, EC, PC, or LC
licenseTypeDescHuman-readable license type, e.g. CONSTRUCTION CONTRACTOR
statusLicense status — ACTIVE, EXPIRED, SUSPENDED, RE-LICENSED, OUT OF BUSINESS, INACTIVE, SUPERCEDED, PASSED AWAY
statusCodeL&I single-letter status code (e.g. A for active)
addressRegistered business address (line 1)
city / state / zipRegistered city, two-letter state, and ZIP. state may be out-of-state (e.g. OR, ID) for firms licensed to work in WA
phoneRegistered contractor phone (digits only)
ubiWashington Unified Business Identifier — cross-agency 9-digit ID
businessTypeEntity type, e.g. Limited Liability Company, Corporation, Individual
specialty1 / specialty1DescPrimary trade specialty code + description
specialty2 / specialty2DescSecondary specialty code + description — present when available (most contractors have one specialty)
primaryPrincipalPrimary principal/owner name from the General dataset
licenseEffectiveDate / licenseExpirationDateLicense dates, ISO YYYY-MM-DD
suspendDateSuspension date — present only for suspended records
insuranceCompanyLiability insurance carrier — present when the contractor has an insurance record on file (~93% of ACTIVE)
insurancePolicyNoPolicy number on file with L&I
insuranceAmountCoverage amount as reported by Socrata (raw string, e.g. 1000000.0000)
insuranceExpirationPolicy expiration date, ISO YYYY-MM-DD
insuranceAgencyProducing agency name — present when available (sparsely populated in source)
bondFirmSurety firm issuing the bond
bondAmountBond face amount as reported (raw string, e.g. 15000.0000, 6000.0000, 30000.0000)
bondExpirationBond expiration — often the literal "Until Canc" (until cancelled) rather than a date, mirroring L&I's source
bondImpairedYes / No — whether the bond is currently impaired by a claim
principalsSemicolon-delimited list of currently-active owners/officers
scrapedAtISO-8601 timestamp of extraction

Honest note on coverage (from the fresh run): core identity/location/date fields, bond fields, and owner fields populate at 100%; insurance fields at ~93–96% (the remainder genuinely have no insurance record on file yet). specialty2, suspendDate, and insuranceAgency are legitimately sparse/data-dependent — they are not bugs, they're simply empty when the source has no value. Amounts (insuranceAmount, bondAmount) and bondExpiration are faithful passthroughs of the raw Socrata values; apply numeric/date parsing downstream if you need typed values.

🔁 Pagination & scale

The actor is built for bulk. It first calls Socrata count(*) to size the run, then walks the General dataset in $limit/$offset pages (default pageSize 1000) using SoQL ORDER BY licenseeffectivedate DESC, advancing page by page until the result set is exhausted or maxRecords is reached. License numbers are then batched into chunks of 100 for the three enrichment datasets' IN(...) lookups (URL-length safe), and results are joined in process.

  • maxRecords controls the ceiling — set 0 for the entire matching set (tens of thousands; up to the full ~160K statewide).
  • Output is de-duplicated on licenseNumber — a contractor appearing on multiple General rows (e.g. a renewal or second specialty) is collapsed to a single record.
  • Verified in the latest run: a pageSize: 250, maxRecords: 500 request fetched 2 pages (offset 0, offset 250 = 500 General rows) which de-duplicated to 484 unique contractors in ~21 seconds with full insurance + bond + principal enrichment.

⚙️ How to use

  1. Open the actor on the Apify Console and click Try for free (or Start). The default input returns ~100 ACTIVE Washington contractors with full enrichment.
  2. Set your filters — statuses, licenseTypes, specialties, cities, and date range — and a maxRecords cap (use 0 for everything).
  3. Run it on-demand, via the API, or on a schedule (Apify Scheduler, any cron).
  4. Download the dataset as JSON, CSV, Excel (XLSX), XML, or HTML from the dataset tab, or pull it from the dataset API endpoint.
  5. Integrate downstream — Google Sheets, Make.com, Zapier, n8n, webhooks to Postgres/Snowflake/BigQuery, or CRM upserts (Salesforce/HubSpot) keyed on licenseNumber or ubi.

API run (cURL)

curl -X POST "https://api.apify.com/v2/acts/haketa~washington-li-contractor-license-scraper/runs?token=YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"statuses": ["ACTIVE"],
"licenseTypes": ["CC"],
"cities": ["SEATTLE"],
"enrichBond": true,
"enrichInsurance": true,
"maxRecords": 500
}'

API run (Python)

from apify_client import ApifyClient
client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("haketa/washington-li-contractor-license-scraper").call(run_input={
"statuses": ["ACTIVE"],
"licenseTypes": ["CC", "EC"],
"specialties": ["01"],
"cities": ["SEATTLE", "BELLEVUE", "TACOMA"],
"maxRecords": 2000,
})
for r in client.dataset(run["defaultDatasetId"]).iterate_items():
print(r["licenseNumber"], r["businessName"], r["status"], r["city"], r["insuranceCompany"], r["bondFirm"])

💡 Tips

  • General contractors = CC + specialty 01. Washington L&I has no distinct GC code — filter licenseTypes: ["CC"] with specialties: ["01"] to get general contractors; a CC with a trade specialty (e.g. roofing CD) is a specialty/sub contractor.
  • Find brand-new contractors with effectiveAfter (e.g. "2025-01-01") — great for untapped prospect lists.
  • Compliance dashboard: run statuses: ["SUSPENDED"] with enrichment on, then post-filter bondImpaired == "Yes" for the highest-risk firms.
  • Fastest bulk pull: set enrichInsurance/Bond/Principal to false and pageSize to 5000 for a lightning-fast identity-only export, then enrich only the subset you care about.
  • HVAC lives under two codes — include both SM and 6A. Plumbing specialty is AD.
  • Join to other WA datasets on ubi (Department of Revenue, Secretary of State, Employment Security) and on licenseNumber for history diffing across scheduled runs.

❓ FAQ

Is scraping Washington L&I data legal? This actor only reads public open data that L&I publishes at data.wa.gov under Washington's Public Records Act (RCW 42.56), via the official Socrata SODA API built for programmatic access. It does not scrape the secure.lni.wa.gov/verify HTML page. Downstream marketing use (CAN-SPAM, TCPA, GDPR/CCPA) remains the data consumer's responsibility.

How many results can I get, and how fast? Washington has ~76,000+ ACTIVE and ~160,000+ total contractor records. Set maxRecords: 0 for the full matching set. In the latest verification run, 484 de-duplicated records with full enrichment came back in ~21 seconds; identity-only bulk pulls are faster still.

Do I need proxies or a login? No. The Socrata API is public and needs no authentication — just an Apify account. No proxy and no browser are required. (A Socrata app token is optional if you want higher per-IP quotas.)

What export formats are supported? JSON, CSV, Excel (XLSX), XML, HTML, RSS, and JSON Lines — from the dataset view or the Apify API.

Is there an API? Yes — run the actor and fetch results through Apify's REST API, or trigger it from Make/Zapier/n8n and webhooks.

How fresh is the data? Live at run time. Socrata mirrors L&I's contractor registration system on a regular cadence (near-daily for status changes, periodic for full refreshes).

Can I filter by city, trade, or license type? Yes — all three, plus status and an effective-date range, filtered server-side via SoQL. Numeric filtering on bond/insurance amount isn't an input yet; apply it downstream on the returned fields.

Are contractor emails included? No — L&I does not publish email addresses in the public datasets. Phone numbers and mailing addresses are included.

Why is insuranceCompany sometimes null on an ACTIVE contractor? Either enrichment was disabled, or the contractor has no insurance record on file in the Socrata dataset at run time (a genuine source gap, ~4–7% of ACTIVE records). For mission-critical verification, also check the official L&I verify page by UBI.

What does bondImpaired = "Yes" mean? Claims have been filed against the bond and drawn down its value. L&I treats an impaired bond as a compliance trigger — the contractor generally cannot legally bid new work until it's restored.

Does the actor de-duplicate? Yes. Each licenseNumber is emitted at most once per run, and the latest-effective-date insurance and bond record is selected per contractor.

Can I get historical snapshots? The actor returns current state. To build history, schedule it (e.g. daily) and archive each Apify dataset run, then diff on licenseNumber.

Need licensing data from other US states or regulatory bodies? Check these complementary Apify actors by haketa:


Ready to pull verified Washington contractor data? Hit Start with the default input for a free sample, or set your filters and maxRecords: 0 to export the whole state — license, insurance, bond, owner, and UBI data in one run. ⭐ If this Washington L&I scraper saves you time, a 5-star rating helps other compliance, construction, and underwriting teams find it.