Click to Dial

Single Request Click-to-dial

Calls can also be initiated from the PBX using four different types of web links.

  1. Links without session and password: When the PBX sends out emails, it includes links that contain the number to dial and the account to use, but not the authentication information. These links have the following form:

    http://pbx/remote_call.htm?user=123%40domain.com&dest=123456789

    The parameter "user" identifies the extension that should initiate the call. The parameter "dest" indicates which number should be dialed. The PBX will challenge the user, and the user must answer the challenge with the username and the password. The username must contain the domain (e.g. "123@domain1.com") if there are several domains on the system.

  2. Links without session but with an md5 hash of the concatenation of username, password, destination number and current time in seconds (since 01/01/1970), which are valid for 2 hours:

    http://pbx/remote_call.htm?user=123&extension=true&dest=123456789&time=current_time&auth=hash

    The parameter "user" identifies the extension that should initiate the call. The parameter "extension=true" makes sure the pbx uses the kind of authentication used here. The parameter "dest" indicates which number should be dialed. The parameter "auth" is used for authentication purposes. It is an md5 hash and calculated as md5(user+pass+dest+time). Every parameter must be URL-encoded, including the resulting hash.

  3. Links without session but with password: In some situations, you cannot answer the challenge from the PBX. For example, if you have a script that makes the PBX start a call, you want to include the credentials in the link already. These links have the following form:

    http://pbx/remote_call.htm?user=123%40domain.com&dest=123456789&auth=123%40domain.com%3Asecret

    The parameter "user" identifies the extension that should initiate the call. The parameter "dest" indicates which number should be dialed (like above). The parameter "auth" use used for authentication purposes. It must have the form "username:password" and it must be URL-encoded.

  4. Links without 'press 1' prompt: In some situations, you want to be connected to the destination without having to listen the message 'press 1 to continue' and pressing the digit '1'. You can use 'connect=true' parameter as shown below to achieve this.

    http://pbx/remote_call.htm?user=123%40domain.com&dest=123456789&auth=123%40domain.com%3Asecret&connect=true

Click-to-dial Embedded into HTML

If you have control over the content in the web page, you can embed an more elaborate click to dial where you don't have to expose the password of the user and also offer a hangup button. It would also be possible to add more controls, for example a hold button.

It is important to note that this way of doing click to dial will assume that there is a user of the PBX sitting in front of the web browser. It is not suitable for generating callbacks to the public.

In order to make this happen, we embed an element that is easy to find after the HTML document has loaded:

<pbxcall number="6173998147">6173998147 <button name="call">Call</button><button name="cancel" style="display:none">Cancel</button></pbxcall>

The tag name "pbxcall" could be anything, however we will use that tag name later in the JavaScript code that needs to be loaded in order to set everything up. The attribute "number" will tell the script what number to use. There must be a button with the name attribute "call" and there may be a button with the name attribute "cancel" if you want to offer the cancellation of a call. There may be more than one pbxcall elements on a page, however the script will cancel only the last call. The styling of the element if completely up to the web designer.

The JavaScript that looks for the pbxcall elements needs to be included in the page for example with a <script type="text/javascript" src="/js/callback.js"></script> element. The script does not do anything unless the user clicks on a dial button. After that it fetches credentials from the REST API of the web server and then uses the token provided by it to log in to the PBX, set up a cookie for that connection and then starts a WebSocket connection to the PBX that is used for the remote control of the users phone. It dials the number provided in the number attribute, and if the cancel button is available offers to cancel the call after it was started. It looks like this:

//
// JavaScript code that adds phone click to dial to the web page
//
// (C) Vodia Networks 2019
//
// This file is property of Vodia Networks Inc. All rights reserved.
// For more information mail Vodia Networks Inc., info@vodia.com.
//

'use strict';

window.addEventListener('load', () => {
  var socket;
  var pending = [];
  var pbxcall;
  var number; // The numbers to be called and their call-ID
  var callid = '';

  var open = function(server, domain, user) {
    socket = new WebSocket('wss://' + server + '/websocket?domain=' + encodeURIComponent(domain) + '&user=' + encodeURIComponent(user));

    socket.onopen = function () {
      while (pending.length) send(pending.shift());
      refresh();
    }

    socket.onclose = function () {
      console.log("Connection closed");
      socket = false;
    }

    socket.onerror = function (evt) {
      console.log('Error ' + evt.data);
      socket.close();
    }

    // The central message dispatch function for incoming websocket frames:
    socket.onmessage = function (evt) {
      var msg = JSON.parse(evt.data);
      console.log("received msg " + JSON.stringify(msg));
      if (msg.action == 'call-state') {
        for (var i = 0; i < msg.calls.length; i++) {
          var call = msg.calls[i];
          if (call['to-number'] == number) {
            callid = call['id'];
          }
        }

	// Can we offer the cancel button?
        var cancelbutton = pbxcall.querySelector('[name="cancel"]');
	if (cancelbutton && callid) cancelbutton.style.display = '';
      }
    }
  }

  // Send something, can be a string or otherwise will be converted into string:
  var send = function(message) {
    if (typeof message == 'string') socket.send(message);
    else socket.send(JSON.stringify(message));
  }

  var rid; // Refresh ID
  var refresh = function() {
    rid && clearTimeout(rid);
    rid = setTimeout(refresh, reconnectTimer / 2);
    socket.doSend({ action: 'wskeepalive' });
  }

  var call = function(server, domain, user) {
    !socket && open(server, domain, user);
    pending.push({ action: 'get-calls' });
    pending.push({ action: "make-call", to: number });
  }

  var dial = function(element) {
    element.style.display = 'none';
    pbxcall = element.parentNode;
    while (pbxcall && pbxcall.tagName != 'PBXCALL') pbxcall = pbxcall.parentNode;
    fetch('/rest/pbxlogin').then(res => res.json()).then(info => {
      fetch('https://' + info.server + '/rest/system/session', {
        method: 'POST',
        body: JSON.stringify({name: 'session', value: info.session})
      }).then(res => res.json()).then(session => {
        number = pbxcall.attributes['number'].value;
        call(info.server, info.domain, info.user);
      });
    });
  }

  var cancel = function(element) {
    if (callid) {
      element.style.display = 'none';
      send({ action: "clear-call", id: String(callid) });
      callid = '';
    }
  }

  // Find all the elements that should be clickable:
  var elements = document.getElementsByTagName('pbxcall');
  for (var i = 0; i < elements.length; i++) {
    var element = elements[i];
    if (element.attributes.number) {
      var callbutton = element.querySelector('[name="call"]');
      callbutton && callbutton.addEventListener('click', (event) => {
        event.preventDefault();
        dial(event.target);
      });

      var cancelbutton = element.querySelector('[name="cancel"]');
      cancelbutton && cancelbutton.addEventListener('click', (event) => {
        event.preventDefault();
        cancel(event.target);
      });
    }
    else {
      console.log("Element has no number attribute");
    }
  }
});

In order to get the session token needed from the PBX, there needs to be a back-end code that fetches that token from the PBX. This is done using the third-party login REST API of the PBX. The credentials for that step are stored server-side and are not exposed to the user. In our example we just use the credentials for a system administrator of the PBX. In PHP, it could look like this:

<?php
// Get a login token for the PBX
$pbxuser = 'admin';
$pbxpass = 'bigsecret';
$pbxadr = 'pbx.vodia.com';
$username = '123';
$domain = 'customer.vodia.com';

$data = array(
  'name' => '3rd',
  'domain' => $domain,
  'username' => $username
);

$options = array(
  'http' => array(
    'method'  => 'POST',
    'content' => json_encode($data),
    'header'=> "Authorization: Basic " . base64_encode($pbxuser . ':' . $pbxpass) . "\r\n" .
               "Content-Type: application/json\r\n" .
               "Accept: application/json\r\n"
    )
);

$url = 'https://' . $pbxadr . '/rest/system/session';
$context = stream_context_create($options);
$result = file_get_contents($url, false, $context);
$sessionid = json_decode($result);

header('Content-Type: application/json');

$info = array(
  'session' => $sessionid,
  'server' => $pbxadr,
  'user' => $username,
  'domain' => $domain
);

print(json_encode($info));
?>

It is important to check if the user has really logged in. In a CRM system this should be easy as there is already a user logged into the page. This is important because the front end does not have any other authentication and the fron end user could in theory use the token to make calls to any destination. It might also be necessary to include CORS headers; however in our example it worked right away without those headers.