• 10 Aug 2021
  • 5 Minutes to read
  • Contributors
  • Dark
  • PDF


  • Dark
  • PDF

You can use WebSocket to connect to the PBX. It allows bidirectional communication between the browser and the PBX.
It is mainly used for call control (including WebRTC calls), call status, other extension information as well as status information about all other relevant extensions and accounts. For example, it gives information about other extensions in the same domain whether an extension is registered or not, or whether it is in a call or idle or whether its available for chat etc.

Connecting to the PBX

After logging in to the PBX using the extension credentials, connect WebSocket to the PBX using the URL:




EXTENSION is the extension for which the relevant information will be provided and on which calls can be made and received. Live status information about the other extensions in the same domain will also be provided when something changes.

Example in JS:

var dest = "wss://";

this.socket = new WebSocket(dest);

this.socket.onopen = function (evt) { onOpen(evt) };

this.socket.onclose = function (evt) { onClose(evt) };

this.socket.onmessage = function (evt) { onMessage(evt) };

this.socket.onerror = function (evt) { onError(evt) };

The callback functions like onxxxx() will be called on those events.

For example when there is a message from the PBX, onMessage(evt) will be called. You can then parse the evt.data which is a JSON object to extract and display the relevant information.

Similarly, commands can also be sent to the PBX using the command:


Receiving messages in onMessage callback:

The onMessage callback is called when there is a message from the PBX. And the message is the argument of the function, say event:

function onMessage (event) {

Process the events here


The event.data message should be parsed as JSON format as below:

{ action: actionname , ... }

Some of the information you can get:

action : domain-state

accounts : Gives the status of all the accounts (not just yours) in the domain, like whether it is registered, DND, available for chat, in a call etc.

calls : The details of the active calls


{ action: "domain-state", messages: , calls: Array(1), accounts: Array(26) }

action : user-state

type : extensions

account : your extension

registered : true/false

chatstatus : online/offline

dnd : true/false


{ action: "user-state", type: "extensions", account: "45", domain: "localhost", chatstatus: "online", … }

action : call-state

id : Id of the call to be identified for any action that may be taken

from : Who made the call

to : To whom it was made (your extension)

callstate : connected/ringing etc.




action: "call-state",

calls: { id: "call id", from: "call from", to: "call-to", callstate: "state of the call like connected etc.", and so on ...}


Making a call and call control

In order to make a WebRTC call through the browser from the extension, you have to use the websocket connection established above. The example below may not run as is. The point was not to make the full project here but to quickly show how the API can be used to make a WebRTC call, end it, hold it, resume it etc.

Example of webscoket connection, making a WebRTC call and call control:

let peercon;
let localStream;
let dtmfSender;
let socket;
let audio;

// Websocket
function startWebSocket(url) {
  // The url will be something like: wss://
  socket = new WebSocket(url);
  socket.onopen = function (evt) { onOpen(evt) };
  socket.onclose = function (evt) { onClose(evt) };
  socket.onmessage = function (evt) { onMessage(evt) };
  socket.onerror = function (evt) { onError(evt) };

function onOpen(evt) {
  var msg = { action: "avoid-disconnect" };

function onClose(evt) {
  console.info("VodiaCall:ERROR: " + "DISCONNECTED");

function onMessage(evt) {
  var msg = JSON.parse(evt.data);
  if (msg.sdp) {
    var d = new RTCSessionDescription({ type: "answer", sdp: msg.sdp });
    peercon.setRemoteDescription(d, remoteSDPSucceeded, remoteSDPFailed);
  else if (msg.candidate) {
    peercon.addIceCandidate(new RTCIceCandidate({ candidate: msg.candidate, sdpMid: "", sdpMLineIndex: 0 }),
              function () { },
              function (error) { console.log("Candidate failed: " + JSON.stringify(error)); });
  else if (msg.bye) {
    doSend(JSON.stringify({ action: "bye-response", callid: msg.callid }));
    // Call your function onCallEnded(); to clean up a call from the UI perspective

function onError(evt) {
  console.log("VodiaCall:ERROR: " + evt.data);

function doSend(message) {

// Making a call
let vodiaPhone = {
  callDir: "none",
  callid: ""

vodiaPhone.makecall = function (destNumber, resume, callid, calldir) {
  this.callDir = calldir ? calldir : "caller";
  let DTLS = { "optional": [{ 'DtlsSrtpKeyAgreement': 'true' }] };
  peercon = new RTCPeerConnection({ "iceServers": [{ "url": "stun:stun.l.google.com:19302" }] }, DTLS);
  peercon.onicecandidate = function (evt) {
    doSend(JSON.stringify({ action: "ice-candidate", "candidate": evt.candidate }));

  peercon.onaddstream = function (evt) {
    audio = document.getElementById("remoteAudio");
    audio.srcObject = evt.stream;
    //call your function onCallConnected() to change UI etc.

  var self = this;
  navigator.mediaDevices.getUserMedia({ "audio": true, "video": false }).then(function(stream) {
    stream.getTracks().forEach(function(track) {
      peercon.addTrack(track, stream);
    localStream = stream.getTracks()[0];
    if (typeof peercon.createDTMFSender === "function")
      dtmfSender = peercon.createDTMFSender(stream.getAudioTracks()[0]);

    let options = { optional: [], mandatory: { OfferToReceiveAudio: true, OfferToReceiveVideo: false } };
      .catch(function(err) {
	  console.info("createOffer failed. Error: " + err);

    function onDescriptionReady(desc) {
      self.callid = createCallId(); // create a unique call id that you will use to control this call like hold/resume, transfer and hangup 
      peercon.setLocalDescription(desc, function (e) { // Suceeded }, function (e) { // Failed: Log it or output it });
      if (resume) {
        if (self.callDir == 'caller')
          doSend(JSON.stringify({ action: "wrtc-hold", holdcmd: "sendrecv", sdp: desc, callid: resumeId, calldir: "caller" }));
          doSend(JSON.stringify({ action: "wrtc-hold", holdcmd: "sendrecv", sdp: desc, callid: resumeId, calldir: "callee" }));
      else {
        doSend(JSON.stringify({ action: "sdp-packet", "to": destNumber, "sdp": desc, callid: self.callid }));
  }, function () { alert("media error"); });
} // makecall

// End a call, local is true if you are ending the call otherwise false if the other side is ending it
vodiaPhone.endcall = function (local, callid, cseq) {
  if (this.localStream && (local || this.callid === callid)) {
    this.peercon && this.peercon.close();
  if (local) {
    if (this.callDir == 'caller')
      doSend(JSON.stringify({ action: "sip-bye", caller: "true", callid: this.callId }));
      doSend(JSON.stringify({ action: "sip-bye", caller: "false", callid: this.callId }));
  else if (msg) {
    this.connection.doSend(JSON.stringify({ action: "bye-response", callid: callid, cseq: cseq }));
  this.callDir = 'none';

// Hold/Resume a call, callid is the id of the call to hold or resume
vodiaPhone.holdcall = function (callid) {
  this.localStream && this.localStream.stop();
  peercon && peercon.close();
  doSend(JSON.stringify({ action: "wrtc-hold", callid: this.callId, holdcmd: "sendonly", calldir: this.callDir === 'caller' ? "caller" : "callee" }));

vodiaPhone.resumecall = function (callid, calldir) {
  this.makecall(''", true, callid, calldir);

vodiaPhone.transfer = function (id, from, to) {
  let msg = { action: "blind-transfer2" };
  msg.id = id || this.callid;
  msg.from = from;
  msg.to = to;

Was this article helpful?

What's Next