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
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.
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:
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:
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
import uuid
send_message.sync(client=client, body=body, idempotency_key=str(uuid.uuid4()))The core loop
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
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:
from handset.api.messaging import send_message message = await send_message.asyncio(client=client, body=body)