API Reference
v1 - REST/JSON over HTTPS. Authentication via sk_live_* bearer tokens.
Machine-readable OpenAPI 3.0 description - import it into Postman or Insomnia, or generate a client with openapi-generator. It is built from the same schemas the API validates against, so it matches what the endpoints actually accept.
Permitted use
This API is not a consumer reporting agency and its responses are not consumer reports under the FCRA. Do not use it to decide employment, credit, insurance, housing, or licensing eligibility. Credentialing, payer enrollment, network participation, and compliance monitoring are permitted. Bulk extraction of the roster data, and marketing to contact details found in it, are prohibited. See the Terms of Service.
1. Authentication
Every request must include an Authorization header with your API key.
Authorization: Bearer sk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Generate keys in your dashboard. Each key is shown once at creation - copy it immediately.
2. GET /api/v1/verify
Verify a single license. Cache-backed; returns sub-100ms when cached.
Query parameters
profession(required) - e.g. nurse, physician, pharmacist, therapist, ems, cpa, pe, real_estate, contractor, electrician, plumber, cosmetology, barber. Coverage spans 43 professions and changes as sources are added, so the authoritative list is the coverage matrix orGET /api/v1/verify/coverage- this list is not maintained by hand.state(required) - 2-letter US state code (uppercase or lowercase)license_number(required) - the license number as issued by the state board
Example request
curl -H 'Authorization: Bearer sk_live_xxx' \ 'https://licenseverificationapi.com/api/v1/verify?profession=nurse&state=CA&license_number=12345'
Example response (200)
{
"success": true,
"data": {
"status": "active",
"expires_at": "2027-06-30",
"holder_name": "TAYLOR, LUTE K.",
"license_type": "Professional Engineer",
"source": "tx-board-professional-engineers",
"source_type": "public_csv",
"cache_hit": false,
"cache_age_hours": null,
"last_verified_at": "2026-07-22T18:26:24Z",
"provenance": {
"source": "tx-board-professional-engineers",
"source_url": "https://pels.texas.gov/",
"method": "public_csv",
"data_as_of": "2026-07-22T18:25:50Z",
"retrieved_at": "2026-07-22T18:26:24Z",
"attribution": null
},
"raw": {}
}
}Status values
active- License is in good standing.expired- License is past its expiration date.inactive- Holder is voluntarily inactive per the board.delinquent- Renewal has lapsed per the board.suspended- License is suspended by the board.revoked- License is revoked by the board.not_found- No license matching the (profession, state, number) tuple in the backing source. For roster-backed sources this means "absent from the latest published file" - see theraw.provenancenote.expired_unconfirmed- The expiry date has passed, but it passed after the snapshot we hold, so we never observed the licence lapse. The holder may have renewed in the meantime and often has: boards frequently label these "active in renewal". Treat it as a prompt to check the board directly, not as a finding against the licensee. It is deliberately notactive, so a system that only admitsactiveis unaffected.status_confidence- How much weightstatuscarries.source_statedmeans the board said it.derived_from_expirymeans we concluded it from the expiry date, on a snapshot taken after that date had passed.derived_after_last_refreshmeans the licence lapsed after our last refresh, so a renewal since then would not be visible to us yet. Treat the last one as a prompt to check the board directly rather than as a finding: the licence may well have been renewed on time.discipline- The board's published discipline indicator, where it publishes one. Three-valued:truemeans a disciplinary action is recorded against this holder,falsemeans the board publishes the fact and records none, andnull- the common case - means this source does not publish it at all. Do not treat null as false: it is an absence of evidence, not evidence of absence. It answers a different question fromstatusand is never folded into it, because a credential can be active and flagged - the flag describes history, not present authority. Nevada publishes it today; Alabama, Illinois and Texas publish equivalents we have not yet wired.not_applicable- The credential does not exist in that state, so there is nothing to verify. Most states issue no controlled-substance registration to prescribers at all: the authority is the federal DEA registration plus the professional licence. This is distinct fromnot_found(we hold the register and this number is absent from it) and fromnot_supported(the credential exists and we have not built it) - only this one means you can stop looking. The response carries anapplicabilityobject with the statute or agency text we relied on, and the date we last checked it, so you can verify the claim rather than take our word for it. Check itsscopebefore concluding a number is fake:all_holdersmeans the state registers nobody, whilepractitionersmeans it registers no prescriber but some other class does - andheld_bynames that class, so a number issued to one of them is genuine and simply outside what we check. We do not check DEA registration, and it is deliberately notactive.not_supported- We did not determine a status. Two cases, told apart by theerrorfield: either we hold no verifier for this (profession, state), which is permanent and listed in coverage; or the board returned a status we do not yet interpret for this one licence, which is temporary and worth retrying. We return this rather than guess, because every available status would assert something the board did not say.
Evidence fields
Every verification carries the fields below. They exist so that an answer can go into a credentialing file and be defended months later by someone who was not present when it was made.
record- A signed, citable record of this answer.idis the identifier to quote,payload_sha256the hash of its canonical payload,signatureandkey_idan Ed25519 signature and the key that made it.retrieve_urlfetches the full record later with no account and no API key, andkeys_urlpublishes the keys, so a third party can check the signature without trusting us. Payloads are kept for seven years; the hash, signature and key id are kept indefinitely, so a copy you hold stays verifiable after we have stopped holding ours. Uncertain answers are signed too - a signed "this number matches several people" is evidence that a verification was attempted, and no record can be created after the fact for a call that was never recorded.status_raw- The board's own status string, verbatim, beside our normalizedstatus. A status of active with a status_raw of "Active/Probation" are both true, and only one of them is the finding.restrictedandrestrictions- Whether the board's published status states a restriction, condition, probation or limitation, with its wording carried verbatim. These answeractiveby design: a restricted practitioner usually still holds practice authority, so the restriction rides alongside the status rather than replacing it. Each entry carries a three-valueddisciplinaryflag, because boards publish "Non-Disciplinary Practice Restriction" and its disciplinary counterpart as distinct strings -nullmeans the board did not characterise it and must not be read as discipline.provenance.data_as_of_basis- What KIND of datedata_as_ofis, so a publication date is never mistaken for our own clock.source_statedthe provider reported it;http_last_modifiedthe origin's header for the file;archive_capturewhen an archive took the snapshot;ingest_timeno upstream date exists, so this is when we looked;unspecifieda date recorded before we tracked its kind. Null exactly when the date is null.provenance.source_file_url- The artefact the answer was read from, which for most sources is more specific than the board's homepage: the dataset query, the export, the published file. Null for adapters that read no file.reasonanddetermination_url- Present only on anot_supportedthat will never become supported.issued_not_publishedmeans the jurisdiction does issue the credential and publishes no register we can retrieve, so waiting for us is pointless. The URL gives the statute we relied on, what retrieval we attempted and when, and where to check by hand. Absent on an ordinary not_supported, which means not built yet.GET /api/public/determinationslists every recorded finding.
3. POST /api/v1/verify/bulk
Verify up to 50 licenses in a single request.
curl -H 'Authorization: Bearer sk_live_xxx' \
-H 'Content-Type: application/json' \
-X POST 'https://licenseverificationapi.com/api/v1/verify/bulk' \
-d '{
"items": [
{ "profession": "nurse", "state": "CA", "license_number": "12345" },
{ "profession": "cpa", "state": "TX", "license_number": "ABC-9876" }
]
}'Each item counts independently against your monthly quota.
4. POST /api/v1/verify/batch
Asynchronous batch jobs for large sets (beyond the 50-item synchronous bulk cap). Submit up to your plan's limit (Free 500 / Developer 2,500 / Growth 10,000 / Scale 25,000 / Enterprise 25,000 items), get a batch_id back immediately, then poll for results. Each item bills exactly like a single verification.
# Submit
curl -H 'Authorization: Bearer sk_live_xxx' \
-H 'Content-Type: application/json' \
-X POST 'https://licenseverificationapi.com/api/v1/verify/batch' \
-d '{ "items": [ { "profession": "cosmetology", "state": "TX", "license_number": "100012" }, ... ] }'
# -> 202 { "success": true, "data": { "batch_id": "…", "status": "queued", "status_url": "…" } }
# Poll status
curl -H 'Authorization: Bearer sk_live_xxx' \
'https://licenseverificationapi.com/api/v1/verify/batch/BATCH_ID'
# Fetch results (paginated)
curl -H 'Authorization: Bearer sk_live_xxx' \
'https://licenseverificationapi.com/api/v1/verify/batch/BATCH_ID/results?offset=0&limit=100'Jobs survive server restarts - completed items are never re-billed. Batch status values: queued, processing, completed, failed.
5. GET /api/v1/verify/coverage
Lists every (profession, state) combination supported by a real verifier.
curl -H 'Authorization: Bearer sk_live_xxx' \ 'https://licenseverificationapi.com/api/v1/verify/coverage'
Internal cross-reference adapters appear with state * (wildcard).
6. GET /api/v1/verify/usage
Returns your current calendar-month usage.
curl -H 'Authorization: Bearer sk_live_xxx' \
'https://licenseverificationapi.com/api/v1/verify/usage'
{
"success": true,
"data": {
"used": 423,
"first_request_at": "2026-05-01T10:14:22Z",
"last_request_at": "2026-05-07T19:48:11Z",
"period": "current_calendar_month"
}
}7. License monitors and webhooks
Monitors watch a license continuously (re-verified daily) and notify you when its status changes - built for PCS moves, credential compliance, and roster hygiene. Monitors are plan-gated by count (Free 1 / Developer 50 / Growth 500 / Scale 2,500 / Enterprise 10,000) and do not consume verification quota.
# 1. Register a webhook endpoint (secret is shown ONCE - store it)
curl -H 'Authorization: Bearer sk_live_xxx' -H 'Content-Type: application/json' \
-X POST 'https://licenseverificationapi.com/api/v1/monitors/webhook-endpoints' \
-d '{ "url": "https://example.com/hooks/licenses" }'
# -> { "data": { "id": "…", "secret": "whsec_…", ... } }
# 2. Create a monitor (seeded with an immediate verification)
curl -H 'Authorization: Bearer sk_live_xxx' -H 'Content-Type: application/json' \
-X POST 'https://licenseverificationapi.com/api/v1/monitors' \
-d '{ "profession": "cosmetology", "state": "TX", "license_number": "100012",
"webhook_endpoint_id": "…" }'Webhook delivery format
Status changes POST a JSON body of type license.status_changed withprevious_status, new_status, the license tuple, and the full provenance block. Deliveries are signed:
X-Webhook-Timestamp: 1784145600
X-Webhook-Signature: sha256=<hex HMAC-SHA256 of "timestamp.body" using your whsec_ secret>
// Verify (Node.js):
const expected = 'sha256=' + crypto.createHmac('sha256', WHSEC)
.update(timestamp + '.' + rawBody).digest('hex');
const valid = crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
&& Math.abs(Date.now() / 1000 - Number(timestamp)) < 300;Event types
license.status_changed- the status changed between two daily checks (default).license.exclusion_match- the holder's name newly matches an exclusion list in force. A name match to review, not a finding against the person (default).license.discipline_changed- the board's discipline flag flipped betweentrueandfalse, withprevious_disciplineandnew_discipline. Opt in.monitor.attention_required- a monitor has stopped observing its licence. Opt in.
An endpoint created without events receives the two defaults. To add the others, pass the full list, e.g. "events": ["license.status_changed", "license.exclusion_match", "license.discipline_changed"]. Email notifications cover all four.
Change feed
GET /api/v1/changes returns changes to your monitored licenses in order, with anext_cursor to resume from, so a missed poll loses nothing. Each item'schange_kind is one of new,status_changed, expiry_changed,removed, discipline_changed(with old_discipline / new_discipline) orexclusion_match (with exclusion_lists andexclusion_match_count). Discipline and exclusion items are recorded from 19 October 2026 forward.
Email notifications are on by default (disable with "email_enabled": false). Endpoints that fail 10 consecutive deliveries are disabled automatically; rotate secrets viaPOST /api/v1/monitors/webhook-endpoints/:id/rotate.
8. GET /api/v1/screen
Screen a person or business against 27 federal and state exclusion lists in a single call: the HHS-OIG LEIE, SAM.gov, the CMS revoked-provider file, the OFAC SDN list, the FDA debarment and clinical-investigator disqualification lists, and 21 state Medicaid exclusion lists. Anyone billing Medicare or Medicaid is required to run this check.
One request covers every list - there is no per-list parameter and no extra charge for the breadth. Which lists were queried, how many records each held and how current each one was all ride back in checked_against, so a clear is auditable rather than something you have to take on trust. State Medicaid lists currently cover AL, CA, IL, KY, MD, ME, ND, NJ, NY, OH, PA, TN, TX, VT and WA.
Query parameters
npi- 10-digit NPI. Strongly preferred: it is the only identifier in this dataset precise enough to act on alone.first_name,last_name- for individualsbusiness_name- for entitiesstate- optional, narrows fuzzy candidates only
At least one of npi, last_name or business_name is required. A request with none returns 400 rather than a "clear" result that would mean nothing.
Three results, not two
OIG guidance is that a name match must be resolved with a second identifier - it is neither a finding nor a clear. A boolean would therefore be wrong in both directions, so result is one of:
excluded- matched on NPI. Definitive; act on it.requires_review- a name matched but nothing confirmed identity. This is not a clear.clear- no match at any tier. The only result that lets you proceed unconditionally.
Example
curl "https://licenseverificationapi.com/api/v1/screen?npi=1760461826" \
-H "Authorization: Bearer sk_live_xxx"
{
"success": true,
"data": {
"result": "excluded",
"requires_review": false,
"finding_count": 1,
"truncated": false,
"findings": [{
"match_confidence": "definitive",
"similarity": null,
"entity_type": "individual",
"last_name": "ABAD-SANTOS", "first_name": "CRISELDA",
"npi": "1760461826",
"general": "PHYSICIAN (MD, DO)", "specialty": "PSYCHIATRY",
"cleared": false,
"sources": [
{ "list_source": "oig_leie",
"display_name": "HHS-OIG List of Excluded Individuals/Entities",
"excluding_agency": "HHS-OIG", "exclusion_type": "1128b4",
"exclusion_date": "2025-01-20", "state": "CA", "cleared": false },
{ "list_source": "cms_revoked",
"display_name": "CMS Revoked Medicare Providers and Suppliers",
"excluding_agency": "CMS", "exclusion_type": "424.535(a)(2)",
"exclusion_date": "2025-01-20", "state": "DE", "cleared": false },
{ "list_source": "cms_revoked",
"display_name": "CMS Revoked Medicare Providers and Suppliers",
"excluding_agency": "CMS", "exclusion_type": "424.535(a)(8);424.535(a)(9)",
"exclusion_date": "2024-03-31", "state": "CA", "cleared": false },
{ "list_source": "ca_dhcs_si",
"display_name": "California DHCS Provider Suspended and Ineligible List",
"excluding_agency": "CA DHCS", "exclusion_type": "SUSPENDED AND INELIGIBLE",
"exclusion_date": "2023-08-24", "state": "CA", "cleared": false }
]
}],
"checked_against": {
"list_count": 18,
"total_records": 325621,
"fuzzy_matching": true,
"lists": [
{ "list_source": "sam",
"display_name": "SAM.gov Exclusions (Public Extract V2)",
"records": 168465, "data_as_of": "2026-09-17T13:32:36Z" },
{ "list_source": "oig_leie",
"display_name": "HHS-OIG List of Excluded Individuals/Entities",
"records": 84001, "data_as_of": "2026-09-17T13:32:09Z" }
]
}
}
}Abbreviated for readability: the real response carries all 18 entries in checked_against.lists, and each citation also carries record_key, dob and city where the list publishes them.
Note the shape: one identity per finding, one entry in sources per exclusion record. The person above is cited four times across three lists, and a single list can cite the same identity more than once when it publishes separate actions. Count findings, not sources, when you are deciding whether someone is flagged.
POST /api/v1/screen/bulk screens many at once and reports a flagged_count so you do not have to scan every item.
9. GET /api/v1/npi/:npi
Look up a US healthcare provider by NPI. Three shapes:
/api/v1/npi/:npi- the provider record/api/v1/npi/:npi/licenses- every license they claim, resolved against state rosters/api/v1/npi/:npi/full- record, cross-reference, and an OIG screen in one call
License numbers on an NPI record are self-reported. The provider types them into their CMS record and CMS never validates them against any board. This endpoint resolves each claim against the issuing state board's published roster and reports it as confirmed, not_found, or state_not_covered - the full set is listed below.
state_not_covered means we hold no roster for that state and profession - it is not a finding about the provider. Only not_found means "we checked the board's own file and the license is absent".
Example
curl "https://licenseverificationapi.com/api/v1/npi/1013523711/licenses" \
-H "Authorization: Bearer sk_live_xxx"
{
"success": true,
"data": {
"npi": "1013523711",
"provider_name": "ROBINSON, TIFFANY",
"summary": {
"claims_total": 2, "claims_checkable": 2,
"confirmed": 2, "name_mismatch": 0, "unresolved": 0,
"not_found": 0, "state_not_covered": 0,
"confirmed_not_active": 0
},
"licenses": [{
"claimed": {
"license_number": "209021612", "state": "IL",
"taxonomy_code": "363L00000X", "primary": true
},
"profession": "nurse",
"coverage": "confirmed",
"verified": {
"status": "active",
"expires_at": "2028-05-31",
"holder_name": "ROBINSON, TIFFANY LEANN",
"source": "il-idfpr-nursing"
}
}]
}
}An NPI that fails its check digit returns 400, not 404 - a typo is reported as a typo rather than as a provider who does not exist.
coverage values
Branch on this, not on presence. The values are deliberately distinct: two of them say something about the provider, two say something about our data, and conflating them is the mistake this field exists to prevent.
confirmed- the number matched a roster record and the holder name corroborates it.name_mismatch- the number exists, but under a materially different name. A finding about the provider, and the one worth escalating.unresolved- matched only through the lossy digits-only key, which was not unique. Not evidence either way; treat as unverified.not_found- we hold that state's roster and the number is not in it.state_not_covered- we hold no roster for that (profession, state). A statement about our coverage, not about the provider. Never present it as a negative finding.no_license_claimed- CMS has no license number recorded for that taxonomy.
confirmed_not_active in the summary counts confirmed licenses whose roster status is not active. That is not a red flag on its own: providers routinely leave lapsed licenses from previous states in their NPPES taxonomy slots, so an expired entry alongside a current one is normal.
10. GET /api/v1/search
Find licensees by name when you do not have a license number. /api/v1/search/licenses searches state rosters; /api/v1/search/providers searches the NPPES provider mirror.
confidence of exact or similar plus a similarity score. Confirm a candidate with /api/v1/verify using its license number before relying on it.Query parameters
name(required) - full or partial namestate,profession- optional filterslimit- 1-100, default 25min_similarity- 0-1, default 0.4
curl "https://licenseverificationapi.com/api/v1/search/licenses?name=Jane+Doe&state=TX" \ -H "Authorization: Bearer sk_live_xxx"
11. POST /api/v1/certificates
Run a verification and receive it signed: a timestamped, Ed25519-signed statement that at this moment a named source reported this license status. Anyone can check it against our published key without an account.
curl -X POST "https://licenseverificationapi.com/api/v1/certificates" \
-H "Authorization: Bearer sk_live_xxx" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: onboarding-4417" \
-d '{"profession":"nurse","state":"IL","license_number":"209021612"}'GET /api/v1/certificates/:id- retrieve oneGET /.well-known/jwks.json- our public key, standard JWKS (no auth)POST /api/public/certificates/verify- check a certificate (no auth)
Verification checks the signature over the payload you supply, not our stored copy, so it detects tampering in the document an auditor was actually handed. The payload is canonicalized RFC 8785-style - sorted keys, no insignificant whitespace - and its SHA-256 is returned so you can confirm you canonicalized identically before suspecting the signature.
A valid signature proves we issued the certificate and it is unaltered. It attests to what the source reported when it was issued; it does not assert the license is still in that state today.
12. Idempotency
Send an Idempotency-Key header on any mutating POST. A retry with the same key returns the original response instead of executing again, so a network timeout cannot double-bill you or create two jobs. Keys are honoured for 24 hours and scoped to your account.
- Same key, same body, original finished - the stored response, with
Idempotent-Replay: true. - Same key, same body, original still running - 409. Two concurrent retries can never both execute.
- Same key, different body - 409. That is a client bug, and answering it with the old response would hide it.
13. Errors and rate limits
HTTP status codes
- 200 - Successful verification, whatever the outcome. The answer is in the body, including
not_foundandnot_supported. Counts against quota, except anot_supportedfor a combination we do not cover, which is free.not_supportedusually means we hold no data for that (profession, state), which is permanent - check the coverage matrix first and do not retry. It can also mean the board returned a status we do not yet interpret for that one licence, which IS worth retrying later. Read theerrorfield to tell them apart before deciding to stop sending a whole combination. - 400 - Invalid request (missing/malformed parameters).
- 401 - Missing or invalid API key.
- 429 - Rate limit exceeded. Check
X-RateLimit-Resetheader. - 500 - A fault on our side, such as a query that no longer matches our schema. It says nothing about your request or about coverage: the combination may well be supported, and we returned this rather than let an internal defect masquerade as
not_supported. Does not count against quota. Safe to retry; if it persists, contact support with the combination you sent. - 503 - Reserved for a genuinely unreachable upstream board. Retry after a delay.
Rate-limit headers
Requests per minute. Every response includes:
- X-RateLimit-Limit: 60
- X-RateLimit-Remaining: 47
- X-RateLimit-Reset: 1778190000
Quota headers
Monthly included volume, reported separately because it answers a different question: how much you have used this month, versus how fast you may go right now.
- X-Quota-Limit: 5000
- X-Quota-Used: 1842
- X-Quota-Remaining: 3158
Once the included volume is used up, an X-Quota-Exceeded: true header is added along with X-Quota-Notice. What happens next depends on the plan:
- Paid plans are throttled, not stopped. Requests keep being served at 10 per minute until the month rolls over. There is no overage billing, so exceeding your plan can never produce a surprise invoice - it only makes the API slower. Your integration does not break mid-month.
- Free stops at its included volume and returns 429 until the next monthly reset or an upgrade.
Retries
429 responses carry retryAfter (seconds) in the body. Treat 503 as transient and retry with backoff; treat 400 and 401 as terminal.
14. Data sources and freshness
Each verification carries a source field naming the upstream board, a source_type, and a full provenance block (source, source_url, method, data_as_of, retrieved_at, attribution) you can store for compliance documentation:
official_api- Direct vendor-blessed API (most authoritative).public_csv- Official roster file published by the state board/agency, mirrored on a daily-to-monthly cadence.provenance.data_as_ofis the timestamp of our last successful refresh of that file. See the coverage matrix for per-source cadence.scrape- Polite scrape of the board's public license-search page.internal_cross_ref- Self-attested data from licensees who use our portfolio of license-tracking apps. Useful as a fallback; treat with appropriate skepticism (theraw.provenancefield is set toself_attested_via_portfolio).cache- Served from our cache;cache_age_hourstells you the staleness andprovenance.methodreports the original source type.
provenance.attribution is null for public-domain sources, which is most of them. Where the upstream board publishes under a license that requires attribution, it carries holder, license and licenseUrl. Today that applies to Illinois: IDFPR publishes under the Open Data Commons Open Database License (ODbL) 1.0. If you republish Illinois results, reproduce that notice.
Cache TTLs by profession: medical 30 days, nurse 14 days, contractor/cosmetology/barber/electrician 7 days. Roster-backed results use a 1-day TTL so the roster refresh cadence is the only staleness layer. Presence-based rosters (e.g. Texas TDLR) list current licensees only - for those, not_found means "absent from the latest file", and absence is never reported as disciplinary action.
Need a profession or state we don't cover? Tell us - Enterprise customers get custom adapters built on request.