Quickstart
Give your agent a phone number — provision a number, attach a brain, and send an SMS — in Node, Python, or curl.
Goal: stand up a working phone number for your agent and send your first message — in about five minutes. You'll need a Saperly account.
Signup is portal-only. Create an account at the dashboard, create your first workspace, and the onboarding wizard will walk you through the phone number, connection, setup output, and one-time API key reveal. You can also create or rotate keys later from Keys in the dashboard.
Install the SDK
The fastest path is the official TypeScript SDK — typed, dependency-light, and a thin wrapper over the Saperly REST API.
npm install @trysaperly/sdkThe official Python SDK is saperly — see
the Python SDK guide.
pip install saperly # or: uv add saperly
export SAPERLY_API_KEY=sap_sk_...No install needed — every endpoint is plain HTTPS with a bearer token.
export SAPERLY_API_KEY=sap_sk_...Numbers and connections are separate
In Saperly v2 there is no single "line" object. A number is the phone number; a connection is the brain that answers it. You create the connection, provision the number, then attach one to the other. The same connection can answer many numbers.
Stand up a hosted line
Three small calls: create a hosted connection (an in-network voice assistant —
no infrastructure of your own), provision a number, and attach the connection to
the number.
import { configure, numbers, connections } from '@trysaperly/sdk'
configure({ apiKey: process.env.SAPERLY_API_KEY! })
// 1. Create the brain (a connection).
const { data: connection } = await connections.create({
body: {
name: 'my agent',
mode: 'hosted',
instructions: 'You are a helpful assistant answering calls for Acme Inc.',
},
})
// 2. Provision a number.
const { data: number } = await numbers.provision({ body: { areaCode: '415' } })
// 3. Attach the brain to the number.
await numbers.assignConnection({
path: { id: number!.id },
body: { connectionId: connection!.id },
})
console.log(number!.phoneNumber) // → +1...import os
from saperly import create_client
from saperly.api.connections import connections_create
from saperly.api.numbers import numbers_provision, numbers_assign_connection
from saperly.models import (
ConnectionsCreateBody,
NumbersProvisionBody,
NumbersAssignConnectionBody,
)
client = create_client(api_key=os.environ["SAPERLY_API_KEY"])
# 1. Create the brain (a connection).
connection = connections_create.sync(
client=client,
body=ConnectionsCreateBody(
name="my agent",
mode="hosted",
instructions="You are a helpful assistant answering calls for Acme Inc.",
),
)
# 2. Provision a number.
number = numbers_provision.sync(client=client, body=NumbersProvisionBody(area_code="415"))
# 3. Attach the brain to the number.
numbers_assign_connection.sync(
client=client,
id=number.id,
body=NumbersAssignConnectionBody(connection_id=connection.id),
)
print(number.phone_number) # → +1...# 1. Create the brain (a connection).
CONNECTION_ID=$(curl -s -X POST https://api.saperly.com/connections \
-H "Authorization: Bearer $SAPERLY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "my agent",
"mode": "hosted",
"instructions": "You are a helpful assistant answering calls for Acme Inc."
}' | jq -r .id)
# 2. Provision a number.
NUMBER=$(curl -s -X POST https://api.saperly.com/numbers \
-H "Authorization: Bearer $SAPERLY_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "areaCode": "415" }')
NUMBER_ID=$(echo "$NUMBER" | jq -r .id)
# 3. Attach the brain to the number.
curl -X POST "https://api.saperly.com/numbers/$NUMBER_ID/connection" \
-H "Authorization: Bearer $SAPERLY_API_KEY" \
-H "Content-Type: application/json" \
-d "{ \"connectionId\": \"$CONNECTION_ID\" }"Those calls provision a real number, certify it, and bind the assistant. Call the number and the hosted assistant answers — speech-to-text, the LLM, and text-to-speech all run in-network, so no call audio ever reaches your servers.
Send an SMS
fromNumberId is the number's id; body is the message text.
import { messaging } from '@trysaperly/sdk'
const { data: message } = await messaging.send({
body: {
fromNumberId: number!.id,
to: '+15555550123',
body: 'Hi from my agent 👋',
},
})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 👋"
),
)curl -X POST "https://api.saperly.com/messages" \
-H "Authorization: Bearer $SAPERLY_API_KEY" \
-H "Content-Type: application/json" \
-d "{ \"fromNumberId\": \"$NUMBER_ID\", \"to\": \"+15555550123\", \"body\": \"Hi from my agent 👋\" }"Errors are returned, not thrown
SDK calls don't throw by default — each returns { data, error, request, response }.
Branch on error (a typed { code, message, ... }) and read data on success.
const { data, error } = await messaging.send({ body: { /* … */ } })
if (error) {
// handle the typed error, e.g. RecipientOptedOut, InsufficientFunds, …
}What just happened
You created a connection. This is the brain. In hosted mode it's an
in-network voice assistant driven by your instructions. (Want your own LLM to be
the brain? See manual mode.)
You provisioned a number. Saperly reserved it from the carrier, recorded it under your workspace, and started metering its monthly rent against your prepaid balance.
You attached the brain to the number and sent a message through a compliant path — consent state and disclosures are tracked automatically.
Next steps
- Core concepts — the model behind numbers, connections, and the prepaid ledger.
- Voice channels — make your own agent (Claude Code / openclaw) phone-reachable.
- Authentication — scoped
sap_skkeys, spend caps, and minting per-agent keys for fleets. - API reference — every endpoint with a try-it playground.