Voice client for the
control and data planes, and the optional browser helpers when you are building
a softphone in the browser.
Voice.calls), move audio frames (Voice.audioBridge), and hand a call
to a server-side AI agent (Voice.agents). This page is the TypeScript
reference. The same shape ships in
Python, Go, and
Rust.
This is a callback surface, not an
EventEmitter. There is no join() or
say(). You originate a call, then feed and drain audio through
publishDownlink / onUplink (or hand the whole call to an agent). See
SDK Events for the one place where events
appear (the human-agent control channel).Construct the client
The top-levelVoice client gives you three scoped helpers: calls,
audioBridge, and agents. Each helper carries your org id and the transport.
string
required
Control-plane origin (no trailing slash). Requests go over HTTPS with
Authorization: Bearer <apiKey>.string
required
API key with
voice:* scopes.string
required
Default org / tenant id applied to every call.
string
Relay hostname for the audio bridge. Defaults to
relay.clutchcall.dev.(input, init) => Promise<Response>
Override the
fetch implementation (for example, in Node before global fetch).WebTransportFactory
Inject a custom WebTransport factory for the audio bridge (mainly for Node
tests).
VoiceError if baseUrl, apiKey, or orgId is
missing.
Place a call and move audio
This is the end-to-end shape of a server-side voice integration: originate, attach the bridge, drain caller audio into your ASR, and push synthesized audio back.1
Originate the call
calls.originate() places an outbound call over a SIP trunk and resolves to
a Call handle once the call is dialing.2
Attach the audio bridge
audioBridge.attach() opens the bidirectional bridge as the server:
it subscribes the caller’s audio (uplink) and publishes audio back
(downlink). Pass onUplink to receive caller frames.3
Push audio back to the caller
Send each encoded frame from your TTS onto the downlink track.
4
Close when the call ends
Tear down both tracks and the underlying session.
Calls — control plane
Reachable as v.calls. Originates calls and fetches their current state. Every
method resolves against the control-plane API and returns a Call.
calls.originate(args) → Promise<Call>
Place an outbound call over a SIP trunk. Resolves once the call is dialing.
string
required
E.164 destination.
string
required
Caller-id, E.164.
string
required
SIP trunk to route over.
string
AI agent id to attach automatically on answer.
number
Seconds to ring before giving up. Server-clamped 5..120, default 30.
calls.get({ sid }) → Promise<Call>
Fetch the current state of a call by its sid. Use it to poll status after
originate(). The control plane is request/response, so re-fetch to observe a
call as it moves through its lifecycle.
Call — one call handle
originate() and get() return a Call. It has read-only fields plus two
mutating methods.
call.transfer(args | string) → Promise<void>
Transfer the call to a PSTN number or re-attach a different agent. Provide
exactly one of to / agent. A bare string is shorthand for { to }. The
engine executes a PSTN transfer as a REFER on the SIP leg
(Telephony API).
VoiceError if you supply neither to nor agent.
call.hangup() → Promise<void>
End the call and drop both audio tracks.
AudioBridge — data plane
Reachable as v.audioBridge (an AudioBridgeFactory). It opens the
bidirectional audio bridge for one call over our media-over-QUIC transport.
It returns a handle that you hold for the call’s lifetime. The bridge maps to
the voice/<sid>/{uplink,downlink} track convention described in
Realtime Tracks.
audioBridge.attach(callSid, opts) → Promise<AudioBridge>
Open the bridge as the server: subscribe to the caller’s audio (uplink)
and publish audio back (downlink). onUplink is required.
(frame: Uint8Array, timestampUs: bigint) => void
required
Callback for inbound caller audio, fired once per encoded frame.
number
Default
48000.number
Default
1.number
Frame duration in ms. Default
20.audioBridge.attachCaller(callSid, opts) → Promise<AudioBridge>
The browser-caller mirror of attach(): subscribe to the downlink
(cloud → caller) and publish the uplink (mic → cloud). Pass onDownlink to
receive audio for playback. This is the softphone path. Pair it with
captureMicrophone and OpusPlayer.
AudioBridge methods
timestampUs is an optional bigint. If you omit it, the SDK stamps a
monotonic microsecond clock. The onUplink / onDownlink setters keep the
public surface stable. The consumer callback that you pass to attach() /
attachCaller() is the one that fires per frame.
The browser/TS SDK moves audio over WebTransport only. The QUIC→WebSocket
fallback ladder currently ships in the native SDK cores (Python, Go, Rust, and
the other wrappers), not in the browser build. Plan for WebTransport-capable
clients when you build in the browser.
Agents — AI-agent attach
Reachable as v.agents. Bind a server-side speech-to-speech agent to a live
call. The engine wires the audio bridge end-to-end, so you do not open an
AudioBridge yourself.
agents.attach(callSid, agent) → Promise<void>
Browser softphone helpers
These helpers ship on themoqt subpath. They turn a microphone into encoded
Opus and play encoded Opus back, so the softphone never touches a WebRTC
transport for media. The browser’s own audio pipeline (echo cancellation, gain
control, noise suppression, Opus encode) runs behind a loopback capture graph.
The SDK taps the encoded frames and sends the payloads over the
media-over-QUIC transport.
Browser Audio Capture
and WebRTC diversion
cover the mechanics.
captureMicrophone(publication, opts?) → Promise<MicCapture>
This helper captures the mic and runs echo cancellation, gain control, and
noise suppression. It encodes the audio to Opus and writes each encoded
frame to a MoQT audio publication. The returned stop() tears down the
capture graph (it does not close the publication). The simplest softphone loop
forwards captured frames into the bridge with publishUplink:
OpusPlayer
OpusPlayer decodes received Opus with WebCodecs and renders it through an
audio ring buffer (padded with silence on underrun). Construct it with an
AudioContext. Call start() once, then push() each frame you receive.
Types
Voice is audio-only. There are no video codecs on this surface. Phone legs
use G.711 on the wire; the engine transcodes them to the codec you request.
Browser legs are Opus. Errors thrown by the SDK are instances of
VoiceError.
The control-plane surface is request/response. There is no socket of call
events, so track a call’s lifecycle with repeated calls.get({ sid }) calls.
The data plane is event-driven. The onUplink / onDownlink callbacks
fire per audio frame. The underlying session auto-reconnects and replays the
publish/subscribe on link loss.
Related
SDK Events
The human-agent control channel — the one
EventEmitter surface.Calls API
The control-plane routes behind
calls and Call.Browser audio capture
How the softphone taps encoded Opus onto QUIC.
Python / Go / Rust
The same Voice client in the other languages.

