TVH Voice docs

#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

This example is executed against the sandbox in CI (tests/e2e/specs/m6-docs.spec.ts).