Documentation Index
Fetch the complete documentation index at: https://developers.telnyx.com/llms.txt
Use this file to discover all available pages before exploring further.
Call State Lifecycle
A call is a state machine. Understanding every state and transition is essential for building a reliable UI and handling edge cases like reconnection, transfer, and one-way audio.
State Diagram
All States
| State | Direction | Description | What your app should do |
|---|
new | Outbound | Call object created, ICE gathering | Show “Connecting…” |
ringing | Outbound | Remote party’s phone is ringing | Show ringing UI, play ringback tone |
ringing | Inbound | Incoming call waiting | Show incoming call UI, ring tone |
active | Both | Call connected, media flowing | Show in-call UI, start timer |
held | Both | Call on hold (sendonly) | Show held state, dim audio |
reconnecting | Both | Media path lost, attempting recovery | Show reconnecting banner |
destroyed | Both | Call ended | Show call ended, clean up UI |
Outbound Call States (Detailed)
new → ringing
const call = client.newCall({
destinationNumber: '+12345678900',
audio: true,
});
// call.state === 'new'
// SDK is: gathering ICE candidates, preparing SDP, sending INVITE
What happens internally:
- SDK creates a PeerConnection
- ICE gathering starts (host → srflx → relay candidates)
- SDP offer created with codec preferences
- INVITE sent over WebSocket to VSP
- VSP translates to SIP INVITE → carrier
Your app: Show “Calling…” with a spinner. Don’t start the call timer yet.
ringing
// Remote party's phone is ringing
// call.state === 'ringing'
What happens:
- Remote phone is ringing (SIP 180 Ringing)
- You may hear ringback tone (generated locally by the SDK or played from network)
Your app: Play ringback tone if SDK doesn’t auto-play it. Show “Ringing…” state.
ringing → active
// Remote party answered
// call.state === 'active'
What happens internally:
- SIP 200 OK received from carrier
- SDP answer processed — codecs and ICE candidates agreed
- DTLS handshake completes — media is encrypted
- SRTP audio starts flowing in both directions
- Audio element auto-created and attached to DOM
Your app: Start call timer. Show in-call controls (mute, hold, hangup). Check audio is playing.
Inbound Call States (Detailed)
ringing (incoming)
client.on('telnyx.notification', (notification) => {
if (notification.type === 'callUpdate') {
const call = notification.call;
if (call.state === 'ringing' && call.direction === 'inbound') {
// Incoming call!
const from = call.remotePartyNumber;
const to = call.remotePartyName;
showIncomingCallUI({ from, to, call });
}
}
});
What happens internally:
- VSP receives SIP INVITE from carrier
- VSP pushes invite message to SDK over WebSocket
- SDK creates a Call object with
state: 'ringing'
telnyx.notification fires with callUpdate
Your app: Show incoming call UI. Play ringtone. Offer Accept/Reject buttons.
ringing → active (answer)
// User clicks "Accept"
call.answer();
// call.state → 'active'
What happens internally:
- SDK sends 200 OK over WebSocket
- getUserMedia() — browser requests microphone permission
- ICE gathering starts
- SDP answer sent
- DTLS handshake
- Media flows
** Important:** call.answer() triggers getUserMedia(). If the user hasn’t granted microphone permission, the browser will show a permission dialog. The call won’t be fully active until permission is granted.
ringing → destroyed (reject)
// User clicks "Reject"
call.hangup();
// call.state → 'destroyed'
What happens: SDK sends SIP 487 Request Terminated (or CANCEL if INVITE still in progress).
Active Call States
active → held
call.hold();
// call.state → 'held'
What happens:
- SDK sends re-INVITE with
sendonly media direction
- Remote party’s audio continues (they hear hold music if configured)
- Your audio stops sending (microphone muted at SIP level)
- Remote party receives a
callUpdate with their call state changing
held → active
call.unhold();
// call.state → 'active'
What happens:
- SDK sends re-INVITE with
sendrecv media direction
- Two-way audio resumes
Reconnecting State
// Network interruption during call
// call.state → 'reconnecting'
What triggers it:
- ICE connectivity checks fail (network change)
- DTLS session breaks
- WebSocket still connected but media path lost
What the SDK does:
- ICE restart — re-gathers candidates
- Attempts to re-establish DTLS
- If successful →
call.state → 'active' (call resumes)
- If fails after timeout →
call.state → 'destroyed' (call drops)
Your app: Show a “Reconnecting…” banner. Don’t hang up — let the SDK try to recover. See Handle Reconnection for details.
Destroyed State
All calls end up here. It’s terminal — no further transitions.
client.on('telnyx.notification', (notification) => {
if (notification.call.state === 'destroyed') {
const call = notification.call;
console.log(`Call ended. Direction: ${call.direction}`);
console.log(`Duration: ${call.duration}s`);
console.log(`End reason: ${call.cause}`); // normal, hangup, timeout, error
}
});
Common causes:
| Cause | Direction | Why |
|---|
normal | Both | Normal hangup — either party ended the call |
originatorCancel | Outbound | Caller hung up while ringing |
timeOut | Outbound | No answer within timeout period |
rejected | Inbound | Callee rejected the call |
error | Both | Network error, ICE failure, or server error |
replaced | Both | Call was replaced (attended transfer) |
Your app: Clean up UI. Upload call report. Show call summary if applicable.
State Transition Matrix
| From | To | Trigger | Direction |
|---|
new | ringing | INVITE sent/received | Outbound |
ringing | active | answer() / 200 OK | Both |
ringing | destroyed | hangup() / reject / timeout | Both |
active | held | hold() | Both |
held | active | unhold() | Both |
active | destroyed | hangup() / BYE | Both |
held | destroyed | hangup() | Both |
active | reconnecting | ICE/DTLS failure | Both |
reconnecting | active | Media restored | Both |
reconnecting | destroyed | Timeout | Both |
Common Pitfalls
Double answer
// WRONG — answering an already-active call
call.answer(); // First answer — starts PeerConnection
call.answer(); // Second answer — creates ANOTHER PeerConnection!
// Result: Two PeerConnections, the second one never connects properly
// SDK bug: CallReportCollector may track the wrong PC → reports show zero audio
Fix: Guard against double answer in your UI:
let answered = false;
function answerCall(call) {
if (answered || call.state !== 'ringing') return;
answered = true;
call.answer();
}
Not handling destroyed
// WRONG — only checking for 'active'
if (call.state === 'active') {
startTimer();
}
// What if the call goes to 'destroyed'? Timer keeps running forever.
// CORRECT — handle both
if (call.state === 'active') {
startTimer();
} else if (call.state === 'destroyed') {
stopTimer();
cleanupCall(call);
}
Missing reconnecting
// If you don't handle reconnecting, the user thinks the call dropped
// and tries to call again — creating a second call
// Show a banner so the user knows to wait
if (call.state === 'reconnecting') {
showReconnectingBanner();
}
See Also