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.
| Method | Path | Returns | Scope |
|---|---|---|---|
POST | /calls | Call (201) | calls:create |
POST | /calls/:id/end | Call | calls:create |
POST | /calls/:id/transfer | Call | calls:control |
GET | /calls/:id | Call | — |
GET | /calls | Call[] | — |
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:
| Field | Type | Notes |
|---|---|---|
fromNumberId | string | The Saperly number to call from. |
to | string | Destination in E.164 format (e.g. +15555550123). |
connectionId | string | Optional. Overrides the number's bound connection for this call. |
instructions | string | Optional. 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 >= 0curl -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 helperSee the Node SDK for the full v1 client.
Related
- 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.
Messaging
Send and list SMS from your Saperly numbers over the v2 API — consent and disclosures are enforced automatically.
Manual mode
Bring your own LLM as the brain of a phone call — Saperly sends it text turns and executes the directives it returns, while speech-to-text, text-to-speech, and the audio all stay in-network.