Python
Call the Saperly v2 REST API from Python today with httpx — provision numbers, attach connections, send SMS, and place calls with a bearer sk_ key. A native v2 Python SDK is in progress.
A native v2 Python SDK is in progress. Until it ships, call the Saperly v2
REST API directly — it's a small, predictable JSON surface (camelCase fields,
a bearer sap_sk key). This page shows the canonical flow with
httpx. Prefer tools over HTTP? The
MCP server exposes the same capabilities to any agent
framework.
The Saperly v2 API is plain JSON over HTTPS. Every request carries
Authorization: Bearer <sk_key>, sends and receives camelCase fields, and
the workspace is read from the key — never from client input. Base URL:
https://api.saperly.comGet an API key from the dashboard → Keys.
A tiny client
httpx (or requests) is all you need. Wrap the base URL and bearer header
once and reuse the client.
import os
import httpx
saperly = httpx.Client(
base_url="https://api.saperly.com",
headers={"Authorization": f"Bearer {os.environ['SAPERLY_API_KEY']}"},
timeout=30.0,
)Fields are camelCase (fromNumberId, areaCode, connectionId) — the
TypeScript style, not Python's snake_case. The env var here is
SAPERLY_API_KEY. Use scoped sap_sk keys (sap_sk_live_…); see
Authentication for scopes, number allow-lists,
and spend caps.
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
POST /numbers searches and orders a number. Pass an areaCode (US/Canada) or
a country; the response includes the new number's id and E.164
phoneNumber.
res = saperly.post("/numbers", json={"areaCode": "415"})
res.raise_for_status()
number = res.json()
print(number["id"], number["phoneNumber"]) # → num_… +1415…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).
res = saperly.post(
"/connections",
json={
"name": "support",
"mode": "hosted",
"instructions": "You are a helpful assistant answering calls for Acme Inc.",
},
)
res.raise_for_status()
connection = res.json()
print(connection["id"]) # → conn_…3. Attach the connection to the number
POST /numbers/{id}/connection binds the handler to the line.
saperly.post(
f"/numbers/{number['id']}/connection",
json={"connectionId": connection["id"]},
).raise_for_status()4. Send an SMS
POST /messages sends from one of your numbers (by fromNumberId) to any
destination.
res = saperly.post(
"/messages",
json={
"fromNumberId": number["id"],
"to": "+15555550123",
"body": "Hi from my agent 👋",
},
)
res.raise_for_status()
message = res.json()
print(message["id"], message["status"])5. Place a call
POST /calls places an outbound call. It uses the number's attached connection
by default; pass a connectionId to override, or instructions for a one-off
hosted prompt.
res = saperly.post(
"/calls",
json={
"fromNumberId": number["id"],
"to": "+14155551234",
# "instructions": "Confirm the 3pm appointment, then hang up.",
},
)
res.raise_for_status()
call = res.json()
print(call["id"], call["status"])New to the platform? Walk through the Quickstart first.
Async
The same calls work with httpx.AsyncClient. Construct it inside async with so
the underlying connection pool closes cleanly.
import asyncio
import os
import httpx
async def main():
async with httpx.AsyncClient(
base_url="https://api.saperly.com",
headers={"Authorization": f"Bearer {os.environ['SAPERLY_API_KEY']}"},
timeout=30.0,
) as saperly:
res = await saperly.get("/numbers")
res.raise_for_status()
print(res.json())
asyncio.run(main())Error handling
Non-2xx responses return a typed JSON error body with a _tag discriminator
(for example Unauthorized, AuthorizationDenied, ConsentRequired,
InsufficientBalance, RateLimited). Branch on _tag and honor 429
Retry-After.
res = saperly.post(
"/messages",
json={"fromNumberId": number["id"], "to": to, "body": body},
)
if res.status_code >= 400:
err = res.json()
tag = err.get("_tag")
if tag == "ConsentRequired":
... # recipient hasn't consented — capture consent first
elif tag == "RateLimited":
... # honor Retry-After, then retry
else:
res.raise_for_status()The full set of endpoints, request/response shapes, and typed error tags lives in the API reference with a try-it playground.
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.