Record both legs of a call as a stereo WAV, fetch the recording URL from the CDR, and split channels for per-speaker analysis.
Recording captures both legs of a call as a single stereo file — the caller
on the left channel, the agent (AI or human) on the right — keyed on the
call’s sid. Because the two speakers land on separate channels, you can run
each side through its own transcript or scorer without any speaker-ID model. Once
a call ends, the recording is uploaded to your workspace’s object storage and its
URL is stamped onto the call’s CDR, where the control plane can hand you a
time-limited download link.
Recording is a workspace-level setting today — there is no per-call
record: true flag on calls.originate(...), and CallData carries no
recording field. Turn recording on once for the workspace (in the voice console /
via your operator) and every call on it is recorded; you cannot opt a single
call in or out from the SDK. A per-call record toggle is not shipped yet.
The media plane taps both RTP legs, mixes them into a stereo stream
(caller = L, agent = R), and streams it to the recording pipeline. No SDK
call is involved.
2
Finalize
On hang-up the pipeline writes a stereo WAV to your workspace’s object
storage and stamps recording_url (and the byte size) onto the call’s CDR.
3
Fetch
Read the CDR to discover the recording, then mint a short-lived presigned
download URL keyed on the sid.
The recording URL is stored on the CDR, not on the live Call object, so you
fetch it through the control-plane API rather than the audio SDK. Two routes do
the job: cdr.recordings lists calls that produced a recording, and
storage.signedUrl returns a one-hour presigned download link for a given
sid. Both are authenticated with your API key.
TypeScript
Python
const API = "https://engine.telequick.dev";const KEY = process.env.TELEQUICK_CREDENTIALS!;const ORG = "org_abc";// Small tRPC caller — the audio SDK talks to these same endpoints under the hood.async function trpc<T>(path: string, input: unknown): Promise<T> { const url = new URL(`/api/trpc/${path}`, API); url.searchParams.set("input", JSON.stringify(input)); const res = await fetch(url, { headers: { authorization: `Bearer ${KEY}` }, }); if (!res.ok) throw new Error(`${path} ${res.status}`); const body = await res.json(); return body.result.data as T;}// 1. list the most recent calls that have a recordingconst { rows } = await trpc<{ rows: Array<{ call_id: string; duration: number }> }>( "cdr.recordings", { orgId: ORG, limit: 20 },);// 2. mint a 1-hour download URL for one of themconst { url, expiresIn } = await trpc<{ url: string; expiresIn: number }>( "storage.signedUrl", { orgId: ORG, callSid: rows[0].call_id },);console.log("download for", expiresIn, "s:", url);
import os, json, urllib.parse, urllib.requestAPI = "https://engine.telequick.dev"KEY = os.environ["TELEQUICK_CREDENTIALS"]ORG = "org_abc"def trpc(path: str, params: dict): q = urllib.parse.urlencode({"input": json.dumps(params)}) req = urllib.request.Request( f"{API}/api/trpc/{path}?{q}", headers={"authorization": f"Bearer {KEY}"}, ) with urllib.request.urlopen(req) as r: return json.load(r)["result"]["data"]# 1. list recent calls that produced a recordingrows = trpc("cdr.recordings", {"orgId": ORG, "limit": 20})["rows"]# 2. mint a 1-hour download URL for one of themsigned = trpc("storage.signedUrl", {"orgId": ORG, "callSid": rows[0]["call_id"]})print("download for", signed["expiresIn"], "s:", signed["url"])
The presigned URL expires in one hour — mint it when you’re about to download,
not when you list. Requests are tenant-scoped: you can only presign recordings
that belong to your orgId, even if you guess another workspace’s sid.
The native SDK cores (Python, Go, Rust, and the other FFI wrappers) surface a
RECORDING_READY call event the moment the file is finalized — it carries
recording_url and duration_seconds, so you can process recordings as they
complete instead of polling the CDR.
from telequick import parse_call_eventdef on_event(evt_bytes): evt = parse_call_event(evt_bytes) if evt.status == "RECORDING_READY": print(evt.call_sid, evt.recording_url, evt.duration_seconds, "s") # evt.estimated_mos / evt.jitter_ms / evt.packets_lost are also populated
This event stream is exposed by the native cores. The browser/TypeScript
voice SDK has no recording-event surface — use the CDR + storage.signedUrl
path above from a browser or Node app.
The download is a stereo WAV with the caller on the left channel and the
agent on the right. Splitting the two gives you a clean per-speaker signal — this
channel split is your speaker separation. There is no built-in diarization or
voiceprint in the recording path, so run each channel through your own ASR (or
any scorer) independently.
TypeScript
Python
// Deinterleave a 16-bit PCM stereo buffer into caller (L) and agent (R).function splitStereoPcm16(interleaved: Int16Array) { const n = interleaved.length / 2; const caller = new Int16Array(n); const agent = new Int16Array(n); for (let i = 0; i < n; i++) { caller[i] = interleaved[2 * i]; // L agent[i] = interleaved[2 * i + 1]; // R } return { caller, agent };}// const wav = await (await fetch(url)).arrayBuffer();// const pcm = new Int16Array(wav, 44); // skip the 44-byte WAV header// const { caller, agent } = splitStereoPcm16(pcm);// await myAsr.transcribe(caller); // what the caller said// await myAsr.transcribe(agent); // what the agent said
import wave, urllib.requesturllib.request.urlretrieve(signed["url"], "call.wav")with wave.open("call.wav", "rb") as w: assert w.getnchannels() == 2 and w.getsampwidth() == 2 frames = w.readframes(w.getnframes())# de-interleave 16-bit stereo: even samples = caller (L), odd = agent (R)import arraypcm = array.array("h", frames)caller = pcm[0::2] # left — the calleragent = pcm[1::2] # right — the AI or human agent# my_asr.transcribe(caller.tobytes())# my_asr.transcribe(agent.tobytes())
Speaker separation here comes entirely from the stereo channel split — there
is no speaker-ID, diarization, or voiceprint model in the voice path. If both
speakers were mixed to mono you could not tell them apart; the two-channel
recording is what makes per-speaker analysis reliable.
Every recorded call also has objective quality on its CDR — an ITU-T E-model
MOS estimate plus jitter and packet-loss — computed on the media plane. Join
that to the recording when you score a call so a low transcript confidence can be
attributed to a bad line rather than a bad agent. See
MOS, jitter & loss for the
fields and how they’re derived.