saperly

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/sdk

The 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_sk keys, spend caps, and minting per-agent keys for fleets.
  • API reference — every endpoint with a try-it playground.

On this page