Metadata-Version: 2.4
Name: licensy
Version: 0.1.0
Summary: Python SDK for Licensy — real-time U.S. medical-license verification across all 50 states + DC, sourced from state medical boards.
Author-email: Licensy <engineering@licensy.ai>
License: MIT
Project-URL: Homepage, https://licensy.ai
Project-URL: Documentation, https://licensy.ai/docs
Project-URL: API Reference, https://mcp.licensy.ai/.well-known/oauth-protected-resource
Project-URL: Source, https://github.com/licensyai/licensy-sdk-python
Keywords: healthcare,credentialing,medical-license,verification,physician,NPI
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Healthcare Industry
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: httpx>=0.25
Requires-Dist: typing-extensions>=4.0; python_version < "3.11"
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
Requires-Dist: respx>=0.21; extra == "dev"
Dynamic: license-file

# 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

```bash
pip install licensy
```

## Quickstart

```python
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

```python
from licensy import AsyncLicensyClient

async with AsyncLicensyClient(api_key="sk_live_...") as client:
    result = await client.verify_license(npi="1700013174", state="CA")
```

## All tools

```python
# 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

```python
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

```python
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:

```python
import httpx
client = LicensyClient(api_key="...", http_client=httpx.Client(proxy="..."))
```

## Real-world recipes

### Daily roster check (cron-friendly)

```python
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

```python
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

```python
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.
