WA Contractor License Status Delta
Pricing
$10.00 / 1,000 license status changeds
WA Contractor License Status Delta
Watches named Washington State L&I contractor licenses in the official registry and alerts only on a genuine status change (Suspended, Revoked, Out of Business, Inactive, Re-Licensed). For general contractors, homeowners, subcontractor vetting, insurers, due-diligence. Free when nothing changes.
Pricing
$10.00 / 1,000 license status changeds
Rating
0.0
(0)
Developer
Radu Furtuna
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
8 days ago
Last modified
Categories
Share
Durable monitor for the official L&I Contractor License Data - General registry, published by the
Washington State Department of Labor & Industries (L&I) as open data on data.wa.gov. Watch specific
contractor licenses by their registration number and get notified only when their license status
genuinely changes. No API key needed, no login, no captcha.
Source
https://data.wa.gov/resource/m8qx-ubtq.json — the Socrata Open Data API for "L&I Contractor License
Data - General" (dataset id m8qx-ubtq). Confirmed live 14.09.2026: 161,190 rows, updated by L&I
(the underlying rowsUpdatedAt was hours old at the time of verification). contractorlicensenumber is
the dataset's unique, permanent identifier for one contractor license (exactly 12 characters, uppercase
letters/digits/asterisk, e.g. ECOSTSC758NN, JUANHH*839MG) — confirmed live by a full scan of all
161,190 values, not a sample. The contractorlicensestatus field observed live (14.09.2026) takes 10
values: ACTIVE (75,760), EXPIRED (61,331), SUSPENDED (9,781), RE-LICENSED (9,371),
OUT OF BUSINESS (4,702), INACTIVE (131), SUPERCEDED (95), PASSED AWAY (15),
RESTORED FROM ARCHIVED (3), REVOKED DUE DEPT ERR (1). A live SoQL measurement of
contractorlicensesuspenddate over the trailing 30 days found 1,517 transitions into a non-working
status, of which 1,430 were specifically SUSPENDED — a real, measured volume of change.
How it works
- Each
watchnames exactly one contractor license bycontractorLicenseNumber(the WA L&I registration number as published in the registry — look it up at secure.lni.wa.gov/verify/ if you only have a business name). One watch = one point query ($where=contractorlicensenumber='<contractorLicenseNumber>') = one HTTP request per run. - The first check of a new watch establishes a baseline (no charge). Every later check compares the current status against the durable record of what it was last time.
- Billing is tied only to the
contractorlicensestatus/statuscodefields — not to business name, address, phone, principal name, or the routinelicenseexpirationdaterenewal that shifts for every actively licensed contractor as they renew. A status that changes and later reverts (e.g. suspended, then reinstated, then suspended again for a new violation) bills every genuine transition, not just the first one — never silently deduplicated against an earlier occurrence of the same status value. - Genuinely new (first found) or status-changed records are pushed to the dataset and billed once each
(
license-status-changed); a check that finds nothing new costs nothing beyond the fixed platform run cost.
Input
{"monitorId": "my-wa-lni-watch","watches": [{ "watchId": "new-sub-acme", "contractorLicenseNumber": "ECOSTSC758NN" }],"notifyOn": "new_alerts","webhookUrl": "https://example.com/webhook"}
Add more contractor licenses later under the same monitorId — each watch keeps its own independent
history. A watchId is permanently bound to the contractorLicenseNumber it first saw; pointing the
same watchId at a different license number later fails the run instead of silently mixing histories.
socrataAppToken is optional — data.wa.gov does not require a key for this dataset, but a free Socrata
app token from your own account raises the anonymous request-rate ceiling if you run many watches across
many monitors.
Output row (per change)
watchId, licenseNumber, changeType ("new"|"status_changed"), status, previousStatus, statusCode, businessName, licenseTypeCode, licenseTypeDesc, address1, address2, city, state, zip, phoneNumber, licenseEffectiveDate, licenseExpirationDate, businessTypeCode, businessTypeDesc, specialtyCode1, specialtyCode1Desc, specialtyCode2, specialtyCode2Desc, ubi, primaryPrincipalName, suspendDate, monitorId, runId, discoveredAt, eventId, billedBilling
Pay-per-event: license-status-changed — charged only for a watch's first found record (baseline is
free) or a genuine status transition since the previous check. Failed/blocked checks are never charged.
Delivery/billing guarantee: at-most-once, not exactly-once
The right to perform an irreversible action (dataset write + PPE charge) is granted by the only atomic
primitive Apify offers — RequestQueue.addRequest(uniqueKey) → wasAlreadyPresent — in a separate
named queue that acts as a permanent journal (<prefix>-<monitorId>-claims). The Apify Key-Value Store
has no CAS, no conditional write and no ETag, so it can only ever be a diagnostic state machine, never
the source of at-most-once.
Concretely: for one computed event, delivery and charging each happen no more than once. If the run
dies after taking the claim, the event may be lost (it stays dataset_unknown/charge_unknown
and is never re-delivered) — but you will never be billed twice. That is a deliberate trade: "never
overcharge" beats "never lose a row".
Boundaries of the guarantee, stated honestly:
- Between the internal lease check and the dataset write/charge there is an unavoidable TOCTOU gap; what actually protects your money is the claim gate, not the lease.
- The guarantee holds for as long as the named claims queue exists. Anyone with account access can delete or recreate it via Console/API, which starts the journal from zero. This is a boundary of any durable storage, not a defect of the protocol.
- The guarantee applies from the build in which the claim gate was introduced onward. Older builds must
not keep running against the same
monitorId.
Important — read before relying on this for any hiring/contracting/insurance decision
This registry is a publication of license status, not a certification of good standing or a complete enforcement record. L&I maintains its own authoritative bond/insurance/complaint records; the public dataset reflects what L&I has published, which can lag real-world enforcement events. This actor is an informational monitor of CHANGES to that public publication — it is NOT a current "licensed to contract" certification, NOT a complaint-history lookup, and NOT legal advice. Always confirm directly at secure.lni.wa.gov/verify/ (or with L&I directly) before acting on any single entry — especially for hiring a subcontractor, homeowner contracting decisions, insurance underwriting, or bonding/due-diligence decisions.
Honest limits
- The durable dataset is a delivery-attempt log, not a guaranteed mirror of the default dataset.
Each row is written to the durable dataset first, then mirrored to the run's default dataset before
billing proceeds for that row. If the durable write succeeds but the default-dataset mirror write
fails (e.g. transient Apify storage error), the item is marked
dataset_unknown, billing for it is permanently blocked (fail-closed — we never charge for a row we can't confirm was delivered), and the run is not retried into re-creating that exact row. The durable dataset can therefore end up with a small number of orphan rows that were never mirrored and never billed. The default dataset is the canonical log of rows successfully written to this run's output (see itsrun_summaryrow) — but a default-dataset row does not by itself prove the row was billed: the row is written beforeActor.charge()runs, so if charging then fails or comes backcharge_unknown, the row is present but not confirmably paid.run_summary.eventsBilledand Apify's own billing ledger are the source of truth for confirmed payment, not the presence of a row in either dataset. - One watch = one contractor license, one request. There is no bulk/roster mode — to track several subcontractors, add one watch per license (up to 30 per run). This keeps the network cost fixed and predictable regardless of the registry's total size (161k+ rows), and keeps each license's history independently auditable.
- A "not found" result on the very first check of a watch is reported honestly, not as an error —
coverage.watches[].matched: false. A typo'dcontractorLicenseNumberwill simply never match; it costs nothing and is safe to correct and retry under the samewatchId. - A license number that WAS found on a previous check but is NOT found on a later one is treated as
source_access_limitedfor that watch this run — no baseline/history update, no billing (two-strike: the history is never rewritten on a single disappearance; the very next run either finds the record again or reportssource_access_limitedagain, indefinitely, until it is found). Unlike some sibling actors in this family (e.g. TX TREC, where an explicitDeletedstatus value is documented and confirmed live), this source does not document its behavior on a genuine record removal — so a disappearance is always treated as an honest signal of distrust in this run's snapshot, never silently reinterpreted as "the license was deleted from the registry". - Billing tracks only
contractorlicensestatus/statuscode, deliberately excludinglicenseexpirationdate(which shifts routinely on every renewal for every actively licensed contractor) and address/contact/principal-name fields (which can change administratively — a move, a new phone number — without any change to license status). Those fields are still delivered in every row for context. - We don't invent data: if the API ever returns something other than a bare JSON array, more than one
row for a single
contractorlicensenumber(violating the dataset's own uniqueness contract), or a row missingcontractorlicensenumber/contractorlicensestatus/statuscode, the run reports it honestly (source_access_limited) instead of guessing.
Author: OmniCoder (https://t.me/OmniCoder)