licenscheck

API docs

The licenscheck API

Contractor license records as reported by the state licensing boards, with each contractor's licenses linked across states. One base URL, JSON everywhere, no SDK needed.

https://api.licenscheck.com

Using Postman or generating a client? Download the OpenAPI spec, which describes every endpoint and field on this page. New here? Watch the 60-second demo.

Authentication

Search (10 a day, or 50 with a free key) and license records need no key. The verdict, linked licenses and monitoring need a key on a paid plan (Starter or API), sent as a Bearer token:

Authorization: Bearer lc_live_…

Keys are shown once when issued, and we store only a hash of each, so keep yours somewhere safe. Get a free key by email (search only, 50 searches a day), or a paid plan on the pricing page. A free key gets a 403 from the verdict, linked licenses and monitoring; the sample report shows what they return. To see the full verdict while browsing licenscheck.com, choose Have a key? Use it here on any locked section: the key is checked, then saved in that browser only (Sign out in the header forgets it), and each license you open counts as a verification, as it would through the API. A wrong or revoked key is refused with a 401 on every endpoint, even the free ones, so you notice a problem right away.

Rate limits

CallerLimit
API plan key300 requests a minute and 50,000 a day (UTC), per key. No daily search limit.
Starter plan key60 requests a minute and 5,000 a day (UTC), per key. No daily search limit.
Free key50 searches a day (UTC), per key.
Without a key10 searches a day (UTC), per visitor.

A search is one GET /licenses request that returns licenses, so each page of results counts; a search that finds nothing doesn't, and neither does opening a license record. Without a paid key, every search response carries X-Searches-Remaining: the searches left today.

Over a limit, the API answers 429 with a Retry-After header saying how many seconds to wait (until midnight UTC, once the day's searches are used up). Refused requests don't count against you, so a client that waits and retries gets straight back in.

Errors

Errors are JSON with a detail field saying what went wrong.

StatusMeaning
401The endpoint needs a key, or the key sent is wrong or revoked.
403The key is a free key, which doesn't include this endpoint.
404No license has that id.
409A limit on what a key can hold: its plan's watched contractors, or 3 webhooks.
422A parameter is invalid, e.g. a search term under 3 characters, one without 3 letters or digits in a row, or an unknown state; detail says which.
429Rate limit or today's searches reached; retry after Retry-After seconds.

What each outcome returns

What to expect for the cases you'll wire into onboarding. Statuses are the board's own; the verdict judges dates itself, so a license its board still calls active after its expiration date reads Risk.

CaseWhat the license record showsVerdict
Active and currentstatus active, expiration_date in the future (or null where the board publishes none)Clear, unless another check warns
Expires within 90 daysstatus active, expiration_date within 90 daysReview
Expiredexpiration_date in the past, whatever the statusRisk found
Suspended or revokedstatus suspended or revokedRisk found
Inactive, on probation, or expired with no datestatus inactive, probation, or expired without a past expiration_dateReview
Dropped off the board's listdelisted_at set; status is the last one the board reportedRisk found
Fine here, suspended in another stateThis record is clean; the linked license shows in linked licensesReview (Risk found if the linked one is revoked, unless it's a related_company: a different business sharing an address or phone)
Board warning on an active licenseboard_alerts, e.g. pending_suspension or insurance_lapsedReview
Active license, but the company behind it is dissolvedbusiness_registration.standing dissolved (Colorado so far: dissolved, revoked or withdrawn with the Secretary of State)Review

No match

A search that finds nothing is a 200 with "results": [] and "total": 0, and doesn't use up a free search. We never return a near-miss as a match: a search finds a name only when every word you send is in it, and a batch row we can't match to exactly one contractor comes back not_found or ambiguous, not guessed. An unknown license id is a 404.

When a board is down

We don't call the boards live while you wait: each board's full list is read daily and stored, so a board's site going down never makes a request fail or time out. A record we couldn't refresh keeps its last status, and last_verified_at says when we last read it, so you can decide how old is too old. A board file that looks wrong (an error page, or far fewer licenses than the day before) is refused rather than loaded, so an outage never marks licenses as delisted, and monitoring sends no events for it.

Get a license

GET/licenses/{id}Free · no key

One license's board record: its status, dates, bond, address, phone and principal as the board reports them, plus facts only some boards publish, such as project limits and insurance.

NameTypeDescription
idpath, UUIDThe license's id, from a search result.

Request

curl https://api.licenscheck.com/licenses/3a5cdcae-1af7-5c92-89d2-46fc2e0722af

Response

{
  "id": "3a5cdcae-1af7-5c92-89d2-46fc2e0722af",
  "contractor_name": "SOUTHERN FOLGER CONTRACTING INC",
  "business_name": null,
  "license_number": "1066907",
  "license_type": "specialty_contractor",
  "state": "CA",
  "board": "CA_CSLB",
  "status": "active",
  "issue_date": "2020-07-13",
  "expiration_date": "2028-07-31",
  "bond_number": "100504856",
  "bond_amount": 25000.0,
  "address": "PO BOX 58, NEWCASTLE, CA, 95658",
  "phone": "9169006593",
  "state_business_id": null,
  "principal_name": null,
  "board_alerts": [],
  "entity_type": "corporation",
  "board_details": [
    {"key": "bond_surety", "label": "Bond issued by", "value": "AMERICAN CONTRACTORS INDEMNITY COMPANY"},
    {"key": "workers_comp", "label": "Workers' comp", "value": "Insured with STARNET INSURANCE COMPANY"}
  ],
  "last_verified_at": "2026-09-27T10:37:45.112910Z",
  "delisted_at": null,
  "created_at": "2026-09-15T00:00:26.940784Z"
}

board is the licensing body that issued the license. A state can have more than one: New York licenses come from NY_DOL (the State Dept. of Labor's public-work registry and mold and elevator licenses), NYC_DOB (New York City's Dept. of Buildings) or NYC_DCWP (New York City's Dept. of Consumer and Worker Protection, for home improvement contractors).

delisted_at is set when the board stops listing a license; status is then the last one it reported. last_verified_at is when we last saw the license in the board's data.

board_alerts lists what the board reports beyond the status: the warning signs a status of "active" hides. Each has a code, the date it's on when the board gives one, and the board's own wording or case number in detail:

  • pending_suspension: a suspension is coming, e.g. for lapsed workers' comp
  • pending_case: an open case, citation or judgment against the license
  • insurance_lapsed: required insurance isn't in force
  • bond_cancelling: the surety has filed a cancellation of the license bond, taking effect on that date. In California, most such licenses were suspended within days of the date
  • bond_lapsed: the license bond has expired, is unpaid, or its cancellation has taken effect
  • enforcement_action: the board reports an enforcement action
  • pending_class_removal: a trade classification is being removed
  • past_discipline: a disciplinary record is on file, though the license is in good standing
  • debarred: the business is barred from public work until on, or with no end date
  • unpaid_wage_assessment: the business owes wages or penalties the labor department assessed

It's empty for most licenses. Every alert except past_discipline makes the verdict Review, on this license or any license linked to it; past_discipline is shown as a note and leaves the verdict as it is.

entity_type is the business's legal form as the board records it: sole_proprietor, llc, corporation, partnership, limited_partnership, llp, joint_venture, association or trust, or null when the board doesn't say.

board_details lists facts a board publishes that not every board does, in display order. Each has a stable key for code, a label for people, and a plain-text value. They never change the verdict. New keys may be added:

  • license_class and project_limit: the license's size class and the largest project it allows, e.g. Virginia's "Class B: under $150,000 a project, under $1,000,000 a year", or an Arkansas commercial license's bid limit
  • bond_surety: the company that issued the license bond
  • liability_insurance: the liability policy's limit and carrier
  • workers_comp: workers' comp coverage, e.g. the insurer, or exempt with no employees
  • status_since: when the license entered its current status, e.g. the suspension date
  • temporary_license, renewal_in_progress: a temporary license, or a lapsed license being renewed

Classifications

GET/licenses/{id}/classificationsFree · no key

Every trade classification held under a license, primary first. Some boards list several per license.

NameTypeDescription
idpath, UUIDThe license's id, from a search result.

Request

curl https://api.licenscheck.com/licenses/3a7580f5-6ec6-569a-b302-b3b937c673fe/classifications

Response

{
  "license_id": "3a7580f5-6ec6-569a-b302-b3b937c673fe",
  "classifications": [
    {
      "id": "4b335aed-69c6-5642-901b-cfe6fa27d063",
      "classification_code": "B",
      "license_type": "building_contractor",
      "is_primary": true,
      "delisted_at": null
    },
    {
      "id": "5a294f8d-763c-5237-bb75-2229304af229",
      "classification_code": "C39",
      "license_type": "roofing_contractor",
      "is_primary": false,
      "delisted_at": null
    }
  ]
}

Continuing education

GET/licenses/{id}/continuing-educationFree · no key

Continuing-education courses completed under a license, most recent first, where the board publishes them.

NameTypeDescription
idpath, UUIDThe license's id, from a search result.

Request

curl https://api.licenscheck.com/licenses/67d66956-e16e-4bfb-85f1-02d10a9e58d6/continuing-education

Response

{
  "license_id": "67d66956-e16e-4bfb-85f1-02d10a9e58d6",
  "records": [
    {
      "id": "471de7a2-f968-574c-8fef-bcc82019d5b2",
      "course_number": "0614919",
      "course_name": "DRYWALL REPAIR AND REPLACEMENT",
      "hours": 1.0,
      "completed_date": "2026-09-04",
      "delisted_at": null
    },
    {
      "id": "ec464044-c82a-596a-a00e-cbe2fa3f0bc0",
      "course_number": "0615028",
      "course_name": "OPTION FOR JOINING MATERIALS",
      "hours": 1.0,
      "completed_date": "2026-09-04",
      "delisted_at": null
    },
    …
  ]
}

Verdict

GET/licenses/{id}/verdictPaid plan

Verify a license: Clear, Review or Risk found, weighing its own status, expiration and bond, the board's alerts (pending suspensions, open cases, lapsed insurance or bonds) on it and every license linked to the same contractor, and its licensing history, including any past discipline.

NameTypeDescription
idpath, UUIDThe license's id, from a search result.
as_ofquery, optionalVerify the license as it stood at a past moment instead of now: a date (2026-10-04, meaning the end of that day, UTC) or an ISO 8601 timestamp. Our record of changes began on October 4, 2026.

Request

curl -H "Authorization: Bearer $LICENSCHECK_KEY" \
  https://api.licenscheck.com/licenses/3a5cdcae-1af7-5c92-89d2-46fc2e0722af/verdict

Response

{
  "license_id": "3a5cdcae-1af7-5c92-89d2-46fc2e0722af",
  "level": "review",
  "word": "Review",
  "summary": "Active in California, but a linked Washington license is suspended.",
  "checks": [
    {
      "label": "Board status",
      "outcome": "pass",
      "text": "Active as reported by the CA board.",
      "segments": [
        { "text": "Active", "strong": true },
        { "text": " as reported by the CA board.", "strong": false }
      ]
    },
    { "label": "Expiration", "outcome": "pass",
      "text": "Valid through Jul 31, 2028 (in 1.8 yr).", "segments": […] },
    { "label": "Surety bond", "outcome": "pass",
      "text": "$25,000 bond on file (#100504856).", "segments": […] },
    { "label": "Linked licenses", "outcome": "warn",
      "text": "Suspended in Washington: SOUTHERN FOLGER CONTRNG INC, linked to this contractor.
               1 linked license in WA. Check whether it affects this job.", "segments": […] },
    { "label": "Licensing history", "outcome": "info",
      "text": "First issued Jul 13, 2020 (6.2 yr ago).", "segments": […] }
  ]
}

level is clear, review or risk: any failed check makes it Risk, otherwise any warning makes it Review. This license's own suspension or revocation, or a revoked license linked to the same contractor, fails; a linked license that's suspended or no longer listed warns, since that's usually one license's lapsed bond or insurance. Each check's outcome is pass, warn, fail or info. text is plain text; segments is the same text with the words to emphasize marked, for your own UI.

As of a past date

Add as_of to answer "was this license good on the day we funded?": the same verdict, computed from the license as its board's record stood then, and its linked licenses as they stood then. The licenses linked to the contractor are today's (links aren't kept over time), and unconfirmed possible matches are left out. as_of in the response says what the answer rests on: the last change we recorded to this license on or before that moment, and the board file it came from.

curl -H "Authorization: Bearer $LICENSCHECK_KEY" \
  "https://api.licenscheck.com/licenses/3a5cdcae-1af7-5c92-89d2-46fc2e0722af/verdict?as_of=2026-10-04"
{
  "license_id": "3a5cdcae-1af7-5c92-89d2-46fc2e0722af",
  "level": "review",
  …,
  "as_of": {
    "at": "2026-10-04T23:59:59.999999Z",
    "record_changed_at": "2026-10-04T10:02:41Z",
    "record_source_file": "gs://licens-data-raw-01679/CA/2026-10-04/licenses.csv.gz",
    "history_since": "2026-10-04T05:51:00Z"
  }
}

A date before our record began, or in the future, is a 422; a date before the license first appeared on its board's list is a 404. A change is dated when we read the board's file (daily), not when the board made it.

Linked licenses

GET/licenses/{id}/graphPaid plan

Every other license linked to the same contractor, with how strong each link is and the evidence behind it.

NameTypeDescription
idpath, UUIDThe license's id, from a search result.

Request

curl -H "Authorization: Bearer $LICENSCHECK_KEY" \
  https://api.licenscheck.com/licenses/3a5cdcae-1af7-5c92-89d2-46fc2e0722af/graph

Response

{
  "license_id": "3a5cdcae-1af7-5c92-89d2-46fc2e0722af",
  "linked_licenses": [
    {
      "license": {
        "id": "78f68a42-ef6d-52c9-9680-5b024a87e26c",
        "contractor_name": "SOUTHERN FOLGER CONTRNG INC",
        "license_number": "SOUTHFC804L5",
        "state": "WA",
        "board": "WA_LNI",
        "status": "suspended",
        …
      },
      "confidence": 0.99,
      "direct_edge_score": 0.99,
      "evidence": [
        "same_phone",
        "same_name"
      ],
      "related_company": false
    }
  ],
  "possible_matches": [],
  "possible_match_count": 0
}
  • confidence: the license's strongest link into the group; direct_edge_score: its direct match with the queried license, or null when it's linked only through other licenses.
  • evidence: same_address, same_phone, same_email, same_business_id, same_principal, same_name, and reviewed when a person at licenscheck confirmed the two are one contractor. A shared email links two licenses only when their names match too.
  • related_company: true for a different business linked to this contractor by a shared address or phone (a sister company, franchise or successor), not the same contractor under another name. Its problems can raise the verdict to Review, never to Risk.
  • possible_matches: up to 10 weaker matches that aren't treated as the same contractor, each with a score, evidence, and worth_review. possible_match_count is how many there are in all.

Stats

GET/statsFree · no key

How many licenses are on file. licenses_is_exact is false when the count is the database's fast estimate.

Request

curl https://api.licenscheck.com/stats

Response

{
  "licenses": 541624,
  "licenses_is_exact": false
}

Account

GET/accountFree · any key

Your key's plan and what it has used: verifications this UTC month against the plan's included ones, the contractors you watch against its limit, and on a free key, the searches left today. Reading it uses no verification and no search. The dashboard is built on it.

Request

curl -H "Authorization: Bearer $LICENSCHECK_KEY" https://api.licenscheck.com/account

Response

{
  "plan": "starter",
  "verifications_used": 61,
  "verifications_included": 100,
  "overage_cents": 35,
  "month_resets_on": "2026-11-01",
  "watches_used": 7,
  "watches_included": 10,
  "searches_left_today": null,
  "searches_per_day": null
}

verifications_included and watches_included are null on a plan without a cap. searches_left_today and searches_per_day are null on a paid plan, which has no daily search limit.

Watch a contractor

POST/monitoring/watchesPaid plan

Watching a license watches the whole contractor: that license and every license linked to it, in any state, including ones linked later. Its verdict and licenses are recorded when you start, and from then on every change that matters becomes an event.

What it costs. Monitoring comes with the paid plans: Starter watches up to 10 contractors at once, API up to 50 (talk to us for more). Starting a watch uses one verification, exactly like GET /licenses/{id}/verdict, since the response tells you the contractor's verdict: it comes out of your plan's included verifications, then costs the plan's per-verification price. Verifying a license and starting a watch on it the same UTC day count once. After that, every event for the contractor is included, for as long as you watch it; stopping a watch frees its place.

NameTypeDescription
license_idbody, UUIDAny license of the contractor, from a search result.
referencebody, string, optionalYour own label for the watch, 1-200 characters, e.g. your client's and sub's ids. It comes back on the watch and on every event and webhook delivery for it.

Request

curl -X POST -H "Authorization: Bearer $LICENSCHECK_KEY" \
  -H "Content-Type: application/json" \
  -d '{"license_id": "3a5cdcae-1af7-5c92-89d2-46fc2e0722af", "reference": "client-42/sub-981"}' \
  https://api.licenscheck.com/monitoring/watches

Response

{
  "id": "070f19f2-f820-4c56-bc2a-1e3bf30bbcbd",
  "license_id": "3a5cdcae-1af7-5c92-89d2-46fc2e0722af",
  "contractor_name": "SOUTHERN FOLGER CONTRACTING INC",
  "state": "CA",
  "board": "CA_CSLB",
  "license_number": "1066907",
  "reference": "client-42/sub-981",
  "linked_states": ["AL", "AR", "AZ", "FL", "MN", "NE", "NV", "NY", "OR", "TN", "WA", "WV"],
  "created_at": "2026-10-05T00:52:38Z",
  "verdict_level": "clear",
  "linked_licenses": 12,
  "checked_at": "2026-10-05T00:52:38Z"
}

A new watch is a 201; watching the same license again with the same reference (or none) returns the same watch with a 200 and uses no verification. The same license under a different reference is a separate watch, so a platform can watch one sub for each client that uses it; each watch takes a place in your plan's limit. At that limit, a new watch is a 409 and isn't charged. GET /monitoring/watches lists your watches (limit, offset, and reference to list only one reference's) with each one's licensee, state, number, reference, the states of its linked licenses and its verdict at the last check, plus total (how many match) and included (how many your plan allows), and DELETE /monitoring/watches/{id} stops one; events already sent stay in the feed.

Events

GET/monitoring/eventsPaid plan

What changed for the contractors you watch, oldest first. We check about every 5 minutes after each board's file comes in, and links between licenses are rebuilt nightly. An event is written when a watched contractor's verdict or one of its checks changes, when any of its licenses changes status, is delisted or relisted, or changes expiration date, bond or board alerts, and when a license is linked to it or unlinked. Address, phone and name edits don't send an event.

NameTypeDescription
afterquery, integerThe last event id you've read; 0 for the oldest.
limitquery, 1-100Max events to return (default 50).
orderquery, asc or descasc (the default) returns the oldest events after after, for polling; desc returns the newest limit of them, newest first, for showing recent changes.
referencequery, stringOnly events of watches with exactly this reference.

Request

curl -H "Authorization: Bearer $LICENSCHECK_KEY" \
  "https://api.licenscheck.com/monitoring/events?after=0"

Response

{
  "events": [
    {
      "id": 1,
      "type": "watch.changed",
      "watch_id": "5b0e…",
      "license_id": "d9617fd5-b25b-554a-917f-ff38f92c7a0f",
      "reference": "client-42/sub-981",
      "created_at": "2026-10-05T01:05:26Z",
      "data": {
        "verdict": {
          "before": "risk",
          "after": "risk",
          "summary": "Expired in Texas, but a linked Texas license is no longer listed by its board."
        },
        "checks_changed": [],
        "changes": [
          {
            "license_id": "c1e6aac6-e02d-56df-a94c-31147047dec0",
            "state": "TX",
            "board": "TX_TDLR",
            "license_number": "TECL39012",
            "name": "THIRD COAST ELECTRIC",
            "field": "delisted",
            "before": false,
            "after": true,
            "changed_at": "2026-10-05T01:05:14Z",
            "source_file": "gs://licens-data-raw-01679/TX/2026-10-05/licenses.csv.gz"
          }
        ],
        "links_added": [],
        "links_removed": [],
        "possible_matches_added": [],
        "possible_matches_removed": []
      }
    }
  ],
  "next_after": 1
}

Pass next_after as after next time to get only new events; nothing is lost between polls (with order=desc, more than limit new events between calls would skip some, so poll in the default order). In changes, field is status, delisted, expiration_date, bond_number, bond_amount or board_alerts, and source_file is the board file the change came from. A change can be on a license other than the one you watched: here, a linked Texas license was delisted. possible_matches_added and possible_matches_removed name unconfirmed matches worth a look (a similar license that's suspended, revoked or no longer listed, or one sharing an address, phone or business id) as they appear or go, with their status and match score.

Webhooks

POST/monitoring/webhooksPaid plan

Have each event POSTed to your server as it's written: the same JSON as one item of the events feed. The URL must be https on the public internet; a key can have 3.

Request

curl -X POST -H "Authorization: Bearer $LICENSCHECK_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/licenscheck-events"}' \
  https://api.licenscheck.com/monitoring/webhooks

Response

{
  "id": "a84ee8e0-a476-4c2c-9aca-fec0b3f52a1e",
  "url": "https://example.com/licenscheck-events",
  "created_at": "2026-10-05T01:20:00Z",
  "last_success_at": null,
  "secret": "whsec_3f9a…"
}

secret is shown only in this response: store it to verify deliveries. Answer each delivery with any 2xx within 10 seconds. Otherwise we retry after 5 and 30 minutes, then 2, 6 and 24 hours; a webhook that hasn't answered with a 2xx for 3 days is turned off. The events feed has every event either way, so after an outage you can catch up from it. GET /monitoring/webhooks lists yours, DELETE /monitoring/webhooks/{id} removes one, and POST /monitoring/webhooks/{id}/test sends a signed webhook.test delivery right away and tells you how your server answered.

Verifying webhook signatures

Every delivery carries Licenscheck-Event-Id (the event's id: deliveries can repeat, so skip one you've already handled) and Licenscheck-Signature: t=<unix time>,v1=<signature>. The signature is the hex HMAC-SHA256 of <t>.<raw body> under your webhook's secret. Check it against the raw body, before parsing it, and refuse a t more than 5 minutes old.

Python

import hashlib, hmac, time

def verify(secret: str, header: str, body: bytes, tolerance: int = 300) -> bool:
    """Check a Licenscheck-Signature header against the raw request body."""
    parts = dict(item.split("=", 1) for item in header.split(","))
    timestamp = int(parts["t"])
    if abs(time.time() - timestamp) > tolerance:
        return False  # too old: a replay
    expected = hmac.new(secret.encode(), f"{timestamp}.".encode() + body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts["v1"])

Node

const crypto = require("crypto");

function verify(secret, header, rawBody, tolerance = 300) {
  const parts = Object.fromEntries(header.split(",").map((item) => item.split("=")));
  const timestamp = Number(parts.t);
  if (Math.abs(Date.now() / 1000 - timestamp) > tolerance) return false; // too old: a replay
  const expected = crypto.createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}

Verify a spreadsheet

POST/batchesPaid plan

Send a CSV of contractors (up to 1,000 rows) and get a verdict for every row. It works in three steps, and nothing is charged until you run it. The website does the same steps for you.

  1. Upload. POST /batches?filename=subs.csv with the file as the request body (Content-Type: text/csv, UTF-8). You get back a draft: its columns, what each one is read as (license_number, name, state, dba, phone, address or keep, guessed from the headers; every street, city and ZIP column is address), how many rows match, and cost, the most running it can add to your bill.
  2. Check. If a column was read wrong, POST /batches/{id}/preview with {"roles": [...]}, one role per column, and get the new matches and cost.
  3. Run. POST /batches/{id}/run with the same roles. It usually finishes within a minute: poll GET /batches/{id} until status is done.

Request

curl -X POST -H "Authorization: Bearer $LICENSCHECK_KEY" \
  -H "Content-Type: text/csv" --data-binary @subs.csv \
  "https://api.licenscheck.com/batches?filename=subs.csv"

How rows match

A row is matched by its license number (with its state, when given) or by its exact name, ignoring punctuation, case and legal endings. Its DBA column, and either side of a name like "Brothers Plumbing Corp dba Mr. Rooter", are tried as names too. A phone or address never finds a license on its own: it only chooses between contractors that share the row's name (the one at the same phone or street address, or in the same city when every other one is listed elsewhere), and confirms a close name, such as a typo, at the same phone or street address. Each row ends in one of four ways:

  • matched: one license, or several licenses of one linked contractor (we verify its active one). It's verified, for one verification.
  • ambiguous: several contractors fit, for example a common name in one state, and the row's phone and address don't pick one out. We never guess. The row lists up to 3 candidates, and POST /batches/{id}/rows/{row}/resolve with {"license_id": ...} verifies the one you pick.
  • not_found: nothing matches. When a name is close to others, they're offered as candidates.
  • invalid: no license number and no name, or a state we can't read.

Only matched rows are charged, one verification per license, and a license you already verified that UTC day costs nothing again.

Results

GET /batches/{id}/rows?view=flagged (Risk and Review), clear, help (ambiguous, not found and invalid) or all pages through the rows: your values by column, the match, and each verified row's result (verdict, summary, failing checks, linked licenses and their states). GET /batches/{id}/results.csv is your file back with those results added as columns. Any cell that would start a spreadsheet formula (=, +, -, @) gets a leading apostrophe. POST /batches/{id}/watch with {"rows": [...]} starts watches on those rows' contractors, up to your plan's limit. GET /batches lists your batches.

Limits

A file can be up to 2 MB, 1,000 rows, 50 columns and 500 characters a cell, in UTF-8 (Excel: "CSV UTF-8"). A key can run 5 batches a day. We keep only the cell values, never the file, and delete them 90 days after the upload, or after a day if the batch never runs.

Issue a proof report

POST/reportsPaid plan

A PDF of a license's verdict, its record and its linked licenses as they stood at a moment, for a loan file or a contract record. It carries a code and a QR code linking to a public page where anyone can confirm the copy they hold is the one we issued. Issuing one uses one verification of the license (none if you already verified it that UTC day).

NameTypeDescription
license_idbody, UUIDThe license to report on.
as_ofbody, stringOptional: an ISO 8601 timestamp (no zone means UTC) or a date (its end, UTC). Omit for now. Our record of changes began 2026-10-04 05:51 UTC, so earlier moments are a 422.

Request

curl -X POST -H "Authorization: Bearer $LICENSCHECK_KEY" \
  -H "Content-Type: application/json" \
  -d '{"license_id": "3a5cdcae-1af7-5c92-89d2-46fc2e0722af", "as_of": "2026-10-08T13:00:00Z"}' \
  https://api.licenscheck.com/reports

Response

{
  "id": "a642c242-673b-4f82-85d3-15ccda5d7c8f",
  "code": "5YTQK-Y3X9H",
  "license_id": "3a5cdcae-1af7-5c92-89d2-46fc2e0722af",
  "contractor_name": "SOUTHERN FOLGER CONTRACTING INC",
  "state": "CA",
  "license_number": "1066907",
  "as_of": "2026-10-08T13:00:00Z",
  "issued_at": "2026-10-11T23:00:13Z",
  "level": "clear",
  "sha256": "2ab1eeacb83cccfaf3ed228e7093f7a074d8119c09fd6c73abf01999fe40dff0",
  "verify_url": "https://licenscheck.com/verify/5YTQK-Y3X9H"
}

GET /reports/{id}.pdf downloads the PDF, byte for byte as issued (we keep it, so its fingerprint always holds), and GET /reports lists your reports. A license that wasn't on its board's list yet at that moment is a 404. Links between licenses are today's, each license shown as it stood at the moment.

Check a report

GET/verify/{code}Free · no key

What a report says, by the code printed on it (any case, dash optional), and the SHA-256 of the PDF we issued. Hash the copy you were given: if it equals sha256, it hasn't been changed. Only what the report prints is returned. The same check runs in the browser at licenscheck.com/verify.

Request

curl https://api.licenscheck.com/verify/5YTQK-Y3X9H

Use with AI agents

MCPhttps://api.licenscheck.com/mcpAPI key

The same verification as the API, as tools an AI agent can call over the Model Context Protocol (Streamable HTTP). Send your key as a Bearer token, as with the API. Agents get the verdict with its reasons, a contractor's licenses in other states, proof reports and monitoring, on your plan's meter: a license checked by an agent and through the API the same UTC day is one verification.

ToolPlanWhat it does
find_contractorAny keyFinds a license by number and/or name and state: one match, several contractors to ask about, or the nearest names. On a free key it uses one of the day's 50 searches.
get_licenseAny keyA license record and its classifications. Free.
check_contractorPaid planThe verdict and its checks, the linked licenses and possible matches worth a look; as_of for a past moment. One verification.
check_contractorsPaid planUp to 100 contractors in one call, saved as a batch. One verification per matched license.
create_proof_reportPaid planA proof report and its public verify link. One verification.
watch_contractor, stop_watching, get_watchlistPaid planMonitoring: start or end a watch, and read the watched contractors and what changed. Starting a watch uses one verification.
get_accountAny keyYour plan and what's left this month. Free.

Claude Code

claude mcp add --transport http licenscheck https://api.licenscheck.com/mcp \
  --header "Authorization: Bearer $LICENSCHECK_KEY"

Cursor, VS Code and other clients

{
  "mcpServers": {
    "licenscheck": {
      "url": "https://api.licenscheck.com/mcp",
      "headers": { "Authorization": "Bearer ${env:LICENSCHECK_KEY}" }
    }
  }
}

VS Code names the top-level key servers. Agent SDKs (Claude, OpenAI, LangChain) take the same URL and header. Without a key, or with a wrong one, /mcp answers 401. Every tool returns structured JSON plus a few lines the agent can quote, with contractor names quoted as data, never as instructions.

About the data

  • Straight from the boards, every day. Each record comes from the state board's own published data, refreshed daily (continuing education weekly), and last_verified_at shows exactly when we last checked it. Status is exactly what the board reports.
  • Every link shows its evidence. We link a contractor's licenses on shared names, addresses, phones and business ids, and the evidence on each link says which, so you can see why.
  • A license's id doesn't change when the board data refreshes, so it's safe to store.
  • For the businesses you hire or work with. Don't use the API to decide whether a person gets credit, insurance, a job or housing: our results aren't consumer reports (see the Terms and Acceptable use).