Click to Dial
  • 12 Mar 2023
  • 7 Minutes to read
  • Contributors
  • Dark
    Light
  • PDF

Click to Dial

  • Dark
    Light
  • PDF

Single Request Click-to-dial

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

  1. Vodia apps: If the user has the Vodia app, you can just use the vodia scheme to initiate the call:
vodia://test.com?dial=6173998147&name=JoeDoe

This requires a recent version of the Vodia app (e.g. 1.22 on iOS). The dial parameter contains the number to dial and you may include a name, if available. The advantage of this scheme is that is is clear that the Vodia app should be used and there is no need to include any authentication information.

  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.

  1. Links without session but with an md5 hash of the concatenation of username, password, destination number, current time in seconds (since 01/01/1970) and duration in seconds for which it is valid:
http://pbx/remote_call.htm?user=123&extension=true&dest=123456789&time=current_time&duration=3600&;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+duration). Every parameter must be URL-encoded, including the resulting hash.

  1. 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. These links have the following form:
http://pbx/remote_call.htm?user=123%40domain.com&dest=123456789&auth=MTIzQGRvbWFpbi5jb206cGFzc3dvcmQ

The parameter "user" identifies the extension (123@domain.com above) that should initiate the call. The parameter "dest" indicates which number should be dialed (like above). The parameter "auth" is used for authentication purposes. It must have the form "123@domain.com:password" and it must be base64-encoded. So, the base64 encoding of "123@domain.com:password" is MTIzQGRvbWFpbi5jb206cGFzc3dvcmQ as shown. The whole url should be url_encoded as shown above.

  1. 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=MTIzQGRvbWFpbi5jb206cGFzc3dvcmQ%3D&connect=true

Click-to-dial Embedded into HTML

If you have control over the content in the web page and you want to use the browser instead of the app through teh Vodia scheme, 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:

window.addEventListener('load', () => {
  let socket, pbxcall, number
  let rid // Refresh ID
  const pending = []

  // The numbers to be called and their call-ID
  let callid = ''
  const open = (server, domain, user) => {
    socket = new WebSocket(`wss://${server}/websocket?domain=${encodeURIComponent(domain)}&user=${encodeURIComponent(user)}`)
    socket.onopen = () => {
      while (pending.length) send(pending.shift());
      refresh()
    }
    socket.onclose = () => {
      console.log("Connection closed")
      socket = false
    }
    socket.onerror = evt => {
      console.log('Error ' + evt.data)
      socket.close()
    }
    // The central message dispatch function for incoming websocket frames:
    socket.onmessage = evt => {
      const msg = JSON.parse(evt.data)
      console.log("received msg " + JSON.stringify(msg))
      if (msg.action == 'call-state') {
        for (let i = 0; i < msg.calls.length; i++) {
          const call = msg.calls[i]
          if (call['to-number'] == number) {
            callid = call['id']
          }
        }
        // Can we offer the cancel button?
        const cancelbutton = pbxcall.querySelector('[name="cancel"]')
        if (cancelbutton && callid) cancelbutton.style.display = '';
      }
    }
  }
  
  // Send something, can be a string or otherwise will be converted into string:
  const send = message => {
    if (typeof message == 'string') socket.send(message)
    else socket.send(JSON.stringify(message))
  }
  
  const refresh = () => {
    rid && clearTimeout(rid)
    rid = setTimeout(refresh, reconnectTimer / 2)
    socket.doSend({ action: 'wskeepalive' })
  }
  
  const call = (server, domain, user) => {
    !socket && open(server, domain, user)
    pending.push({ action: 'get-calls' })
    pending.push({ action: "make-call", to: number })
  }
  
  const dial = async element => {
    element.style.display = 'none'
    pbxcall = element.parentNode
    while (pbxcall && pbxcall.tagName != 'PBXCALL') pbxcall = pbxcall.parentNode;
    const info = await (await fetch('/rest/pbxlogin')).json()
    const session = await (await fetch(`https://${info.server}/rest/system/session`, {
      method: 'POST',
      body: JSON.stringify({name: 'session', value: info.session})
    })).json()
    number = pbxcall.attributes['number'].value
    call(info.server, info.domain, info.user)
  }
  
  const cancel = element => {
    if (callid) {
      element.style.display = 'none'
      send({ action: "clear-call", id: String(callid) })
      callid = ''
    }
  }
  
  // Find all the elements that should be clickable:
  const elements = document.getElementsByTagName('pbxcall')
  for (let i = 0; i < elements.length; i++) {
    const element = elements[i]
    if (element.attributes.number) {
      const callbutton = element.querySelector('[name="call"]')
      callbutton && callbutton.addEventListener('click', even => {
        event.preventDefault()
        dial(event.target)
      })
      const 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.


Was this article helpful?

What's Next