saperly

Your first call

Stand up a hosted line, call it, and read back the transcript — the voice version of the quickstart, in about five minutes.

Goal: stand up a phone number your agent answers, call it from your own phone, and read back what was said. This is the voice counterpart to the Quickstart — same five minutes, but you end up talking to your agent out loud.

You'll need a Saperly account and an API key. Create both from the dashboard onboarding (see the Quickstart).

Stand up a hosted line

A hosted connection is answered by an in-network voice assistant — speech-to-text, the model, and text-to-speech all run in the network, so no call audio ever reaches your servers. The instructions are your agent's prompt; the line answers with a sensible default voice you can change anytime from the dashboard's voice picker. Create the connection, provision a number, then attach one to the other.

import { configure, numbers, connections } from '@trysaperly/sdk'

configure({ apiKey: process.env.SAPERLY_API_KEY! })

// 1. The brain.
const { data: connection } = await connections.create({
  body: {
    name: 'support agent',
    mode: 'hosted',
    instructions:
      'You are a friendly support agent for Acme Inc. Greet the caller, answer ' +
      'their question, and keep replies short.',
  },
})

// 2. A real number.
const { data: number } = await numbers.provision({ body: { areaCode: '415' } })

// 3. Bind them.
await numbers.assignConnection({
  path: { id: number!.id },
  body: { connectionId: connection!.id },
})

console.log(number!.phoneNumber) // → the number to call
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. The brain.
connection = connections_create.sync(
    client=client,
    body=ConnectionsCreateBody(
        name="support agent",
        mode="hosted",
        instructions=(
            "You are a friendly support agent for Acme Inc. Greet the caller, "
            "answer their question, and keep replies short."
        ),
    ),
)

# 2. A real number.
number = numbers_provision.sync(client=client, body=NumbersProvisionBody(area_code="415"))

# 3. Bind them.
numbers_assign_connection.sync(
    client=client,
    id=number.id,
    body=NumbersAssignConnectionBody(connection_id=connection.id),
)

print(number.phone_number)  # → the number to call
# 1. The brain.
CONNECTION_ID=$(curl -s -X POST https://api.saperly.com/connections \
  -H "Authorization: Bearer $SAPERLY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "support agent",
    "mode": "hosted",
    "instructions": "You are a friendly support agent for Acme Inc. Greet the caller, answer their question, and keep replies short."
  }' | jq -r .id)

# 2. A real 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. Bind them.
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\" }"

The result is a real, certified number ready to ring — answered by the hosted assistant with a default voice you can swap anytime from the dashboard.

Call it

Dial the phoneNumber from any phone. The hosted assistant picks up, speaks its greeting, and holds a real conversation driven by your instructions. The compliance disclosure (when enabled) is spoken first, before anything else.

No webhook to wire

For an inbound call to a hosted line, there is nothing else to set up — the number answers itself. You only reach for webhooks when you want to be notified about calls, not to make them work.

See the call

After you hang up, the call shows up under your workspace with its outcome — who called, how long it ran, and how it ended.

# Most recent calls on your workspace
curl "https://api.saperly.com/calls" \
  -H "Authorization: Bearer $SAPERLY_API_KEY"
import { voice } from '@trysaperly/sdk'

const { data: calls } = await voice.list()
console.log(calls![0]) // status, direction, durationSec, from, to

Each call carries its status, direction, durationSec, and the numbers involved (from / to). The full turn-by-turn transcript — what the caller said and what your agent replied — is at GET /calls/{id}/transcript (voice.transcript({ path: { id } })). Audio stays in the network; you get the record.

What just happened

You stood up a hosted line. A connection (the brain) plus a certified number, bound together — and an in-network voice agent answering, with no media infrastructure of your own.

You talked to your agent. The hosted assistant ran the whole conversation in-network from your instructions.

You got the record. The call's outcome is on your workspace, and the turn-by-turn transcript is a text fetch away — while the audio never left the network.

Next steps

  • Connections — hosted vs manual, and how a connection is reused across many numbers.
  • Manual mode — make your own model the brain on every call, relayed as text.
  • Voice channels — call your own coding agent (Claude Code / openclaw) and talk to it in its context.
  • Place & control calls — outbound calls, transfer, and live control.

On this page