Build a production-ready warm transfer service in Node.js using the Telnyx Voice API. Full code, webhook handling, and production patterns included.
A warm transfer is the difference between a customer feeling handed off and a customer feeling taken care of. When a support agent says "let me connect you with our billing specialist who can help," the customer expects to actually talk to that person, not to be dropped into a queue, not to repeat their story, and not to wonder whether the context survived the handoff.
Building that experience programmatically is harder than it looks. You need to keep the original agent on the line while a third party answers, bridge the audio, and clean up state when the call ends. Most CPaaS APIs expose the primitives, but stitching them together into a reliable flow is where engineering teams lose days.
This walkthrough shows how to build a production-ready warm transfer service in Node.js using the Telnyx Voice API. The full code is open source and runs in under five minutes from clone to first call.
A cold transfer is the simplest case: agent A is on a call with a customer, agent A dials agent B, agent B answers, agent A hangs up. The customer and agent B are now connected, but agent B has zero context.
A warm transfer adds a private conversation between agent A and agent B before the handoff. Agent A typically says something like "I have a customer on the line who needs help with a disputed charge on invoice 4471, can you take it?" Agent B accepts or escalates, and only then does agent A drop off.
The technical requirements:
+15551234567) or the API rejects them.Telnyx's Call Control API exposes all of this through a single SDK. The example below wires it together with Express.
┌──────────────┐ ┌──────────────────┐ ┌──────────────┐
│ Customer │ ◄────── │ Telnyx Voice │ ◄────── │ Agent A │
│ (caller) │ │ Private Network │ │ (your app) │
└──────────────┘ └──────────────────┘ └──────────────┘
│
│ (warm transfer)
▼
┌──────────────────┐
│ Agent B │
│ (third party) │
└──────────────────┘Your Node.js service sits behind the Telnyx Voice API. It receives webhooks for every call event, maintains a small in-memory state map keyed by call_control_id, and exposes HTTP endpoints that trigger call control actions.
ngrok or another tunnel for local webhook testingEdit .env with your credentials:
The Telnyx Node.js SDK follows the standard constructor pattern:
One client, one API key, one bill. Telnyx is an AI Communications Infrastructure platform: voice, messaging, SIP, AI assistants, and IoT all live behind the same authentication surface.
Each call control action maps to a single SDK call:
The transfer action is the warm transfer primitive. When you call it on an active call, Telnyx's media server bridges the third party into the existing call without dropping the original participants. The agent stays on the line until they hang up their leg.
Every call event arrives as a POST to your webhook URL. The handler reconciles state:
The in-memory callState object is fine for single-instance development. For production, swap it for Redis or PostgreSQL keyed by call_control_id so state survives restarts and scales horizontally.
Each call control action gets an HTTP endpoint so your frontend, CRM, or agent dashboard can trigger it:
The error handling matters. The Telnyx SDK throws typed errors (AuthenticationError, RateLimitError, APIStatusError, APIConnectionError) that map cleanly to HTTP status codes. Your agent dashboard can render meaningful messages instead of generic 500s.
In another terminal, expose your local server:
Set the ngrok URL as your Call Control Application's webhook URL in the Telnyx Portal. Now trigger a call:
The response includes a call_control_id. Use it to transfer:
The third party answers, the original agent stays on the line, and the handoff happens with full context.
The example above is intentionally minimal, but the pattern scales. Three things make it production-ready:
1. State is keyed by call_control_id, not by phone number. Phone numbers are not unique identifiers. A customer might call from two lines, or a transfer might loop back to the original number. The call_control_id is a UUID that Telnyx generates per call leg and is the only safe key for state.
2. Webhooks are the source of truth, not the API response. When you call client.calls.dial, you get back a call_control_id, but the call hasn't connected yet. The call.answered webhook is what tells you the customer picked up. Your UI should listen for webhooks (via WebSocket or SSE) rather than polling the API.
3. E.164 validation happens at the edge. Rejecting malformed numbers at your HTTP boundary saves a round trip to Telnyx and gives the caller a faster error. The + prefix check is a coarse filter; for stricter validation, use the Telnyx Number Lookup API.
The base example covers the happy path. Real call centers need more:
client.calls.actions.record_start(call_control_id) before the transfer to capture the full conversation for QA.call.whisper event to brief the receiving agent before the customer hears them.transfer_to with a lookup against your routing rules engine.Each of these is a small addition on top of the same call control primitives.
Warm transfers are a trust signal. When a customer hears "I'm connecting you with Maria in billing, who has all the context," and Maria actually has the context, that's the carrier layer doing its job. The audio bridged cleanly, the state reconciled, the webhook fired, and the agent dashboard updated, all because Telnyx owns the media path end to end.
That's the difference between an API you integrate and infrastructure you build on. Telnyx's private global network carries the call from your customer's phone to your agent's softphone to the third party's desk, with sub-second latency and no public internet hops. When something goes wrong, you call one support number and one team owns the fix.
The full example is in the telnyx-code-examples repository under warm-transfer-nodejs. It includes:
server.js - the complete Express applicationREADME.md - quickstart and API referenceGUIDE.md - step-by-step walkthroughAPI.md - typed endpoint reference.env.example - environment variable templateClone it, add your API key, and you have a working warm transfer service in under five minutes.
Related articles