How we shipped six jurisdictions of live license verification in one session
By Netanel Presman, General Contractor (CSLB #1105249) · Published · 5 min read · Wave 181
Summary
Wave 181 added live license verification for California, Oregon, Washington, New York City Home Improvement Contractor registrations, Indiana, and Quebec RBQ in one commit. Each board returned a different schema, so the registry hides per-board quirks behind a single interface and degrades gracefully when a regulator API is offline.
Article body
Most contractor platforms promise "verified pros" the way a doorman promises "discretion." Nobody checks. On Angi, the badge means the contractor paid. On Thumbtack, it means the profile form said they were licensed. On AskBaily, it means we just asked the regulator, right now, in front of you, and either the regulator said yes or we show you the error.
Wave 181 extended that live-query rail from one jurisdiction to six: California CSLB, Oregon CCB, Washington L&I, New York City Department of Consumer and Worker Protection (Home Improvement Contractor), Indiana PLA, and the Regie du batiment du Quebec. One commit, one session, six boards, 597 tests green on the licensing suite.
The reason this is not a press release is that the work was almost entirely about absorbing the boards' differences without exposing them to the homeowner or the contractor filling out /for-pros. Each board publishes in a different schema, returns different field names, uses different status vocabularies, and has a different idea of what "active" means.
Six boards, six schemas
California CSLB returns a clean XML payload with LicenseNumber, BusinessName, ExpirationDate, and a LicenseStatus string ("Active", "Suspended", "Inactive"). Oregon CCB returns JSON with a "status" key whose possible values include strings we had never seen a contractor actually produce in the field ("Pending Bond", "Lapsed in Grace"). Washington L&I returns the business by UBI number and license number separately — you can hold the business entity but not the contractor registration, or vice versa, and the UI has to say which.
NYC's HIC is the outlier. It is not a "license" in the Western US sense — it is a Home Improvement Contractor registration issued through NYC Open Data. The registration is checked against a different authority than the electrical, plumbing, and master plumber licenses issued by DOB. So for NYC we had to decide, as a product question, whether "license verified" meant HIC-registered (what homeowners actually need for a remodel bid) or DOB-licensed for the trade. We chose HIC, and we surface the DOB answer separately on the /for-pros flow for NYC contractors.
Indiana's Professional Licensing Agency returns a database-backed lookup that is the closest to a 1990s IBM green-screen of anything still in production. The field we needed, "License Status Description," was not part of the documented API. We extracted it from the HTML response with a tolerant parser and cached aggressively, because the PLA endpoint is slower than any other board in the set.
Quebec's RBQ is the only non-English board. Status strings come in French ("Valide", "Suspendue", "Annulee"), and the contractor's sub-categories (the RBQ equivalent of CSLB classifications) are their own taxonomy. We added a dedicated refresh script, scripts/refresh-rbq-cache.ts, so the category vocabulary can be re-pulled without touching application code. Full commit manifest is in the engineering changelog; the file list is 18 files, 2,056 lines added, mostly test fixtures.
The registry pattern
The six implementations live at lib/licensing/states/{california,oregon,washington,nycHic,indiana,quebec}.ts. They all export a function with the same signature: accept a normalized LicenseQuery, return a normalized LicenseResult. Everything board-specific — the URL, the auth handshake, the field-name aliasing, the status-vocabulary mapping — lives in the per-state file. The registry at lib/licensing/registry.ts is a dispatch table. Callers do not branch on jurisdiction; they call one function and get back one shape.
That shape has a "verification_mode" discriminator: live, cached, or degraded. The degraded mode exists because boards go down. When they do, we do not silently return cached data and pretend it is live. We return the last-known-good plus a timestamp plus a banner the caller renders as "We couldn't reach {Board} right now — this is cached from 47 minutes ago." Homeowners see the truth. Contractors see the truth. We do not fake the green check.
What Angi and Thumbtack cannot do
Live verification is not a feature Angi cannot add. It is an economic choice they will not make. Their supply side pays them for leads. The lead is valuable precisely because it is routed to seven or more contractors simultaneously. If Angi rejected contractors in real time based on board status, their inventory would drop by a material percentage on any given week — boards flag licenses for non-payment, bond lapses, clerical errors, and disciplinary holds constantly — and their earnings-per-lead math would stop working. Their legal team cannot ship it because the public schema would expose how many of their "verified pros" are flagged right now.
AskBaily has the opposite economic model: we route a single scope to one contractor, we take a flat per-project fee from the contractor on closed-won, and we do not resell the lead. Live verification protects the homeowner and the contractor both. If the contractor's license lapses between bid and close, we catch it before the contract signs. That reduces disputes, which reduces refunds, which is why we can afford the verification rail in the first place.
Tests, SHAs, and what happens next
The commit manifest is one entry: fdc9cbb3bf6c39c1925c1e495ba1a0dc705dc1f2, Wave 181 license verifier 6-jurisdiction expansion. Eighteen files changed, 2,056 lines added (most of it test fixtures with real recorded board responses so the suite runs offline), 168 lines removed. 597 tests green on the licensing module suite, including 141 tests each for Oregon and Washington, 143 for NYC HIC, 137 for Quebec, and 162 for Indiana.
Wave 187 layered the same verifier behind per-city /for-pros pages so contractors in Portland, Seattle, NYC, Quebec City, and Indianapolis can complete the application without a human in the loop. Wave 188 wired the verifiers into /tools/contractor-check, the homeowner-facing self-verify tool, and pushed six per-state how-to guides under /guides/. Wave 196 dropped the LicenseCard universal embed, which calls the same rail from every LA spoke page to render a live badge next to a price card. Wave 207 published the CC-BY-4.0 coverage dataset so anyone else in the industry can audit which boards we support and which we do not yet.
Jurisdictions we do not yet verify are labeled "coverage pending" in the same dataset. We do not pretend to verify boards we cannot reach. That, more than any of the per-board implementation detail, is the moat.
Sources & references
Commit attestation
- fdc9cbb3bf6c39c1925c1e495ba1a0dc705dc1f2
- Tests green
- 597
- Files changed
- 18
- Lines added
- 2,056
- Waves
- 181
- Author
- netanel
Commit SHAs are from the AskBaily private repository. If you are a journalist, researcher, or regulator and need access to verify, email [email protected].
Frequently asked
- How often do you re-verify a contractor's license?
- Every time the LicenseCard renders or a contractor submits /for-pros, we query the board live. If the board is reachable, the badge is live. If not, we show cached status with an explicit timestamp and degraded banner.
- Why is NYC HIC separate from DOB trade licenses?
- HIC is a DCWP consumer-protection registration for any home-improvement work; DOB licenses are trade-specific (electrical, plumbing, master plumber). A homeowner hiring a remodel GC needs HIC. A plumber working on the project also needs the trade license. We surface both, separately.
- What happens when a board API is offline?
- The result carries a verification_mode of 'degraded' with the last-known-good timestamp. The UI renders 'We couldn't reach {Board} right now; cached from {X} minutes ago.' We never return a green check on stale data.