docs

Docs › SDKs

Python SDK

handset on PyPI — generated from the same OpenAPI spec as the API and the TypeScript SDK. Typed attrs models, sync and asyncio variants for every endpoint, Python 3.11+.

Install & configure

shell
pip install handset

Create one client and pass it to every call. The token is your API key — start with a test key (hs_test_…): simulated carrier, free numbers, instant compliance.

setup.py
from handset import AuthenticatedClient

client = AuthenticatedClient(
    base_url="https://api.handset.dev/v1",
    token=os.environ["HANDSET_API_KEY"],
)

Send a message

Endpoints live under handset.api.<area> — one module per endpoint with sync, sync_detailed, asyncio, and asyncio_detailed functions:

send.py
from handset.api.messaging import send_message
from handset.models import SendMessageBody

message = send_message.sync(
    client=client,
    body=SendMessageBody(
        from_="num_01kzv…",      # a tenant number ID (from_ — from is reserved)
        to="+14155550134",
        body="Your technician is 15 minutes out.",
    ),
)
print(message.id, message.status)   # msg_…, "queued"

Errors & status codes

sync returns the parsed model on success, or an ErrorBody on API errors. Use sync_detailed when you need the status code:

errors.py
from handset.models import ErrorBody

res = send_message.sync_detailed(client=client, body=body)
if isinstance(res.parsed, ErrorBody):
    code = res.parsed.error.code       # e.g. "recipient_opted_out"
    print(res.status_code, code)
else:
    message = res.parsed

Codes worth branching on: recipient_opted_out (they sent STOP — do not retry), campaign_not_approved (10DLC in review), rate_limited, tenant_not_found.

Rate limits (20 req/s per key, bursts to 60) are handled for you: the client's transport retries 429s automatically, honoring Retry-After, up to 3 attempts. Opt out with httpx_args={"transport": httpx.HTTPTransport()}.

Idempotent sends

idempotent.py
import uuid

send_message.sync(client=client, body=body, idempotency_key=str(uuid.uuid4()))

The core loop

onboard.py
from handset.api.tenants import create_tenant
from handset.api.phone_numbers import search_available_numbers, purchase_number
from handset.api.messaging import list_conversations
from handset.models import TenantCreate, PurchaseNumberBody

# 1. Your customer, as a tenant
tenant = create_tenant.sync(client=client, body=TenantCreate(
    name="Bayview Dental", timezone="America/Los_Angeles",
))

# 2. A number in their area code, live on arrival
found = search_available_numbers.sync(client=client, area_code="415")
number = purchase_number.sync(client=client, body=PurchaseNumberBody(
    tenant_id=tenant.id, phone_number=found.data[0].phone_number,
))

# 3. Read threads back for your UI
threads = list_conversations.sync(client=client, tenant_id=tenant.id)

Pagination

paginate.py
from handset.api.messaging import list_messages
from handset.types import UNSET

after = UNSET
while True:
    page = list_messages.sync(client=client, limit=100, after=after)
    for m in page.data:
        handle(m)
    if not page.has_more:
        break
    after = page.next_cursor

Async

Every endpoint has an asyncio twin with the same signature:

async.py
from handset.api.messaging import send_message

message = await send_message.asyncio(client=client, body=body)
Building in TypeScript instead? Same objects, same spec: TypeScript SDK guide.