Our voice agents can hand a live phone call to a human. Getting there took two rewrites, and the first one shipped without ever delivering a single call — while our own logs said it worked. This is what the feature does now, and what the failure taught us.
What the caller hears
Someone asks to speak to a person, or the agent decides the question is not its own: a complaint, a payment, something outside its instructions. The agent says one short line, and from there the caller hears real ringing — the second number is being dialled.
When the phone is answered, the agent goes quiet. Two people are on the line and nothing else is on it with them. If nobody answers, nothing happens: the caller stays with the agent, which knows the dial failed and can offer a callback or take a message.
The route a transfer takes
A transfer here is not "hand the line to the carrier". It is a second call, dialled into the same conversation.
- Is the number on the agent's approved list? If not, refused — and the agent is still on the line.
- Is this a phone call at all? A messenger chat or a web voice widget has no second line to connect.
- Dial the second number into the same call. The caller hears ringing; the agent is still there.
- Did they pick up within 45 seconds? If not, the dial is cancelled and the caller stays with the agent.
- They picked up — the agent loses the right to speak, and the two of them talk.
Both gates sit before anything is dialled, so a refusal never costs the caller a single ring. The pickup fork is the only place where a call becomes a transferred one.
Why not SIP REFER
Telephony has a cheaper way: a REFER, after which the carrier connects the two parties itself and you leave the call — the minutes stop flowing through you.
We built that first. It never delivered a call. Telnyx answered every REFER with 200 OK and did nothing: no progress NOTIFY, no ring at the destination. Meanwhile our own bookkeeping recorded outcome = transferred, so the dashboards were green while the caller was simply dropped.
The documentation explains it. REFER-based external transfer on a SIP trunk requires an active inbound call to your own number to match against, plus a Diversion header carrying that number. We were sending REFER on calls we originated — the one shape it cannot work on. And LiveKit's TransferSIPParticipant has no field for a custom header, so the requirement is unreachable from there anyway.
Two other explanations were tested and disproved rather than assumed: enabling third-party call control on the connection changed nothing, and neither did a sip: URI instead of tel:.
Three things measurement changed
Presence in the room is not a pickup signal. The dialled participant appears in the media room the moment dialling starts — measured at 310 ms, with no call at the destination. Only wait_until_answered tells you a human lifted the handset.
Removing the agent takes the call with it. Measured in isolation on a live call: RemoveParticipant, and within five seconds the room was empty, carrier leg included. The room does not outlive its agent job. So the agent is silenced, not removed.
Silencing alone is not enough. Revoking publish stops the agent being heard. Its job keeps running: nine seconds after one handover it composed and "spoke" a line that went into the call history, while it carried on transcribing two people who had just been handed to each other. It now gets an explicit stand-down.
The honest cost
Bridging means the conversation runs over our media path, and minutes are billed on both legs — exactly what a REFER avoids. In exchange it works identically on inbound and outbound calls, on any carrier, with nothing to configure on the telephony side.
If your logs can say "transferred" without anyone's phone ringing, that is the bug worth looking for first.
Originally published at dialogbrain.com, where the same piece has a flow diagram.
Top comments (0)