Connections
A connection is the handler bound to a phone number — the thing that answers a call — in either hosted or manual mode, with audio always staying in-network.
A connection is the handler bound to a phone number — the thing that answers a call. You create a connection, configure how it should behave, and assign it to a number. It comes in two modes, and in both of them the call audio stays in-network — it never reaches your servers. See Core concepts.
Modes
| Mode | Who is the brain | What you bring |
|---|---|---|
hosted | An in-network voice assistant (cascade STT → LLM → TTS) | Your OpenAI-compatible LLM, a voice slug, and optional MCP servers |
manual | Your own LLM | Saperly forwards text turns and executes directives — see Manual mode |
In hosted mode you point the in-network assistant at your own OpenAI-compatible LLM, choose a voice by slug, and optionally declare MCP servers the assistant can call. In manual mode you are the brain: Saperly forwards each text turn to your LLM and executes the directives it returns.
Audio never leaves the carrier
Both modes keep speech-to-text, text-to-speech, and voice activity detection in-network. Saperly only ever sees text turns and tool calls — there is no audio socket to your server.
Endpoints
Base URL https://api.saperly.com. Authenticate with Authorization: Bearer sk_live_...; the workspace is resolved from the token.
| Method | Path | Returns | Scope |
|---|---|---|---|
GET | /connections | Connection[] | connections:read |
GET | /connections/:id | Connection | connections:read |
POST | /connections | Connection (201) | connections:write |
PATCH | /connections/:id | Connection | connections:write |
DELETE | /connections/:id | { status: "deleted" } | connections:write |
A GET or PATCH/DELETE against an unknown id returns ConnectionNotFound (404).
The connection shape
{
"id": "conn_...",
"name": "support line",
"mode": "hosted", // 'hosted' | 'manual'
"backend": "network", // hosted brain: 'network' (default) | 'openai_realtime'
"smsAutoReply": false, // hosted: auto-answer inbound SMS with the model
"instructions": "You are ...", // the system prompt (put any opening line here)
"complianceEnabled": true, // when on, `disclosure` is the forced TCPA opener
"disclosure": "You are speaking ...", // spoken first, uninterruptibly, when compliance is on
"llm": {
"kind": "managed", // 'managed' | 'byo'
"model": "openai/gpt-4o",
"byoBaseUrl": "https://...", // when kind === 'byo'
"secretRef": "secret_..." // reference to your stored LLM key
},
"tts": { "voiceId": "aria" }, // a Saperly voice slug (e.g. 'aria', 'atlas')
"stt": { "language": "en" },
"mcpServers": [
{
"url": "https://mcp.example.com",
"auth": { "type": "bearer", "token": "..." }, // or { "type": "none" }
"allowedTools": ["lookup_order"]
}
],
"createdAt": "2026-06-18T..."
}llm, tts, stt, and mcpServers are each nullable — omit them and the connection uses managed defaults.
Key fields
| Field | Meaning |
|---|---|
backend | The hosted brain backend. network (the default) is an in-network voice assistant; openai_realtime is an OpenAI Realtime voice. Set on create or update. |
smsAutoReply | On a hosted connection, when true the line auto-answers inbound SMS with its model. Defaults to off. |
instructions | The system prompt that defines the assistant's behavior. Any opening line the agent should say goes here — there is no separate greeting field. |
complianceEnabled | When true (the default), the disclosure is spoken as the line's first, uninterruptible utterance — the TCPA notice. When false, there is no forced opener. |
disclosure | The TCPA notice spoken first when complianceEnabled is on. Leave it empty with compliance on and Saperly fills a standard org-named default. |
llm.kind | managed uses a Saperly-managed model; byo points at byoBaseUrl with a secretRef to your stored key. |
mcpServers | MCP servers the hosted assistant may call as tools during a conversation. allowedTools scopes which tools are exposed. |
Create payload
{
"name": "support line", // required, 1–120 chars
"mode": "hosted", // optional, defaults to 'hosted'
"backend": "network", // optional; 'network' (default) | 'openai_realtime'
"smsAutoReply": false, // optional; hosted only, defaults to false
"instructions": "...", // optional
"complianceEnabled": true, // optional, defaults to true
"disclosure": "...", // optional; the TCPA opener when compliance is on
"llm": { ... }, // optional
"tts": { ... }, // optional
"stt": { ... }, // optional
"mcpServers": [ ... ] // optional
}For manual mode, a manualSecret (an mc_ prefix followed by hex) is minted once when the connection is created. Your agent uses it to connect — see Manual mode. The default manual LLM model is openai/gpt-4o.
Create a hosted connection
This creates an in-network assistant with instructions, a TCPA disclosure, and a voice selected by slug. Voices are Saperly's in-network voices, selected by slug.
Voices: a connection's voice is chosen by a Saperly voice slug (e.g. aria, atlas) via tts.voiceId.
import { Saperly } from '@saperly/sdk'
const saperly = new Saperly({ apiKey: process.env.SAPERLY_API_KEY! })
const connection = await saperly.connections.create({
name: 'support line',
mode: 'hosted',
instructions:
'You are a friendly support agent for Acme Inc. Be concise. Open with: Thanks for calling Acme — how can I help?',
complianceEnabled: true,
disclosure: 'You are speaking with an AI assistant for Acme.',
tts: { voiceId: 'aria' },
})
console.log(connection.id) // → conn_...curl -X POST https://api.saperly.com/connections \
-H "Authorization: Bearer $SAPERLY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "support line",
"mode": "hosted",
"instructions": "You are a friendly support agent for Acme Inc. Be concise. Open with: Thanks for calling Acme — how can I help?",
"complianceEnabled": true,
"disclosure": "You are speaking with an AI assistant for Acme.",
"tts": { "voiceId": "aria" }
}'Assign it to a number
A connection does nothing until it is bound to a number. Assign it with POST /numbers/:id/connection:
await saperly.numbers.setConnection(numberId, { connectionId: connection.id })curl -X POST https://api.saperly.com/numbers/$NUMBER_ID/connection \
-H "Authorization: Bearer $SAPERLY_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "connectionId": "conn_..." }'Now calls to that number are answered by the connection. To make it the brain of a live call from your own agent, switch the connection to manual mode — Saperly reconciles all the carrier config for you — then read Manual mode and Voice channels.
Calling your own tools
Declaring mcpServers on a hosted connection lets the in-network assistant call your tools mid-conversation — for example, to look up an order or check availability. Each server entry takes a url, optional auth (bearer with a token, or none), and an optional allowedTools allow-list. The servers are registered to the assistant when the connection is synced.
Related
- Numbers — provision and manage the phone numbers connections bind to.
- Manual mode — run your own LLM as the brain of a live call.
- Voice — place and control outbound calls.
- API reference — the full generated contract.
Authentication
Saperly uses one tier of scoped sk_ API keys, each carrying a grant of scopes, an optional number allow-list, and an optional spend cap. The workspace is always read from the key, and a key with keys:admin can mint child keys bounded by its own grant.
Compliance
Consent, disclosures, and 10DLC are first-class in Saperly and enforced independently of any LLM — outbound contact without recorded consent is blocked before it goes out.