Saperly Voice API
Place and control outbound calls and fetch call records. Audio always stays in-network — there is no live media-control surface here by design. Funds are reserved on start and settled from the carrier-reported duration on end.
Place and control outbound calls and fetch call records. Audio always stays in-network — there is no live media-control surface here by design. Funds are reserved on start and settled from the carrier-reported duration on end.
Every API here is available over the APIs.io API and to AI agents over MCP.
One button, every client — Claude, Cursor, VS Code and the rest.
https://apis.io/mcp
find_apisBrowse and filter every API in the catalog.get_api_artifactsOne API's artifacts, grouped by type.get_openapiThe primary OpenAPI for this API.find_similar_apisAPIs that look like this one.apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.resolveTurn a domain, URL or GitHub org into the provider it belongs to.find_cohortsEvery scored population of providers in the catalog.curl "https://apis.io/api/v1/apis/saperly:saperly-voice-api"
curl "https://apis.io/api/v1/apis?limit=25"
Discovery needs no key. Ratings and market analysis are Pro.
Free tier, no form to fill in. Signing in shares your email address with us — we store it to create your key and to recognise you if you sign in with another provider. See our Privacy Policy and Terms.
A second provider on the same verified email joins the account you already have.
openapi: 3.2.0
info:
title: Saperly Voice API
version: 0.1.0
description: 'Operations tagged voice across 2 of this provider''s published API definitions: api-saperly-com-openapi.json, saperly-openapi.yml. Each path carries the servers of the definition it was published in.'
servers:
- url: /
description: This worker
- url: https://api.saperly.com
description: Production
security: []
tags:
- name: Voice
description: Place and control outbound calls and fetch call records. Audio always stays in-network — there is no live media-control surface here by design. Funds are reserved on start and settled from the carrier-reported duration on end.
paths:
/calls:
post:
tags:
- Voice
operationId: voice.place
parameters: []
security: []
responses:
'201':
description: Success
content:
application/json:
schema:
type: object
properties:
id:
type: string
numberId:
type: string
direction:
type: string
enum:
- inbound
- outbound
to:
type: string
from:
type: string
status:
type: string
description: Lifecycle status. `initiated` (placed or ringing, not yet finalized), `completed` (connected and hung up; billed by duration), `no_answer` (the callee never picked up; zero cost), `failed` (the carrier could not connect it, or it was expired unfinalized; zero cost), `rejected` (refused before answer — insufficient balance; zero cost). Only `completed` bills.
rateCentsPerMin:
anyOf:
- type: number
- type: 'null'
durationSec:
anyOf:
- type: number
- type: 'null'
costCents:
anyOf:
- type: number
- type: 'null'
hasRecording:
type: boolean
hasTranscript:
type: boolean
createdAt:
type: string
required:
- id
- numberId
- direction
- to
- from
- status
- rateCentsPerMin
- durationSec
- costCents
- hasRecording
- hasTranscript
- createdAt
additionalProperties: false
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Unauthorized'
'402':
description: InsufficientFunds | SpendLimitExceeded
content:
application/json:
schema:
anyOf:
- $ref: '#/components/schemas/InsufficientFunds'
- $ref: '#/components/schemas/SpendLimitExceeded'
'403':
description: AuthorizationDenied | RecipientOptedOut
content:
application/json:
schema:
anyOf:
- $ref: '#/components/schemas/AuthorizationDenied'
- $ref: '#/components/schemas/RecipientOptedOut'
'404':
description: NumberNotFound
content:
application/json:
schema:
$ref: '#/components/schemas/NumberNotFound'
'409':
description: IdempotencyConflict
content:
application/json:
schema:
$ref: '#/components/schemas/IdempotencyConflict'
'422':
description: NumberHasNoConnection | AudioConfigurationError | UnsupportedVoice | IdempotencyKeyMismatch
content:
application/json:
schema:
anyOf:
- $ref: '#/components/schemas/NumberHasNoConnection'
- $ref: '#/components/schemas/AudioConfigurationError'
- $ref: '#/components/schemas/UnsupportedVoice'
- $ref: '#/components/schemas/IdempotencyKeyMismatch'
'429':
description: RateLimited
content:
application/json:
schema:
$ref: '#/components/schemas/RateLimited'
'500':
description: InternalError
content:
application/json:
schema:
$ref: '#/components/schemas/InternalError'
'502':
description: CallStartFailed
content:
application/json:
schema:
$ref: '#/components/schemas/CallStartFailed'
summary: Place an outbound call
requestBody:
content:
application/json:
schema:
type: object
properties:
fromNumberId:
type: string
description: The Saperly number id to place the call from.
to:
type: string
description: The destination in E.164 format, e.g. "+15555550123".
connectionId:
anyOf:
- type: string
description: Optional. Use this connection for the call instead of the number's bound one.
- type: 'null'
instructions:
anyOf:
- type: string
description: A per-call system prompt that overrides the connection's saved instructions for this call only. Omit to run the call on the line's saved config. (Voice/model/language are not per-call overridable via the API — set them on the connection.)
- type: 'null'
required:
- fromNumberId
- to
additionalProperties: false
description: Place an outbound call from a Saperly number. The connection bound to the number (or `connectionId`) is the brain; audio stays in-network.
required: true
get:
tags:
- Voice
operationId: voice.list
parameters: []
security: []
responses:
'200':
description: Success
content:
application/json:
schema:
type: array
items:
type: object
properties:
id:
type: string
numberId:
type: string
direction:
type: string
enum:
- inbound
- outbound
to:
type: string
from:
type: string
status:
type: string
description: Lifecycle status. `initiated` (placed or ringing, not yet finalized), `completed` (connected and hung up; billed by duration), `no_answer` (the callee never picked up; zero cost), `failed` (the carrier could not connect it, or it was expired unfinalized; zero cost), `rejected` (refused before answer — insufficient balance; zero cost). Only `completed` bills.
rateCentsPerMin:
anyOf:
- type: number
- type: 'null'
durationSec:
anyOf:
- type: number
- type: 'null'
costCents:
anyOf:
- type: number
- type: 'null'
hasRecording:
type: boolean
hasTranscript:
type: boolean
createdAt:
type: string
required:
- id
- numberId
- direction
- to
- from
- status
- rateCentsPerMin
- durationSec
- costCents
- hasRecording
- hasTranscript
- createdAt
additionalProperties: false
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Unauthorized'
'403':
description: AuthorizationDenied
content:
application/json:
schema:
$ref: '#/components/schemas/AuthorizationDenied'
'429':
description: RateLimited
content:
application/json:
schema:
$ref: '#/components/schemas/RateLimited'
'500':
description: InternalError
content:
application/json:
schema:
$ref: '#/components/schemas/InternalError'
summary: List calls
servers:
- url: /
description: This worker
/calls/{id}/end:
post:
tags:
- Voice
operationId: voice.end
parameters:
- name: id
in: path
schema:
type: string
description: The call's id.
required: true
security: []
responses:
'200':
description: Success
content:
application/json:
schema:
type: object
properties:
id:
type: string
numberId:
type: string
direction:
type: string
enum:
- inbound
- outbound
to:
type: string
from:
type: string
status:
type: string
description: Lifecycle status. `initiated` (placed or ringing, not yet finalized), `completed` (connected and hung up; billed by duration), `no_answer` (the callee never picked up; zero cost), `failed` (the carrier could not connect it, or it was expired unfinalized; zero cost), `rejected` (refused before answer — insufficient balance; zero cost). Only `completed` bills.
rateCentsPerMin:
anyOf:
- type: number
- type: 'null'
durationSec:
anyOf:
- type: number
- type: 'null'
costCents:
anyOf:
- type: number
- type: 'null'
hasRecording:
type: boolean
hasTranscript:
type: boolean
createdAt:
type: string
required:
- id
- numberId
- direction
- to
- from
- status
- rateCentsPerMin
- durationSec
- costCents
- hasRecording
- hasTranscript
- createdAt
additionalProperties: false
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Unauthorized'
'403':
description: AuthorizationDenied
content:
application/json:
schema:
$ref: '#/components/schemas/AuthorizationDenied'
'404':
description: CallNotFound
content:
application/json:
schema:
$ref: '#/components/schemas/CallNotFound'
'429':
description: RateLimited
content:
application/json:
schema:
$ref: '#/components/schemas/RateLimited'
'500':
description: InternalError
content:
application/json:
schema:
$ref: '#/components/schemas/InternalError'
summary: End a live call
servers:
- url: /
description: This worker
/calls/{id}/transfer:
post:
tags:
- Voice
operationId: voice.transfer
parameters:
- name: id
in: path
schema:
type: string
description: The call's id.
required: true
security: []
responses:
'200':
description: Success
content:
application/json:
schema:
type: object
properties:
id:
type: string
numberId:
type: string
direction:
type: string
enum:
- inbound
- outbound
to:
type: string
from:
type: string
status:
type: string
description: Lifecycle status. `initiated` (placed or ringing, not yet finalized), `completed` (connected and hung up; billed by duration), `no_answer` (the callee never picked up; zero cost), `failed` (the carrier could not connect it, or it was expired unfinalized; zero cost), `rejected` (refused before answer — insufficient balance; zero cost). Only `completed` bills.
rateCentsPerMin:
anyOf:
- type: number
- type: 'null'
durationSec:
anyOf:
- type: number
- type: 'null'
costCents:
anyOf:
- type: number
- type: 'null'
hasRecording:
type: boolean
hasTranscript:
type: boolean
createdAt:
type: string
required:
- id
- numberId
- direction
- to
- from
- status
- rateCentsPerMin
- durationSec
- costCents
- hasRecording
- hasTranscript
- createdAt
additionalProperties: false
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Unauthorized'
'403':
description: AuthorizationDenied
content:
application/json:
schema:
$ref: '#/components/schemas/AuthorizationDenied'
'404':
description: CallNotFound
content:
application/json:
schema:
$ref: '#/components/schemas/CallNotFound'
'409':
description: IdempotencyConflict
content:
application/json:
schema:
$ref: '#/components/schemas/IdempotencyConflict'
'422':
description: IdempotencyKeyMismatch
content:
application/json:
schema:
$ref: '#/components/schemas/IdempotencyKeyMismatch'
'429':
description: RateLimited
content:
application/json:
schema:
$ref: '#/components/schemas/RateLimited'
'500':
description: InternalError
content:
application/json:
schema:
$ref: '#/components/schemas/InternalError'
'502':
description: CallStartFailed
content:
application/json:
schema:
$ref: '#/components/schemas/CallStartFailed'
summary: Blind-transfer a live call
requestBody:
content:
application/json:
schema:
type: object
properties:
to:
type: string
description: Where to blind-transfer the live leg — an E.164 number (e.g. "+15551230000") or a `sip:` URI.
required:
- to
additionalProperties: false
required: true
servers:
- url: /
description: This worker
/calls/{id}:
get:
tags:
- Voice
operationId: voice.get
parameters:
- name: id
in: path
schema:
type: string
description: The call's id.
required: true
security: []
responses:
'200':
description: Success
content:
application/json:
schema:
type: object
properties:
id:
type: string
numberId:
type: string
direction:
type: string
enum:
- inbound
- outbound
to:
type: string
from:
type: string
status:
type: string
description: Lifecycle status. `initiated` (placed or ringing, not yet finalized), `completed` (connected and hung up; billed by duration), `no_answer` (the callee never picked up; zero cost), `failed` (the carrier could not connect it, or it was expired unfinalized; zero cost), `rejected` (refused before answer — insufficient balance; zero cost). Only `completed` bills.
rateCentsPerMin:
anyOf:
- type: number
- type: 'null'
durationSec:
anyOf:
- type: number
- type: 'null'
costCents:
anyOf:
- type: number
- type: 'null'
hasRecording:
type: boolean
hasTranscript:
type: boolean
createdAt:
type: string
required:
- id
- numberId
- direction
- to
- from
- status
- rateCentsPerMin
- durationSec
- costCents
- hasRecording
- hasTranscript
- createdAt
additionalProperties: false
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Unauthorized'
'403':
description: AuthorizationDenied
content:
application/json:
schema:
$ref: '#/components/schemas/AuthorizationDenied'
'404':
description: CallNotFound
content:
application/json:
schema:
$ref: '#/components/schemas/CallNotFound'
'429':
description: RateLimited
content:
application/json:
schema:
$ref: '#/components/schemas/RateLimited'
'500':
description: InternalError
content:
application/json:
schema:
$ref: '#/components/schemas/InternalError'
summary: Get a call by id
servers:
- url: /
description: This worker
/calls/{id}/recording:
get:
tags:
- Voice
operationId: voice.recording
parameters:
- name: id
in: path
schema:
type: string
description: The call's id.
required: true
security: []
responses:
'200':
description: Success
content:
application/json:
schema: {}
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Unauthorized'
'403':
description: AuthorizationDenied
content:
application/json:
schema:
$ref: '#/components/schemas/AuthorizationDenied'
'404':
description: CallNotFound
content:
application/json:
schema:
$ref: '#/components/schemas/CallNotFound'
'429':
description: RateLimited
content:
application/json:
schema:
$ref: '#/components/schemas/RateLimited'
'500':
description: InternalError
content:
application/json:
schema:
$ref: '#/components/schemas/InternalError'
'502':
description: UpstreamError
content:
application/json:
schema:
$ref: '#/components/schemas/UpstreamError'
summary: Get a call recording (302 redirect to a download URL)
servers:
- url: /
description: This worker
/calls/{id}/transcript:
get:
tags:
- Voice
operationId: voice.transcript
parameters:
- name: id
in: path
schema:
type: string
description: The call's id.
required: true
security: []
responses:
'200':
description: Success
content:
application/json:
schema: {}
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Unauthorized'
'403':
description: AuthorizationDenied
content:
application/json:
schema:
$ref: '#/components/schemas/AuthorizationDenied'
'404':
description: CallNotFound
content:
application/json:
schema:
$ref: '#/components/schemas/CallNotFound'
'429':
description: RateLimited
content:
application/json:
schema:
$ref: '#/components/schemas/RateLimited'
'500':
description: InternalError
content:
application/json:
schema:
$ref: '#/components/schemas/InternalError'
summary: Get a call transcript
servers:
- url: /
description: This worker
components:
schemas:
UpstreamError:
type: object
properties:
_tag:
type: string
enum:
- UpstreamError
status:
type: number
allOf:
- description: The HTTP status the carrier returned upstream.
code:
type: string
description: A short machine-readable code for the upstream failure.
message:
type: string
description: A human-readable description of the upstream failure (carrier identity scrubbed).
required:
- _tag
- status
- code
- message
additionalProperties: false
UnsupportedVoice:
type: object
properties:
_tag:
type: string
enum:
- UnsupportedVoice
field:
type: string
voice:
type: string
message:
type: string
required:
- _tag
- field
- voice
- message
additionalProperties: false
RateLimited:
type: object
properties:
_tag:
type: string
enum:
- RateLimited
bucket:
type: string
description: The rate-limit bucket that was exhausted.
required:
- _tag
- bucket
additionalProperties: false
CallNotFound:
type: object
properties:
_tag:
type: string
enum:
- CallNotFound
callId:
type: string
required:
- _tag
- callId
additionalProperties: false
NumberHasNoConnection:
type: object
properties:
_tag:
type: string
enum:
- NumberHasNoConnection
numberId:
type: string
required:
- _tag
- numberId
additionalProperties: false
Unauthorized:
type: object
properties:
_tag:
type: string
enum:
- Unauthorized
message:
type: string
description: Why the request was rejected (missing, invalid, or insufficient credentials).
required:
- _tag
- message
additionalProperties: false
AudioConfigurationError:
type: object
properties:
_tag:
type: string
enum:
- AudioConfigurationError
message:
type: string
required:
- _tag
- message
additionalProperties: false
IdempotencyConflict:
type: object
properties:
_tag:
type: string
enum:
- IdempotencyConflict
message:
type: string
description: Details of the conflict — a concurrent request is still processing under the same `Idempotency-Key`.
required:
- _tag
- message
additionalProperties: false
RecipientOptedOut:
type: object
properties:
_tag:
type: string
enum:
- RecipientOptedOut
to:
type: string
required:
- _tag
- to
additionalProperties: false
SpendLimitExceeded:
type: object
properties:
_tag:
type: string
enum:
- SpendLimitExceeded
limitCents:
type: number
priorSpendCents:
type: number
requestedCents:
type: number
required:
- _tag
- limitCents
- priorSpendCents
- requestedCents
additionalProperties: false
InsufficientFunds:
type: object
properties:
_tag:
type: string
enum:
- InsufficientFunds
balanceCents:
type: number
requestedCents:
type: number
required:
- _tag
- balanceCents
- requestedCents
additionalProperties: false
InternalError:
type: object
properties:
_tag:
type: string
enum:
- InternalError
traceId:
type: string
description: A correlation id for this failure — quote it when reporting the problem so the request can be traced.
required:
- _tag
- traceId
additionalProperties: false
CallStartFailed:
type: object
properties:
_tag:
type: string
enum:
- CallStartFailed
reason:
type: string
required:
- _tag
- reason
additionalProperties: false
AuthorizationDenied:
type: object
properties:
_tag:
type: string
enum:
- AuthorizationDenied
reason:
type: string
required:
- _tag
- reason
additionalProperties: false
NumberNotFound:
type: object
properties:
_tag:
type: string
enum:
- NumberNotFound
numberId:
type: string
required:
- _tag
- numberId
additionalProperties: false
IdempotencyKeyMismatch:
type: object
properties:
_tag:
type: string
enum:
- IdempotencyKeyMismatch
message:
type: string
description: Why the `Idempotency-Key` is unprocessable — either malformed (e.g. over the length cap) or reused for a request with a different payload.
required:
- _tag
- message
additionalProperties: false
x-refined-from:
- api-saperly-com-openapi.json
- saperly-openapi.yml