Licensy Python SDK
Real-time U.S. medical-license verification across all 50 states + DC, sourced directly from state medical boards. Free tier returns basic status / expiration; paid tiers (Pro / Enterprise) unlock board-source screenshots, bulk verification, disciplinary actions, point-in-time history, and webhook subscriptions.
Install
pip install licensy
Quickstart
from licensy import LicensyClient
client = LicensyClient(api_key="sk_live_...")
result = client.verify_license(npi="1700013174", state="CA")
if result.license_active:
print(f"Active through {result.expiration_date}")
else:
print(f"Inactive: {result.status}")
Async
from licensy import AsyncLicensyClient
async with AsyncLicensyClient(api_key="sk_live_...") as client:
result = await client.verify_license(npi="1700013174", state="CA")
All tools
# Single state, single physician
client.verify_license(npi="...", state="CA")
# All states for one physician
result = client.list_physician_licenses(npi="...", include_inactive=False)
for license in result:
print(license.state, license.status)
# Cheapest call — just the status word
client.get_license_status(npi="...", state="CA") # → "active"
# Audit-grade screenshot of the state-board page (Paid only)
client.get_screenshot_url(npi="...", state="CA")
# Bulk verification (Paid only)
client.bulk_verify([
{"npi": "1700013174", "state": "CA"},
{"npi": "1700013174", "state": "NY"},
])
# Find disciplinary actions (Paid only)
client.search_disciplinary_actions(npi="...")
# Was this physician licensed on YYYY-MM-DD? (Paid only)
client.get_license_history(npi="...", state="CA", as_of_date="2024-08-12")
# Subscribe to status-change webhooks (Paid only)
sub = client.subscribe_to_changes(
npis=["1700013174"],
webhook_url="https://your-app.com/webhooks/licensy",
)
client.unsubscribe(subscription_id=sub.subscription_id)
Error handling
from licensy import (
LicensyClient,
AuthError,
NotFoundError,
RateLimitError,
TierUpgradeRequired,
ValidationError,
)
try:
client.get_screenshot_url(npi="...", state="CA")
except TierUpgradeRequired as e:
print(f"Need {e.required_tier} plan for screenshots")
except RateLimitError as e:
print(f"Slow down — retry in {e.retry_after_seconds}s")
except NotFoundError:
print("No license on file for that NPI/state")
except AuthError:
print("Check your API key")
except ValidationError as e:
print(f"Bad input: {e.details}")
Configuration
client = LicensyClient(
api_key="sk_live_...",
base_url="https://mcp.licensy.ai", # default; override for staging
timeout=30.0,
)
You can also bring your own httpx.Client for advanced proxying / retry
behaviour:
import httpx
client = LicensyClient(api_key="...", http_client=httpx.Client(proxy="..."))
Real-world recipes
Daily roster check (cron-friendly)
import csv, datetime, os, sys
from licensy import LicensyClient, NotFoundError
client = LicensyClient(api_key=os.environ["LICENSY_API_KEY"])
today = datetime.date.today()
warn_in = datetime.timedelta(days=60)
with open("roster.csv") as f, open("alerts.csv", "w") as out:
reader = csv.DictReader(f)
writer = csv.writer(out)
writer.writerow(["npi", "state", "issue"])
for row in reader:
try:
r = client.verify_license(npi=row["npi"], state=row["state"])
except NotFoundError:
writer.writerow([row["npi"], row["state"], "no record"])
continue
if not r.license_active:
writer.writerow([row["npi"], row["state"], f"inactive: {r.status}"])
elif r.expiration_date and (r.expiration_date - today) < warn_in:
writer.writerow([row["npi"], row["state"], f"expires {r.expiration_date}"])
print("Done — see alerts.csv", file=sys.stderr)
Webhook receiver (FastAPI) with signature verification
import hmac, hashlib, os
from fastapi import FastAPI, Header, HTTPException, Request
SECRET = os.environ["LICENSY_WEBHOOK_SECRET"].encode()
app = FastAPI()
@app.post("/webhooks/licensy")
async def receive(request: Request, x_licensy_signature: str = Header(...)):
body = await request.body()
expected = hmac.new(SECRET, body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, x_licensy_signature):
raise HTTPException(401, "bad signature")
event = await request.json()
# event.type ∈ {"license.expired", "license.status_changed", ...}
print(f"{event['type']} for NPI {event['data']['npi']}/{event['data']['state']}")
return {"ok": True}
Signing-secret format and snippets in 5 languages at https://licensy.ai/docs#webhook-signing.
Bulk verification with retry
import time
from licensy import LicensyClient, RateLimitError
client = LicensyClient(api_key=os.environ["LICENSY_API_KEY"])
pairs = [{"npi": n, "state": s} for n, s in roster]
while True:
try:
results = client.bulk_verify(pairs)
break
except RateLimitError as e:
time.sleep(e.retry_after_seconds)
inactive = [r for r in results if not r.license_active]
print(f"{len(inactive)} inactive of {len(results)}")
Versioning
licensy.__version__ is the package version. The wire protocol is
versioned at /api/v1/...; minor SDK releases stay backward-compatible
on the API.
License
MIT.
Metadata
Release files for licensy 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| licensy-0.1.0.tar.gz | 14.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| licensy-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 25.4 kB
Release files / licensy-0.1.0.tar.gz
| Download URL | licensy-0.1.0.tar.gz |
|---|---|
| Size | 14.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ce634276855073991aec1b435dbcabc1642a7a787a2d83abffa2bd24502deb43
|
|
BLAKE2b-256 checksum How to use checksums |
fcc0881b8c4fb4445140a4f4a4a23af0a27ad5fb0d1a462b9db5d07dcb200e32
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.11.11
|
Release files / licensy-0.1.0-py3-none-any.whl
| Download URL | licensy-0.1.0-py3-none-any.whl |
|---|---|
| Size | 11.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
1973dffb9ad9de32f6ee66296f69b7c7e500e67912909b683abaf6fee19f5cb0
|
|
BLAKE2b-256 checksum How to use checksums |
27ebbcd9df763bde872bb3b700a2b2cc98e54227571424fdd12d3f79fd91ee60
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.11.11
|