saperly
Guides

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

ModeWho is the brainWhat you bring
hostedAn in-network voice assistant (cascade STT → LLM → TTS)Your OpenAI-compatible LLM, a voice slug, and optional MCP servers
manualYour own LLMSaperly 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.

MethodPathReturnsScope
GET/connectionsConnection[]connections:read
GET/connections/:idConnectionconnections:read
POST/connectionsConnection (201)connections:write
PATCH/connections/:idConnectionconnections: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

FieldMeaning
backendThe hosted brain backend. network (the default) is an in-network voice assistant; openai_realtime is an OpenAI Realtime voice. Set on create or update.
smsAutoReplyOn a hosted connection, when true the line auto-answers inbound SMS with its model. Defaults to off.
instructionsThe system prompt that defines the assistant's behavior. Any opening line the agent should say goes here — there is no separate greeting field.
complianceEnabledWhen true (the default), the disclosure is spoken as the line's first, uninterruptible utterance — the TCPA notice. When false, there is no forced opener.
disclosureThe TCPA notice spoken first when complianceEnabled is on. Leave it empty with compliance on and Saperly fills a standard org-named default.
llm.kindmanaged uses a Saperly-managed model; byo points at byoBaseUrl with a secretRef to your stored key.
mcpServersMCP 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.

  • 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.

On this page