Skip to main content

Calling & Call Control

Make and receive phone calls, and control them with hold, transfer, mute, and DTMF.

Making Calls

Outbound Call Flow

// Dial a phone number (E.164 format)
const call = await phone.call('+14155551234');

// Or dial an extension
const call = await phone.call('105');

// Optional: specify caller ID
const call = await phone.call('+14155551234', {
callerId: '+14155559876',
});

The call object emits events as the call progresses:

EventDescription
tryingServer is processing the call
ringingRemote party's phone is ringing
answeredCall connected, audio is flowing
heldCall placed on hold
resumedCall resumed from hold
endedCall ended

Inbound Call Flow

phone.on('incoming', (call) => {
// call.id — call identifier
// call.from — caller's number (E.164)
// call.fromName — caller's display name (if available)
// call.to — called number or extension

// Answer
call.answer();

// Or reject
call.reject(); // silent decline
call.reject('busy'); // send busy signal
});

Ring duration is controlled by the caller's routing (FMFM, dial plan, or upstream PBX). When the upstream gives up, the call ends with reason no-answer.

Multiple devices

When a call arrives, it rings all of the user's registered devices simultaneously (SIP desk phones, WebRTC softphones). The first device to answer wins — all others stop ringing.

Call Ended Reasons

call.on('ended', (reason) => {
switch (reason) {
case 'hangup':
// Normal hangup by either party
break;
case 'busy':
// Destination is busy
break;
case 'no-answer':
// No one answered
break;
case 'failed':
// Call setup failed (invalid number, network error)
break;
case 'transferred':
// Call was transferred
break;
case 'rejected':
// Remote party rejected the call
break;
}
});

Call Control

Hold and Resume

// Place the call on hold (remote party hears hold music)
call.hold();

// Resume the call
call.resume();

// Listen for hold events (local or remote hold)
call.on('held', (heldBy) => {
if (heldBy === 'remote') {
console.log('The other party put you on hold');
}
});

call.on('resumed', () => {
console.log('Call resumed');
});

Mute

// Client-side mute (stops sending audio from the microphone)
call.mute();
call.unmute();

// Check mute state
console.log(call.isMuted); // true or false

The SDK both disables the local microphone track and sends a call.mute message to the server. Server-side mute is authoritative — even if the client sends audio, it is not forwarded to the remote party.

DTMF

DTMF tones are sent inline with the audio stream using the browser's built-in RTCDTMFSender API (RFC 4733 telephone-event RTP payloads). The SDK does not expose a separate DTMF method; use RTCDTMFSender.insertDTMF() on the audio sender of the underlying RTCPeerConnection:

const sender = call.peerConnection.getSenders().find((s) => s.track && s.track.kind === 'audio');
sender.dtmf.insertDTMF('1234#');

Blind Transfer

Transfer the call to another destination immediately:

// Transfer to an extension
call.transfer('105');

// Transfer to a phone number
call.transfer('+14155551234');

// The call ends for you after the transfer
call.on('ended', (reason) => {
console.log(reason); // 'transferred'
});

Attended Transfer

Consult with the transfer target before completing the transfer:

// Step 1: Consult — your current call is held, a new call is placed
const consultCall = await call.attendedTransfer('105');

consultCall.on('answered', () => {
console.log('Consulting with transfer target...');

// Step 2: Complete — connect the original caller to the target
call.completeTransfer();

// Or cancel — hang up the consultation call, original call resumes
// consultCall.hangup();
});

If the consultation call fails (busy, no-answer), the original call is automatically resumed.

Multi-Call Handling

A single WebSocket connection supports multiple simultaneous calls. This enables call waiting, call swap, and conferencing workflows.

Call Waiting

When a second call arrives while you're on a call, the incoming event fires as usual:

phone.on('incoming', (call) => {
if (phone.activeCalls.length > 0) {
// Already on a call — this is call waiting
showCallWaitingUI(call);
} else {
showIncomingCallUI(call);
}
});

Swapping Calls

Hold the current call and answer or resume another:

// Hold current call and answer the waiting call
currentCall.hold();
waitingCall.answer();

// Later, swap back
waitingCall.hold();
currentCall.resume();

Active Calls

// List all active calls
const calls = phone.activeCalls;
// [{ id, from, to, state: 'active' | 'held' | 'ringing', direction }]

// Find a specific call
const call = phone.getCall(callId);

Direct WebSocket Usage

If you're not using the SDK, you can interact with the signalling protocol directly. See the Signalling Protocol reference for the complete message specification.

Making a Call (Raw WebSocket)

// 1. Create the peer connection and generate an SDP offer
const pc = new RTCPeerConnection({ iceServers });
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
stream.getTracks().forEach((track) => pc.addTrack(track, stream));
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);

// 2. Initiate the call with the offer in-band
ws.send(
JSON.stringify({
type: 'call.create',
req_id: 'my-call-1',
destination: '+14155551234',
sdp: offer.sdp,
})
);

// 3. Handle server messages
ws.onmessage = async (event) => {
const msg = JSON.parse(event.data);

switch (msg.type) {
case 'call.trying':
// Server assigned a call_id
const callId = msg.call_id;
break;

case 'sdp.answer':
// Server forwarded Asterisk's answer to our offer
await pc.setRemoteDescription({ type: 'answer', sdp: msg.sdp });
break;

case 'ice.candidate':
await pc.addIceCandidate({
candidate: msg.candidate,
sdpMid: msg.sdp_mid,
sdpMLineIndex: msg.sdp_m_line_index,
});
break;

case 'call.ringing':
// Play ringback tone
break;

case 'call.answered':
// Call connected — audio is flowing
break;

case 'call.ended':
pc.close();
break;
}
};

// 4. Send local ICE candidates
pc.onicecandidate = (event) => {
if (event.candidate) {
ws.send(
JSON.stringify({
type: 'ice.candidate',
call_id: callId,
candidate: event.candidate.candidate,
sdp_mid: event.candidate.sdpMid,
sdp_m_line_index: event.candidate.sdpMLineIndex,
})
);
} else {
ws.send(JSON.stringify({ type: 'ice.done', call_id: callId }));
}
};