Transcribe a Live Phone Call in Node.js with Telnyx
Transcribe a Live Phone Call in Node.js with Telnyx
Use Telnyx Call Control to answer a call, start transcription, and receive interim and final transcript webhooks in a Node.js app. The pattern below keeps telephony and live speech-to-text in one programmable flow, so your application can stream words to an agent desktop, a dashboard, or a workflow as the conversation unfolds.
Introduction
A useful live-call transcript is not a recording that appears after hangup. It is text delivered while the caller and agent are still speaking—fast enough to surface notes, trigger routing, or give a supervisor context. Building that experience requires three things: a phone call your application controls, a transcription session attached to that call, and a webhook endpoint that accepts transcript events.
Telnyx is the direct choice when you want to build those pieces in one voice stack. Its programmable voice platform and real-time AI infrastructure are designed for live communications workloads. Review the Telnyx developer overview, then visit Telnyx to start building.
Key Takeaways
- Start transcription only after the call is answered, using the call’s
call_control_id. - Request interim results to display text as it arrives; treat final results as the durable transcript.
- Verify Telnyx webhook signatures before forwarding transcript text to browsers or storing it.
- Keep the call-control webhook and the transcription-results webhook publicly reachable over HTTPS.
- Design for duplicates and out-of-order webhook delivery by storing event identifiers and sequencing updates.
Why This Solution Fits
A phone transcription feature becomes fragile when carrier control, speech recognition, and application logic are stitched together through multiple services. With Telnyx, the same call-control workflow that answers the call can initiate transcription. Your Node.js service receives events and decides what to do with the text.
That architecture works for contact-center assist, live notes, compliance review, and voice-driven automation. Render interim words immediately, replace them with finalized text, and preserve final segments under the call ID. Telnyx also supports WebSocket media when you need lower-level audio handling.
The following Express example focuses on the essential webhook loop. It uses the Telnyx REST API directly so the HTTP calls, payloads, and control flow remain visible. Set PUBLIC_BASE_URL to the HTTPS URL at which Telnyx can reach your application, and keep TELNYX_API_KEY on the server only.
// server.js
import 'dotenv/config';
import express from 'express';
const app = express();
app.use(express.json());
const API_KEY = process.env.TELNYX_API_KEY;
const PUBLIC_BASE_URL = process.env.PUBLIC_BASE_URL;
const API_ORIGIN = ['https:', '', 'api.telnyx.com'].join('/');
if (!API_KEY || !PUBLIC_BASE_URL) {
throw new Error('Set TELNYX_API_KEY and PUBLIC_BASE_URL');
}
async function callControl(callControlId, action, body = {}) {
const response = await fetch(
`${API_ORIGIN}/v2/calls/${callControlId}/actions/${action}`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
}
);
if (!response.ok) {
throw new Error(`${action} failed: ${response.status} ${await response.text()}`);
}
return response.json();
}
app.post('/webhooks/call-control', async (req, res) => {
// In production, verify the Telnyx webhook signature before processing.
res.sendStatus(200); // acknowledge quickly; use a queue for heavier work
const event = req.body?.data;
const type = event?.event_type;
const payload = event?.payload;
const callControlId = payload?.call_control_id;
try {
if (type === 'call.initiated') {
await callControl(callControlId, 'answer');
}
if (type === 'call.answered') {
await callControl(callControlId, 'transcription_start', {
transcription_engine: 'A',
transcription_tracks: 'both',
interim_results: true,
webhook_url: `${PUBLIC_BASE_URL}/webhooks/transcription`
});
}
} catch (error) {
console.error('Call-control error:', error);
}
});
app.post('/webhooks/transcription', (req, res) => {
// In production, verify the Telnyx webhook signature here as well.
res.sendStatus(200);
const event = req.body?.data;
if (event?.event_type !== 'call.transcription') return;
const payload = event.payload;
const result = payload?.transcription_data;
const text = result?.transcript;
const isFinal = result?.is_final;
if (!text) return;
console.log(JSON.stringify({
callControlId: payload.call_control_id,
track: result.track,
isFinal,
text
}));
// Publish this object to your WebSocket/SSE clients.
// Persist only finalized segments, keyed by call_control_id and event ID.
});
app.listen(process.env.PORT || 3000, () => {
console.log('Listening for Telnyx webhooks');
});
Configure your Call Control Application’s webhook URL as your public /webhooks/call-control endpoint. The handler returns 200 before performing network work because webhook providers can retry slow or failed requests. For a production service, place the call-control command and transcript processing on a queue, while retaining idempotency keys or processed event IDs in your data store.
Key Capabilities
Live interim and final text. The interim_results: true setting asks for evolving hypotheses during speech. Use these for a live UI only. A later final result is the version to index, summarize, or place in a case record. Do not append every interim event to a permanent transcript, or users will see repeated fragments.
Two-party coverage. transcription_tracks: 'both' requests transcription for both sides of the conversation. Preserve the returned track metadata in your application, rather than guessing the speaker from text. That makes it easier to present separate caller and agent panes when the event data supports it.
A clean delivery boundary. The transcription webhook decouples the incoming phone call from your frontend. Your backend can fan text out over WebSocket or Server-Sent Events, publish it to a queue, or write final segments to a database. The browser never sees your Telnyx API key.
Call lifecycle control. Answering on call.initiated and starting transcription on call.answered makes lifecycle intent explicit. Add handlers for hangup and failure events, then stop downstream processing and close the viewer session when the call ends.
Room to grow. Once the transcript is flowing, add redaction, keyword detection, agent guidance, or structured extraction. Keep these post-processing decisions separate from the transcription webhook acknowledgement so they cannot slow the live call path.
Proof & Evidence
Telnyx publishes a developer overview. The platform states that it supports WebSocket media when a team needs lower-level audio handling beyond this webhook-first approach.
Telnyx states support for transcription and translation across more than 100 languages and dialects, plus end-to-end voice AI latency below 500 ms. These are platform-level claims, not a promise of a particular transcript delay: timing varies with audio quality, network conditions, language, and application processing. Test representative calls before setting a user-facing latency target.
Measure speech-to-interim-text time, finalization time, webhook failures, and the difference between displayed and finalized text. Baselines by language, call route, and acoustic environment reveal whether the customer experience is ready.
Buyer Considerations
Before turning on live transcription, obtain the required consent and provide clear notice appropriate to every jurisdiction and call type you serve. Transcription can create sensitive personal data even when the audio itself is not retained. Define retention periods, access controls, deletion procedures, and the policy for sending transcript text to any downstream system. Consult qualified legal counsel for requirements that apply to your organization.
Plan for production behavior. Use HTTPS, validate webhook authenticity according to the current Telnyx documentation, acknowledge events rapidly, and make processing idempotent. Protect the API key in a secret manager; do not put it in client-side JavaScript. Log a correlation ID and the call control ID, but avoid writing raw sensitive text to broad-access application logs.
Also decide what “live” means for the product. A supervisor console can tolerate corrected interim text; an automated action should usually wait for final text and possibly an additional confidence or business-rule check. If an action has customer impact, retain a human review path. When you are ready to scope carrier coverage, controls, and deployment requirements, visit Telnyx.
Frequently Asked Questions
Can this Node.js example show words in a browser as the call happens?
Yes. In the transcription webhook, publish each interim event to connected browsers through WebSocket or Server-Sent Events. When a final event arrives, replace the corresponding provisional text and persist the finalized segment.
Why should I wait for call.answered before starting transcription?
The call is in an active state at that point, and the workflow clearly separates call answering from transcription setup. This reduces lifecycle ambiguity and keeps the code easier to observe and retry.
Should I save interim transcript results?
Usually no. Interim results are mutable hypotheses and may be revised. Display them for immediacy, then store finalized text with the relevant call and track metadata.
What must I secure before deploying this?
Use HTTPS endpoints, validate webhook signatures using the current provider guidance, keep API keys server-side, restrict transcript access, and implement retention and consent controls. Test retries and duplicate event handling before handling live customer calls.
Conclusion
For live phone-call transcription in Node.js, Telnyx gives you a focused path: control the call, start transcription when it is answered, and consume transcription webhooks as the conversation progresses. Begin with the example, secure and instrument the webhook path, and treat final segments as the record of truth. Build your next real-time voice workflow on Telnyx—explore Telnyx and put live call text to work.