3CX Call Control API on 3CX Phone System version 15

5.00 avg. rating (95% score) - 2 votes

I revisited the 3CX Call Control API in one of my latest projects, this time on 3CX version 15.5. The last time I did any development work using 3CX Call Control API was in 2013 on version 11 and 3CX has grown a lot since then, providing more useful features, especially the ability to run 3CX on Debian Linux as well as on the cloud. Despite this, the Call Control API is still poorly documented, poorly maintained and officially unsupported by 3CX. All there is from 3CX regarding this API is a single blog post that has largely remained the same over the years.  Notwithstanding this, I still find the API to be powerful as it provides in-depth customization of several 3CX features and this article will share some my findings.

The first thing that captured my attention was the requirement that the build output path has to be set to “C:\Program Files\3CX Phone System\Instance1\Bin” for the API to work under 3CX v15 for Windows, as stated by 3CX. This puzzled me as an experienced .NET developer. Why would the build output path have anything to do with whether the API will work? After experimenting, I concluded that changing the build output path is totally unnecessary. What is needed is to just copy the files required by the API (3CXPhoneSystem.ini, 3cxpscomcpp2.dll, sl.dll and tcxpscom_native.dll) from the 3CX installation folder to your executable folder, and things should work as per normal. Take note that the files might change with each 3CX installation, so always update the files after you update 3CX. Also make sure to take the files from C:\Program Files\3CX Phone System\Instance1\Bin and not C:\Program Files\3CX Phone System\Bin. If the wrong files are used, you will encounter error “config server is not connected” or some other weird behaviors. I did not try the Call Control API on 3CX for Debian Linux so I cannot say whether copying the library files (or their Linux equivalents) will still be necessary.

To develop using 3CX v15 Call Control API, your app will have to be compiled in 64-bit mode with .NET Framework 4.6.1. For the project to build with no errors, you might also need to add a reference to the NETStandard 2.0.3 library, preferably via via NuGet.

Initializing the 3CX API in version 15 is still largely the same as in previous versions. The following code demonstrates how to do this, assuming all 3CX files are in the same directory as the executable:

public static void Init3CXAPI() {
 var filePath = Path.GetDirectoryName(System.Reflection.Assembly.GetEntryAssembly().Location) + "\\3CXPhoneSystem.ini";
 if (!File.Exists(filePath)) {
  throw new Exception("Cannot find 3CXPhoneSystem.ini in " + filePath);
 }
 instanceBinPath = Path.Combine(Utilities.GetKeyValue("General", "AppPath", filePath), "Bin");

 AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;

 var a = new Random(Environment.TickCount);
 PhoneSystem.ApplicationName = "3CX API App #" + a.Next().ToString(); // any unique name

 var value = Utilities.GetKeyValue("ConfService", "ConfPort", filePath);
 var port = 0;
 PhoneSystem.CfgServerHost = "127.0.0.1";
 if (!string.IsNullOrEmpty(value)) {
  int.TryParse(value.Trim(), out port);
  PhoneSystem.CfgServerPort = port;
 }
 value = Utilities.GetKeyValue("ConfService", "confUser", filePath);
 if (!string.IsNullOrEmpty(value))
  PhoneSystem.CfgServerUser = value;
 value = Utilities.GetKeyValue("ConfService", "confPass", filePath);
 if (!string.IsNullOrEmpty(value))
  PhoneSystem.CfgServerPassword = value;
 var psRoot = PhoneSystem.Root.GetDN(); // important
}

private static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args) {
 var name = new AssemblyName(args.Name).Name;
 if (name == "3cxpscomcpp2")
  return Assembly.LoadFrom(Path.Combine(instanceBinPath, name + ".dll"));
 else
  throw new FileNotFoundException();
}

Remember to access PhoneSystem.Root at the end of the process to actually initialize the 3CX object model, otherwise various API methods will not work properly. Also, according to the documentation, calling PhoneSystem.Root.Disconnect() when you finish using the API is recommended, although I find it to be unnecessary as the connection to the API will be closed once your application exits.

Access to several 3CX methods has been simplified in v15. For example, in 11, you must create an instance of the PBX from the code before accessing any call-related function such as DivertCall:

PBXConnection pbx = Utilities.CreatePbxConn();
pbx.DivertCall(callID, ext, dstNo, toVoiceMail);

In the above code, method CreatePbxConn is located in Utilities.cs, a file provided by 3CX with methods to establish connection to the 3CX config server. With 3CX version 15, the above code can be simplified too:

PhoneSystem.Root.DivertCall(callID, ext, dstNo, toVoiceMail);

Some other properties have been removed or made obsolete. For example, property ActiveConnection.HistoryIDOfTheCall previously provided a way to link a call API object with 3CX call record, as its value is stored both in the CDR output and 3CX PostgreSQL database, has now been deprecated. Although the property is still available, a compiler warning will be generated if you attempt to use the property, which is no longer stored in the database or in the CDR. Table callhistory3 which previously stored the call information is also no longer in use in v15. Instead, call information is now stored in tables cl_calls, cl_participants, cl_party_info and cl_segments. Among these, cl_participants will be the most useful as it contains the call time, answer time, end time and duration of all legs of the call:

3cx_cl_participants

There is an issue with the new database design. Because HistoryIDOfTheCall is not stored anywhere, there seems to be no way to map the API call object to an entry in the database. The call_id column in the cl_participants table is not the same as the ActiveConnection.CallID property. The former starts from 0 upon 3CX installation and increases by 1 for each call whereas the latter resets every time 3CX is restarted. A workaround is to enable call recording on the extensions you want to monitor via API, and check the value for recording_url, which looks like:

180/[+1234567890]_180-181_20180519065316(35).wav

where the number (35) in bracket is the value of the ActiveConnection.CallID property. From here you can find out the call_id value of the database entry, and identify all legs of the call in the database. The call duration (excluding ringing time) is in the billing_duration column. Column billing_cost specify the number of minutes to be billed for this call. By default it is just the value of the billing_duration column in minutes, but a custom value can be set by customizing the 3CX billing cost, see this for details.

Two possible problems remain with this approach. First, since ActiveConnection.CallID is reset with each startup of 3CX, make sure that you only check for active calls (end_time is NULL); otherwise you might end up picking an old call having the same CallID. Secondly, if user called an extension and reached its voicemail instead, column recording_url will not be updated and will remain NULL, and identifying such calls will not be possible. An easy workaround is just to consider all calls older than a few hours that have not yet been located in the database to have failed, or to disable voicemail completely for the extensions you want to monitor via the API.

Column recording_url is not used by the 3CX web portal to retrieve the recording files. The portal simply searches the recording folder for any WAV files whose names match the expected format, and shows them in the list. Hence, if you plan to convert the recordings to MP3 (for example, to save disk space), take note that any subsequent updates to the database will not be reflected on the portal, even after restarting Nginx web server service and clearing web browser cache. To fix this, a dirty workaround is to convert the file to MP3 while still keeping the extension as WAV. Most modern media players (Media Player Classic, VLC Media Player) no longer rely on the file extension to determine the format but instead read the file header, and will still play the recording file properly. Another way is to modify the obfuscated code for the 3CX web portal, located at C:\ProgramData\3CX\Data\Http\wwwroot\public\app.807e10d98cfac19e.js, so that it will retrieve both MP3 and WAV files. For more details, refer to the comments section of this article.

The credentials to access the 3CX database via pgAdmin can be found in the 3CXPhoneSystem.ini file. Section QMDatabase provides the credentials for a read-only account, whereas section CfgServerProfile provides the database admin credentials:

[CfgServerProfile]
;configuration server connection to database
;exclusively used by configuration server
DBHost=127.0.0.1
DBPort=5480
MasterDBUser=phonesystem
MasterDBPassword=XXXX
MasterTable=phonesystem_mastertable
DefFile=Objects.cls

[QMDatabase]
DBHost=127.0.0.1
DBPort=5480
DBName=database_single
dbUser=logsreader_single
dbPassword=XXXX

3CX v15 also does not allow configuring an internal call to forward to another extension on the PBX using its outbound routes. If you need this, bypass this restriction by creating a dedicated extension which will hold the credentials for a loopback SIP trunk created on the same PBX. Set the extension to allow multiple incoming and outgoing calls, and configure the outbound rules to terminate on the loopback SIP trunk you just created. This works in most cases but has the disadvantage of causing a single outgoing call on the PBX that goes through the loopback trunk to appear as two concurrent calls to 3CX.

Take note that due to a 3CX bug, the registration indicator for the loopback SIP trunk may appear to be red (e.g. not registered) despite correct credentials. If this happens, try to select the trunk and click on Refresh Registration. As long as the Register OK timestamp is updated, it means that the loopback trunk has been successfully configured:

loopback_SIP_trunk

3CX v15 does not allow stripping of more than 9 digits in the user interface for outbound rules. To achieve this, navigate to the outboundrule table and locate the rule that we want to change:

rule_strip

Then, open the outboundroute table, look for routes whose fkidoutboundrule value matches the rule that we have located:

rule_strip2

Change the value of the strip field to the desired values and go to Control Panel > Administrative Tools > Services to restart the 3CX PhoneSystem Database Server service. The new value for the strip digits should now take effect. Take note that after these changes, if you attempt to edit the rule in 3CX, the Strip Digits dropdown will be blank as the user interface does not support stripping more than nine digits. Do not save the rule from the management console, otherwise the changes will be lost and the strip digits setting will be reverted to nine. For any changes to the rules, edit the database instead. Also, to clarify any doubt, you can’t just edit the HTML of the page and add more options to the strip dropdown hoping that 3CX will take the new value. When I tried this, 3CX simply ignored the new value and stored 9 as the number of digits to be stripped.

3CX will also ignore the outbound route if the number of digits to be stripped is more than the length of the dialed number, instead of stripping it to an empty number before prepending the digits configured by the user. If this happens (and if there are no other rules matching the dialed number), the call will be rejected with code 404 and the activity log will show a cryptic error message “[Flow] Target endpoint for XXXX can not be built!” which really means “There are no outbound routes matching the number XXXX”. Furthermore, if the length of a number dialed is the same as the extension number length on the PBX (e.g. 3,4 or 5 digits), 3CX will ignore the outbound rules totally and simply attempt to make the call as an internal call.

The following code shows how to create a new outbound rule having the lowest priority that will be applicable for extensions from 100 to 199:

OutboundRule r = PhoneSystem.Root.GetTenant().CreateOutboundRule();
r.Name = "Test Route";
r.NumberOfRoutes = 3;
r.Priority = PhoneSystem.Root.GetOutboundRules().Max(x => x.Priority) + 1; // priority has to be unique, 0 means highest
r.OutboundRoutes[0].Gateway = defaultGateway;
// r.OutboundRoutes[1].Gateway = defaultGateway2;
// r.OutboundRoutes[2].Gateway = defaultGateway3;
r.DNRanges = new DNRange[] { r.CreateDNRange() };
r.DNRanges[0].From = "100";
r.DNRanges[0].To = "199";
r.Prefix = "001234";
r.Save();

To configure the route to terminate calls on an available gateway, you must pass in the exact name of the gateway, available from PhoneSystem.GetGateways() method. To retrieve the originally dialed number for a call (before stripping/prepending digits) that has been processed via the outbound routes, use the ActiveConnection.DialedNumber property.

If you are using a SIP stack such as PJSIP on a 3CX extension, for example to monitor incoming DTMF tones, as well as the 3CX call control API in the same project, there is a challenge of mapping the SIP stack call object with the 3CX API call object. pjsua_call_info has the id field of type int, but is not the same as ActiveConnection.CallID. Although they are calculated similarly, e.g. both start at 0 when 3CX or PJSIP is initialized and increment with each call, their values will not be the same unless both 3CX and PJSIP are started at the same time and receive the same set of calls.

To perform the mapping, you need to rely on field call_id, a string of type pj_str_t. This is the dialog ID of the SIP call, and available from the API as a key having value sip_dialog_set_id in the Dictionary property ActiveConnection.AttachedData. Take note that the values retrieved from PJSIP and from 3CX might not be exactly the same. For example, one could be

gg8sew0aod8fish3sxo4..

while the other could be

gg8sew0aod8fish3sxo4..b1e603l4a0

In my code, I simply take the value before the .. and link the call objects as long as the first 15 characters or so match.

Take note that sip_dialog_set_id is only available in later builds of 3CX Phone System v15.5. For example, version 15.5.10244 returns this property in ActiveConnection.AttachedData whereas version 15.5.1694 does not. This property is used by 3CX MyPhone in order to make a call and monitor its status. You can quickly check if your build supports this by opening MyPhoneServer.dll and VCEWebRTCNet.dll (found in C:\Program Files\3CX Phone System\Bin) in a hex editor, and search for sip_dialog_set_id.

I also want to mention that most 3CX .NET DLLs are not obfuscated. You can therefore decompile them using a tool such as dotPeek and take a look at the (almost) original source code. The following screenshot of the decompiled MyPhoneServer.dll shows how 3CX MyPhone retrieves the active call status in its LocalConnection.cs class, in build 1694 (left) and in build 10244 (right). As can be seen, sip_dialog_set_id is only accessed in build 10244 by using ActiveConnection[“sip_dialog_set_id”]:

3cs_local_connection_myphone_diff

Despite a lot of searching, I could not locate where exactly 3CX sets the value of sip_dialog_set_id but could only find where the value is retrieved. Maybe 3CX uses some string encryption when setting the value, making it impossible to find the references by performing a text grep on all files in C:\Program Files\3CX Phone System, or that this part of the code has somehow been obfuscated,

There is also a 3CX bug that will happen if the time zone of the machine on which 3CX is running is different from the 3CX time zone configuration, or had differed at some point in the past, or if there was a significant change in the system time, e.g. more than a few hours. If this is the case, the internal system time value kept by 3CX will appear to differ from the actual value, even after accounting for time zone settings. This causes the 3CX activity log to show past or future dates for current events. Most importantly, all 3CX digital receptionists will not auto-disconnect after the configured input timeout settings and play “Thank you, goodbye”, which is the typical behavior. Instead, the IVR call will be allowed to last until the caller hangs up. This seems to be due to some sort of timing bugs on 3CX Phone System version 15.5, which was triggered in my case when a 3CX backup from a machine on a different timezone was restored.

Another advice is that, should you wish to connect to the 3CX instance, either via a softphone such as X-Lite or via a SIP library, from the same machine as the 3CX server, always connect via the machine’s network IP address (e.g. 192.168.x.x), not 127.0.0.1 or localhost and do not use an STUN server. For whatever reasons, in my case, if I connected via 127.0.0.1 or used an STUN server, there would be audio issues with calls as well as DTMF issues. Attempting to send/receive DTMF via PJSIP in this configuration would result in error Invalid RTP version (PJMEDIA_RTP_EINVER) regardless of the DTMF method used (in-band, RFC2833 or SIPINFO). This is strange given that 3CX does not perform any DTMF analysis and simply passes any DTMF it might receive to the SIP client. Connecting via the network IP with no STUN server fixed the issue.

If your machine has multiple physical network cards, 3CX will not like it. During installation, the 3CX installer will check and refuse to install if more than one network cards is detected. If the additional network card is added after 3CX has been installed, the 3CX PhoneSystem Nginx Server Windows service will not be able to start, resulting in error 502 (Bad Gateway) when trying to access 3CX web portal. A change of IP address after installation may also confuse 3CX and cause it to stop working. The only solution is to remove the additional network card, restore the original IP configuration, reboot the computer and 3CX should work again.

Last but not least, if you are developing using this API, make sure that you test the code carefully and are confident that the methods you are using really do what you expect it to do. Just because IntelliSense says a method is available doesn’t mean it will do what you want. According to the API documentation, several methods are reserved for future use and will either do nothing or throw exception when called. Also, there’s a reason why names of objects in the API start with TCX and not 3CX. This is because in most common programming languages, variable names cannot start with digits. In Visual Studio, attempting to do so will cause a compiler error most of the times, or hard-to-find bugs in complicated projects with unusual setup. So just be aware of this and enjoy programming the 3CX Call Control API :)

 

See also

3CX programming tips & tricks

 

5.00 avg. rating (95% score) - 2 votes
ToughDev

ToughDev

A tough developer who likes to work on just about anything, from software development to electronics, and share his knowledge with the rest of the world.

8 thoughts on “3CX Call Control API on 3CX Phone System version 15

  • June 6, 2018 at 4:08 am
    Permalink

    I have been using freepbx and had a really useful script which converted call recordings from wav to mp3 (below).
    And the only real hurdle is updating the recordings filename in the SQL table. I’m guessing its table: cl_participants and field recording_url. But I’m not sure how to access via the command line. Any thoughts?

    #!/bin/bash
    recorddir=”/var/lib/3cxpbx/Instance1/Data/Recordings”
    # Start the Loop, store the path of each WAV call recording as variable $wavfile
    for wavfile in `find $recorddir -name \*.wav`; do

    # Make Variables from the WAV file names, stripping the file path with sed
    wavfilenopath=”$(echo $wavfile | sed ‘s/.*\///’)”
    mp3file=”$(echo $wavfile | sed s/”.wav”/”.mp3″/)”
    mp3filenopath=”$(echo $mp3file | sed ‘s/.*\///’)”

    # Convert the WAV files to MP3, exit with an error message if the conversion fails
    nice lame -b 16 -m m -q 9-resample “$wavfile” “$mp3file” && rm
    -frv $wavfile || { echo “$wavfile encoding failed” ; exit 1; }

    # Update the recording file extension in Database
    mysql -u root -s -N -D dbname<<<"UPDATE cl_participants SET
    recording_url='$mp3filenopath' WHERE recording_url = '$wavfilenopath'"

    done

    Reply
  • June 9, 2018 at 2:28 am
    Permalink

    Thanks for the tip! I was able to convert files to mp3, and change them in the database. Unforunately the web interface doesnt actually read the names from the db. So no matter what is in the db it still doesnt show the file. If however the mp3 file is listed as .wav file (even though in the db it remains mp3), then it is listed in the portal. Very strange. I’m guessing there is a filter in the php or html that only loads wavs. Do you know where the web portal pages are stored?

    If anyone is interested in the script let me know and I will post.

    Reply
    • ToughDev
      June 9, 2018 at 10:29 am
      Permalink

      Hi,

      It will be great if you can share the scripts :)

      On Windows, the web portal pages for the 3CX Management Console are located at C:\ProgramData\3CX\Data\Http\wwwroot. The pages for 3CX MyPhone are at C:\ProgramData\3CX\Instance1\Data\Http\Interface\MyPhone. On Linux, similar path structures exist in /var/lib/3cxpbx/. If you can’t find it, do a grep for all HTML files containing ‘3CX Phone System Management Console’ (without the quotes) and it will point you to the 3CX portal index.html file. However, 3CX version 15 uses NGINX web server and most pages have been minified/obfuscated. You will see file names such as 1.66161e3b53932a3c7fed.chunk.js so I am afraid modifying the code to show your MP3 files will be quite a feat. Earlier versions of 3CX used either the Abyss or IIS web server (an option to select at installation) with fewer obfuscated files (if I remember correctly), but still, updating the pages wouldn’t be an easy task, at least not without the source code.

      From your findings, I think 3CX doesn’t use recording_url column, but simply searches the recording folder for any WAV files whose names match the expected format, and shows them in the list. If this is the case, a dirty workaround would be to convert the file to MP3 while still keeping the extension as .wav. Most modern media players (Media Player Classic, VLC Media Player) no longer rely on the file extension to determine the format but instead read the file header, and will still play the recording file properly.

      Reply
  • June 12, 2018 at 3:15 am
    Permalink

    I’m pretty sure I found the file search for wav files.
    /var/lib/3cxpbx/Data/Http/wwwroot/public/app.807e10d98cfac19e.js

    Unfortunatley changing it didnt affect the display (although I didnt restart the web server so that could affect)

    Also I created the script for converting to mp3 replacing the file name as wav so it continues to be displayed (note: it processes yesterday’s recordings):
    https://drive.google.com/open?id=1CbeYYUDqierBZU0T9zlp5H23WRfW3I7Z

    Unfortunately I noticed the following: file is created as a wav. File can be downloaded ok. Script it run, then file is still displayed since it is an mp3 file just with a .wav extension. However when I click download, it opens a new tab and does nothing :( So would be great if someone else could test it.

    Reply
      • ToughDev
        June 12, 2018 at 11:00 am
        Permalink

        Thanks a lot for sharing the updated scripts! Glad to know that you managed to get it working.

        I have also uploaded both your scripts (first script with database modifications and second script without database changes) to the server. They can be downloaded here http://toughdev.com/public/3cx_recording_scripts.zip

        As for why your changes to the .JS file to show MP3 instead of WAV are not reflected, try to restart the Nginx web server Windows service as well as clearing your browser cache. I believe this should work.

        Reply

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>