saperly
Guides

Voice

Place and control outbound calls and fetch call records over the v2 API — audio always stays in-network, funds are reserved on start and settled on end.

Place and control outbound calls, and list and fetch call records. Call audio stays in-network and never reaches your servers. The handler that actually talks during a call is the connection bound to the number (or the connectionId you pass).

Endpoints

Base URL https://api.saperly.com. Authenticate with Authorization: Bearer sk_live_...; the workspace is resolved from the token.

MethodPathReturnsScope
POST/callsCall (201)calls:create
POST/calls/:id/endCallcalls:create
POST/calls/:id/transferCallcalls:control
GET/calls/:idCall
GET/callsCall[]

A GET or /end against an unknown id returns CallNotFound (404).

Outbound voice requires consent

Calling a peer requires recorded consent first. Record explicit_outbound consent before placing a call to a number you initiated contact with — Saperly enforces this automatically. See Compliance.

Place a call

POST /calls takes the following payload:

FieldTypeNotes
fromNumberIdstringThe Saperly number to call from.
tostringDestination in E.164 format (e.g. +15555550123).
connectionIdstringOptional. Overrides the number's bound connection for this call.
instructionsstringOptional. A per-call system prompt that overrides the line's saved instructions for this call only.
import { Saperly } from '@saperly/sdk'

const saperly = new Saperly({ apiKey: process.env.SAPERLY_API_KEY! })

const call = await saperly.calls.place({
  fromNumberId: 'num_...',
  to: '+15555550123',
  instructions: 'You are calling to confirm a delivery window. Keep it under a minute.',
})

console.log(call.id)
curl -X POST https://api.saperly.com/calls \
  -H "Authorization: Bearer $SAPERLY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "fromNumberId": "num_...",
    "to": "+15555550123",
    "instructions": "You are calling to confirm a delivery window. Keep it under a minute."
  }'

If the carrier can't start the call, it fails with CallStartFailed (502).

Per-call instructions

The optional instructions string is a per-call system prompt. It overrides the line's saved instructions for this call only — the connection's stored prompt is left untouched. Use it to give one call a narrow goal (confirm an appointment, read back an order) without editing the connection.

Transfer a live call

POST /calls/:id/transfer does a blind transfer of the live leg to a new destination. This is signaling only — it hands the call off at the carrier and works across every backend. It requires the calls:control scope.

The payload takes a single to, either an E.164 number or a sip: URI:

{ "to": "+15551230000" }   // or { "to": "sip:agent@example.com" }
curl -X POST https://api.saperly.com/calls/$CALL_ID/transfer \
  -H "Authorization: Bearer $SAPERLY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "to": "+15551230000" }'

End a call

POST /calls/:id/end settles the call. The payload reports the elapsed duration:

{ "durationSec": 42 }   // number >= 0
curl -X POST https://api.saperly.com/calls/$CALL_ID/end \
  -H "Authorization: Bearer $SAPERLY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "durationSec": 42 }'

Get and list calls

const call = await saperly.calls.get(callId)
const calls = await saperly.calls.list()
curl "https://api.saperly.com/calls/$CALL_ID" \
  -H "Authorization: Bearer $SAPERLY_API_KEY"

curl "https://api.saperly.com/calls" \
  -H "Authorization: Bearer $SAPERLY_API_KEY"

Billing: reserve → settle

Funds are reserved when a call starts and settled when it ends — Saperly's prepaid ledger holds an estimate at start, then settles against the reported durationSec and releases the remainder. See Billing for the reserve → settle → release flow and Concepts for the model.

Who talks during the call

The handler that speaks is the connection bound to the number, or the connectionId you pass on POST /calls. For an agent that is the brain of the live call — receiving turns and issuing directives — use a manual-mode connection and see Manual mode and Voice channels.

v1 SDK equivalents

On the published @saperly/sdk / saperly v1 surface:

saperly.calls.create({ lineId, toNumber })
saperly.calls.conversation({ lineId, toNumber, topic }) // conversation helper

See the Node SDK for the full v1 client.

  • Connections — the handler that answers and talks.
  • Manual mode — your own LLM as the brain of a live call.
  • Voice channels — call your own coding agent and talk to it in its own context.
  • Billing — the prepaid ledger and reserve → settle → release.
  • Compliance — consent required for outbound voice.

On this page