Play synthesized audio back to the caller with bridge.publishDownlink().
Once you own the audio bridge for a call, everything your text-to-speech engine
produces goes to the caller through one method: publishDownlink(frame). Each
call pushes a single encoded audio frame — typically a 20 ms Opus packet —
onto the voice/<sid>/downlink track, and the engine paces it to the caller’s
leg (transcoding to G.711 on a PSTN call for you).This is the send side of the bring-your-own ASR/TTS
pattern. For the receive side — feeding caller speech into your recognizer — see
Stream Audio to ASR.
Open the bridge with attach(), then forward each synthesized chunk straight to
publishDownlink. attach() always subscribes uplink too, so pass an onUplink
callback even if you only care about playback here (a no-op is fine, or wire it to
your ASR).
TypeScript
Python
import { Voice } from "@telequick/sdk/voice";const v = new Voice({ baseUrl: "https://engine.telequick.dev", apiKey: process.env.TELEQUICK_CREDENTIALS!, orgId: "org_abc",});const call = await v.calls.originate({ to: "+15551234567", from: "+15558675309", trunkId: "trunk_main",});const bridge = await v.audioBridge.attach(call.sid, { codec: "opus", onUplink: (frame, tsUs) => {}, // playback-only here});// stream synthesized Opus straight to the callermyTts.onChunk((opus) => bridge.publishDownlink(opus));
publishDownlink expects an encoded frame in the codec you asked for in
attach(). With codec: "opus" push Opus packets; with codec: "pcm16" push raw
PCM16. Send one frame per call — a 20 ms tick — rather than a whole utterance in a
single buffer, so playback stays smooth and barge-in can cut it off cleanly.
Many realtime and TTS models hand you 16-bit PCM rather than Opus. Ask for
pcm16 and the bridge transcodes both directions — you push exactly what the
model gives you.
TypeScript
Python
const bridge = await v.audioBridge.attach(call.sid, { codec: "pcm16", sampleRate: 16000, onUplink: (pcm, tsUs) => model.appendAudio(pcm),});// model → caller: no re-encode in your processmodel.onAudioOut((pcm) => bridge.publishDownlink(pcm));
bridge = v.audio_bridge.attach( call.sid, codec="pcm16", sample_rate=16000, on_uplink=lambda pcm, ts_us: model.append_audio(pcm),)# model → caller: no re-encode in your processmodel.on_audio_out(lambda pcm: bridge.publish_downlink(pcm))
publishDownlink stamps each frame with a monotonic clock when you omit the
timestamp — fine for live streaming. If you are replaying buffered audio or
splicing clips, pass a microsecond timestamp so frames land in order.
TypeScript
Python
let tsUs = 0n;for (const frame of opusFrames) { bridge.publishDownlink(frame, tsUs); tsUs += 20_000n; // 20 ms per frame, in microseconds}
ts_us = 0for frame in opus_frames: bridge.publish_downlink(frame, timestamp_us=ts_us) ts_us += 20_000 # 20 ms per frame, in microseconds
Keep the bridge reference alive for the whole call — if it is garbage-collected
both tracks drop. When the call ends, close() the bridge before you hangup()
so the downlink track tears down cleanly.