Python
The official Saperly v2 Python SDK (saperly on PyPI) — provision numbers, attach connections, send SMS, and place calls with a bearer sk_ key. Typed models, sync + async, built-in retries and webhook verification.
The official Python SDK is saperly — a
typed client generated from the Saperly v2 OpenAPI contract. It covers the whole
camelCase data plane (numbers, connections, messaging, voice, usage, consent,
pricing, keys), with sync and async calls, built-in retries, and webhook
signature verification.
pip install saperly # or: uv add saperlyRequires Python 3.9+. Get an API key from the dashboard → Keys.
Prefer raw HTTP or tools over a client library? Every endpoint is plain JSON over HTTPS — call it directly (see Calling the REST API directly), or use the MCP server to expose Saperly as tools to any agent framework.
A tiny client
create_client(api_key=...) returns a configured client you pass to each
operation. The key rides on Authorization: Bearer <key> for every request; the
workspace is read from the key, never from client input.
import os
from saperly import create_client
client = create_client(api_key=os.environ["SAPERLY_API_KEY"])Each operation is a small module with four entry points: sync(...) (returns the
parsed body), sync_detailed(...) (returns a Response with status_code /
headers / parsed), and the asyncio(...) / asyncio_detailed(...) async
equivalents. Request bodies are typed models with snake_case fields (the SDK
maps them to the API's camelCase on the wire).
The canonical flow
Provision a number, give it a brain (a connection), attach the two, then send an SMS and place a call.
1. Provision a number
from saperly.api.numbers import numbers_provision
from saperly.models import NumbersProvisionBody
number = numbers_provision.sync(client=client, body=NumbersProvisionBody(area_code="415"))
print(number.id, number.phone_number) # → 019…(uuid) +1415…Pass country=... instead of area_code for numbers outside the US/Canada.
2. Create a connection
A connection is the handler a number routes to. mode is "hosted" (an
in-network voice assistant answers, configured by instructions) or "manual"
(your own agent decides each turn — see Manual mode).
from saperly.api.connections import connections_create
from saperly.models import ConnectionsCreateBody
connection = connections_create.sync(
client=client,
body=ConnectionsCreateBody(
name="support",
mode="hosted",
instructions="You are a helpful assistant answering calls for Acme Inc.",
),
)
print(connection.id)3. Attach the connection to the number
from saperly.api.numbers import numbers_assign_connection
from saperly.models import NumbersAssignConnectionBody
numbers_assign_connection.sync(
client=client,
id=number.id,
body=NumbersAssignConnectionBody(connection_id=connection.id),
)4. Send an SMS
from saperly.api.messaging import messaging_send
from saperly.models import MessagingSendBody
message = messaging_send.sync(
client=client,
body=MessagingSendBody(from_number_id=number.id, to="+15555550123", body="Hi from my agent 👋"),
)
print(message.id, message.status)5. Place a call
voice_place uses the number's attached connection by default; pass a
connection_id to override, or instructions for a one-off hosted prompt.
from saperly.api.voice import voice_place
from saperly.models import VoicePlaceBody
call = voice_place.sync(
client=client,
body=VoicePlaceBody(
from_number_id=number.id,
to="+14155551234",
# instructions="Confirm the 3pm appointment, then hang up.",
),
)
print(call.id, call.status)New to the platform? Walk through the Quickstart first.
Error handling
sync(...) returns the parsed body — the success model on 2xx, or the typed
error model on a handled non-2xx. Use sync_detailed(...) when you need the
status code and headers (for example to honor 429 Retry-After):
res = messaging_send.sync_detailed(
client=client,
body=MessagingSendBody(from_number_id=number.id, to=to, body=body),
)
if res.status_code == 200:
print(res.parsed.id, res.parsed.status)
else:
err = res.parsed # typed error model (e.g. RecipientOptedOut, RateLimited)
if res.status_code == 429:
... # honor res.headers["Retry-After"], then retryIdempotent requests (GET/HEAD/OPTIONS/DELETE) are retried once on 5xx +
connection errors; POST/PATCH are never retried. Tune with
create_client(api_key=..., retries=3) (or retries=0).
Async
Every operation has asyncio(...) / asyncio_detailed(...). The client is an
async context manager:
import asyncio, os
from saperly import Saperly
from saperly.api.numbers import numbers_list
async def main():
with Saperly(api_key=os.environ["SAPERLY_API_KEY"]) as saperly:
numbers = await numbers_list.asyncio(client=saperly.client)
print(numbers)
asyncio.run(main())Webhooks
Verify the signature on inbound Saperly webhooks with verify_webhook:
from saperly import verify_webhook
result = verify_webhook(raw_body, secret=os.environ["SAPERLY_WEBHOOK_SECRET"], headers=request.headers)
if not result.valid:
return 401Calling the REST API directly
You don't need the SDK — the v2 API is plain JSON over HTTPS. Wrap the base URL
and bearer header once with httpx (fields are
camelCase on the wire — fromNumberId, areaCode, connectionId):
import os, httpx
saperly = httpx.Client(
base_url="https://api.saperly.com",
headers={"Authorization": f"Bearer {os.environ['SAPERLY_API_KEY']}"},
timeout=30.0,
)
res = saperly.post("/numbers", json={"areaCode": "415"})
res.raise_for_status()
print(res.json()["id"])Non-2xx responses carry a typed JSON error body with a _tag discriminator
(Unauthorized, AuthorizationDenied, RecipientOptedOut, InsufficientFunds,
RateLimited, …). Branch on _tag and honor 429 Retry-After.
Next steps
- Node / TypeScript SDK — the typed v2 client (
@trysaperly/sdk). - MCP — expose Saperly as tools to any agent framework.
- Authentication — scopes, spend caps, and scoped
sap_skkeys. - API reference — every endpoint with a try-it playground.
Node / TypeScript
@trysaperly/sdk is the typed TypeScript client for the Saperly v2 API — provision numbers, send SMS, place calls, and verify webhooks with full IntelliSense.
MCP
Saperly exposes its tools over the Model Context Protocol so any agent framework can provision numbers, send SMS, and place calls as first-class tools — over Streamable HTTP with a bearer sk_ key.