Voice API & SDKs
Build on Pact Voice — read calls and transcripts, configure AI voice agents and outbound campaigns, and stream live call events. REST + WebSocket, TypeScript & Python SDKs.
The Pact Voice public API lets third-party developers read voice calls and transcripts, configure autonomous AI voice agents, run outbound campaigns, and subscribe to a live call-event stream. It is REST over HTTPS returning JSON, plus one WebSocket for live events.
- Base URL:
https://api.pact.place - Auth: an OAuth 2.0 access token or a scoped API key, sent as
Authorization: Bearer <token>. - Everything is a
public_id— a UUID. The API never exposes an internal numeric id.
Scopes
| Scope | Grants |
|---|---|
read:calls | List/read inbound calls, transcripts, and the live stream |
voice:agent | Configure AI voice agents and place simulated test calls |
voice:campaign | Create and manage outbound call campaigns |
See OAuth & API scopes for how to request them.
Install an SDK
# TypeScript / JavaScript (npm)
npm install @pact/voice
# Python (PyPI)
pip install pact-voice
Quickstart — list your recent calls
The whole thing in ten lines:
import { PactVoiceClient } from "@pact/voice";
const voice = new PactVoiceClient({ token: process.env.PACT_API_KEY! });
const { data: calls } = await voice.listCalls({ limit: 5 });
for (const call of calls) {
console.log(call.caller_number, call.status, `${call.duration_seconds}s`);
const { turns } = await voice.getCallTranscript(call.public_id);
console.log(` ${turns.length} turns — ${call.summary ?? "(no summary)"}`);
}
import asyncio
from pact_voice import PactVoiceClient
async def main():
async with PactVoiceClient(token="pact_live_...") as voice:
page = await voice.list_calls(limit=5)
for call in page["data"]:
print(call["caller_number"], call["status"], f"{call['duration_seconds']}s")
asyncio.run(main())
curl one-liners
# List recent inbound calls
curl -s https://api.pact.place/v1/api/voice/calls?limit=5 \
-H "Authorization: Bearer $PACT_API_KEY"
# One call's transcript
curl -s https://api.pact.place/v1/api/voice/calls/$PUBLIC_ID/transcript \
-H "Authorization: Bearer $PACT_API_KEY"
# Create an AI voice agent
curl -s -X POST https://api.pact.place/v1/api/voice/agents \
-H "Authorization: Bearer $PACT_API_KEY" -H "Content-Type: application/json" \
-d '{"name":"Front desk","role":"receptionist","provider":"twilio_realtime"}'
# Place a deterministic, net-$0 simulated test call
curl -s -X POST https://api.pact.place/v1/api/voice/agents/$AGENT_ID/test-call \
-H "Authorization: Bearer $PACT_API_KEY" -H "Content-Type: application/json" \
-d '{"mode":"simulated"}'
# Mint a single-use ticket for the live WebSocket
curl -s -X POST https://api.pact.place/v1/api/voice/stream/ticket \
-H "Authorization: Bearer $PACT_API_KEY"
Endpoints
Calls — scope read:calls
| Method & path | Returns |
|---|---|
GET /v1/api/voice/calls | Page of inbound calls (cursor) |
GET /v1/api/voice/calls/{public_id} | One call |
GET /v1/api/voice/calls/{public_id}/transcript | Turn-by-turn transcript |
Lists are cursor-paginated: pass ?limit= and follow next_cursor until it is
null (the SDKs' iterateCalls / iterate_calls do this for you).
Agents — scope voice:agent
| Method & path | Purpose |
|---|---|
GET /v1/api/voice/agents | List agents |
POST /v1/api/voice/agents | Create an agent |
GET /v1/api/voice/agents/{ref} | Get one (id or public_id) |
PATCH /v1/api/voice/agents/{ref} | Update prompt / voice / budget / status |
GET /v1/api/voice/agents/{ref}/calls | Recent agent calls |
GET /v1/api/voice/agents/{ref}/analytics | Per-agent analytics + budget |
POST /v1/api/voice/agents/{ref}/test-call | Place a simulated test call |
test-call runs a deterministic, net-$0 simulated conversation and returns
its full transcript. mode: "live" is a documented seam — it returns 402 when
the per-agent daily budget is reached and 501 otherwise; real provider dialing
is not exposed on the public API, so you are never told a $0 fake call was real.
Campaigns — scope voice:campaign
| Method & path | Purpose |
|---|---|
GET /v1/api/voice/campaigns | List campaigns |
POST /v1/api/voice/campaigns | Create a campaign (+ steps) |
GET /v1/api/voice/campaigns/{ref} | Status + progress |
POST /v1/api/voice/campaigns/{ref}/enqueue | Add contact targets |
POST /v1/api/voice/campaigns/{ref}/pause | Pause dialing |
POST /v1/api/voice/campaigns/{ref}/resume | Resume (activates a draft) |
Live call events (WebSocket)
Browsers cannot send an Authorization header on a WebSocket, so connecting is
a two-step handshake:
POST /v1/api/voice/stream/ticket(bearer-authed, scoperead:calls) mints a single-use ticket that expires in ~60s.- Connect to
wss://api.pact.place/v1/api/voice/stream?ticket=<ticket>.
The socket emits JSON frames: stream.open, then call.started /
call.updated / call.ended as calls happen, plus periodic heartbeats. The
SDKs wrap the whole handshake:
for await (const ev of voice.streamCalls()) {
if (ev.type === "call.started") console.log("ring:", ev.call.caller_number);
if (ev.type === "call.ended") console.log("done:", ev.call.duration_seconds, "s");
}
async for ev in voice.stream_calls():
if ev["type"] == "call.started":
print("ring:", ev["call"]["caller_number"])
Sample apps
Ready-to-run samples ship in each SDK's examples/ directory:
| Sample | Where | What it shows |
|---|---|---|
| Terminal client | sdks/voice-ts/examples/terminal.ts | A live TUI call feed over WebSocket |
| React widget | sdks/voice-ts/examples/react-widget.tsx | A drop-in "live calls" React component |
| curl one-liners | sdks/voice-ts/examples/curl.sh | Every endpoint as a shell one-liner |
| Python quickstart | sdks/voice-py/examples/quickstart.py | List calls + transcripts |
| Python stream | sdks/voice-py/examples/stream.py | async for over live call events |
Errors
Failures are standard HTTP status codes with a {"detail": "..."} body. The
SDKs map them to typed exceptions you branch on: 401 auth, 403 scope, 404
not-found, 429 rate-limit (retryable, honours Retry-After), 402 cost-cap,
400/422 validation. Rate limits and 5xx are retried automatically with
jittered backoff; scope, validation and cost-cap errors fail fast.