Python SDK for Licensy — real-time U.S. medical-license verification across all 50 states + DC, sourced from state medical boards.
Project description
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.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file licensy-0.1.0.tar.gz.
File metadata
- Download URL: licensy-0.1.0.tar.gz
- Upload date:
- Size: 14.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ce634276855073991aec1b435dbcabc1642a7a787a2d83abffa2bd24502deb43
|
|
| MD5 |
6488e04e9dbba0a2237ba44a5b7e1aa1
|
|
| BLAKE2b-256 |
fcc0881b8c4fb4445140a4f4a4a23af0a27ad5fb0d1a462b9db5d07dcb200e32
|
File details
Details for the file licensy-0.1.0-py3-none-any.whl.
File metadata
- Download URL: licensy-0.1.0-py3-none-any.whl
- Upload date:
- Size: 11.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1973dffb9ad9de32f6ee66296f69b7c7e500e67912909b683abaf6fee19f5cb0
|
|
| MD5 |
cc3110f18805e1cc51f42bab8b209b79
|
|
| BLAKE2b-256 |
27ebbcd9df763bde872bb3b700a2b2cc98e54227571424fdd12d3f79fd91ee60
|