Extending 3cx with a client side callscript

Calling people is an integral component in our day-to-day business, so there must be a quick way of starting calls.
In the past, we had an asterisk pbx, which can be operated in a quite flexible way.

When we became a 3cx partner, we switched our telephone system from the asterisk to 3cx and obviously, our past script wasn’t working anymore.

There is no easy way of triggering calls via e.g. a REST API.
We’ve searched on the internet, read through the 3cx forums and reddit, yet the only thing related to a REST API in 3cx was the lack thereof.
On their GitHub page, 3cx has (next to the Jenkins meme plugin) only the call flow demos which we didn’t want to try as the call flow desinger is only available for windows.
Custom solutions such as https://creomate.com/3cx-api-faq exist, however, we simply wanted to trigger calls without installing some proprietary binary on our pbx.

It is possible with the 3cx webui to initiate a call on a desk phone, so there MUST be a way to trigger it via a script, right?
Right… This was actually harder than we thought.
By using the dev tools we found out, that there are many POST requests, yet the base64 responses to those requests were partly some non-printable bytesequences, partly something like COQBog4xCAESKkNhbGwgdG8ge2Rlc3RpbmF0aW9ufSBoYXMgYmVlbiBpbml0aWFsaXplZBjjAQ== (translates to *Call to {destination} has been initialized for those who cannot read it on the spot).

So there must be something else going on, as the frontend was showing some information it couldn’t have known from the responses to the POST requests.
Going through all requests, there was one websocket request that seemed interesting.

Looking at the webtraffic, the websocket didn’t seem to do anything

And only after some time, we checked the “Response” tab, which revealed the function of the websocket:

The 3cx webclient uses POSTs for sending data from the webclient to the server and the websocket only for receiving results from the server.
Using a websocket here makes sense, as the client can notice an incoming call really fast through server push, but why the webclient isn’t using full duplex communication remains a secret.

Copying together the POST requests and the websocket responses into a python script, we played around with the parameters to try to find out, how to trigger a call on a desk phone programmatically.

We came up with a python script that takes an extension number, the password to that extension and a number to call.
It logs the user into the 3cx webclient, establishes a websocket, sends some magic bytes to get the phone configuration and finally triggers the call by providing the phone id and the phone number.

Go ahead and take a look at the code at https://github.com/helsinki-systems/3cx-callscript, there are several comments that try to explain stuff like what the call-triggering body has to include to be working.
The script is still quite rough, but it works smoothly for us and is in daily use since quite some time now.
There is also a .desktop file that, when added to the correct location and linked to the correct script location, allows clicking tel: links on websites to directly trigger calls from applications like a web browser.

Couple the call script with some other script that syncs contacts locally (e.g. by using vdirsyncer, khard and rofi) and you got yourself a nice and quick way of dialing numbers without having to type them into the phones keypad or the 3cx webclient.