PPactDocs
Developers

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

ScopeGrants
read:callsList/read inbound calls, transcripts, and the live stream
voice:agentConfigure AI voice agents and place simulated test calls
voice:campaignCreate and manage outbound call campaigns

See OAuth & API scopes for how to request them.

Install an SDK

bash
# TypeScript / JavaScript (npm)
npm install @pact/voice

# Python (PyPI)
pip install pact-voice

Quickstart — list your recent calls

The whole thing in ten lines:

ts
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)"}`);
}
python
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

bash
# 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 & pathReturns
GET /v1/api/voice/callsPage of inbound calls (cursor)
GET /v1/api/voice/calls/{public_id}One call
GET /v1/api/voice/calls/{public_id}/transcriptTurn-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 & pathPurpose
GET /v1/api/voice/agentsList agents
POST /v1/api/voice/agentsCreate 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}/callsRecent agent calls
GET /v1/api/voice/agents/{ref}/analyticsPer-agent analytics + budget
POST /v1/api/voice/agents/{ref}/test-callPlace 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 & pathPurpose
GET /v1/api/voice/campaignsList campaigns
POST /v1/api/voice/campaignsCreate a campaign (+ steps)
GET /v1/api/voice/campaigns/{ref}Status + progress
POST /v1/api/voice/campaigns/{ref}/enqueueAdd contact targets
POST /v1/api/voice/campaigns/{ref}/pausePause dialing
POST /v1/api/voice/campaigns/{ref}/resumeResume (activates a draft)

Live call events (WebSocket)

Browsers cannot send an Authorization header on a WebSocket, so connecting is a two-step handshake:

  1. POST /v1/api/voice/stream/ticket (bearer-authed, scope read:calls) mints a single-use ticket that expires in ~60s.
  2. 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:

ts
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");
}
python
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:

SampleWhereWhat it shows
Terminal clientsdks/voice-ts/examples/terminal.tsA live TUI call feed over WebSocket
React widgetsdks/voice-ts/examples/react-widget.tsxA drop-in "live calls" React component
curl one-linerssdks/voice-ts/examples/curl.shEvery endpoint as a shell one-liner
Python quickstartsdks/voice-py/examples/quickstart.pyList calls + transcripts
Python streamsdks/voice-py/examples/stream.pyasync 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.