#Recipe: Call logs, timelines and recordings
Every call (click-to-call, dialed from a phone, inbound, agent-to-agent) has a record with CDR fields
and a timeline of channel events (initiated, agent_answered, remote_ringing, bridged, held,
dtmf, transferred, recording_started, remote_hangup, ended, …). Recordings are stereo (agent /
other party), stored in object storage, played through signed URLs valid for 15 minutes, and deleted
after the retention period (or now, with DELETE /v1/calls/{id}/recording).
#TypeScript (SDK)
// Call log with filters, a call's event timeline, recordings and CSV export.
import { createVoiceClient } from '@voice/sdk';
const voice = createVoiceClient({ baseUrl: process.env.VOICE_API_URL!, apiKey: process.env.VOICE_API_KEY! });
const { data: page } = await voice.GET('/v1/calls', { params: { query: { limit: 5, direction: 'outbound' } } });
for (const c of page!.data) console.log(c.initiated_at, c.direction, c.from, '→', c.to, c.status, c.billable_seconds ?? 0, 's');
// More pages: pass next_cursor as cursor until it is null.
const first = page!.data[0];
if (first) {
const { data: detail } = await voice.GET('/v1/calls/{id}', { params: { path: { id: first.call_id } } });
console.log('timeline', detail!.timeline.map((e) => e.type));
if (detail!.recording?.status === 'ready') {
const { data: rec } = await voice.GET('/v1/calls/{id}/recording', { params: { path: { id: first.call_id } } });
console.log('signed recording URL valid until', rec!.expires_at);
}
}
// CSV export uses the same filters (plain HTTP: it is not JSON).
const csv = await fetch(`${process.env.VOICE_API_URL}/v1/calls/export.csv?direction=outbound`, { headers: { authorization: `Bearer ${process.env.VOICE_API_KEY}` } });
console.log((await csv.text()).split('\n')[0]);
#Notes
- Cursor pagination: pass
next_cursorascursoruntil it isnull. - CSV cells that start with
= + - @are prefixed with'(spreadsheet formula injection).
This example is executed against the sandbox in CI (tests/e2e/specs/m6-docs.spec.ts).