Integration with IP telephony

Out of the box, the system includes integrations with various IP telephony providers. If the specific integration you need is not available, you can use this API to create your own integration. To do this, you need to create a custom module and implement several functions in the module scripts using TypeScript. Built-in and custom IP telephony integration modules do not differ fundamentally in terms of functionality. The following capabilities will be available to your module:

  • initiate an outgoing call from the system via the context menu of a Phone type field;
  • display the call page with brief customer information and call control buttons, when the system user answers an incoming call or makes an outgoing call;
  • open the customer summary page;
  • automatically create and save an app item if a call was missed and the number is not in the database;
  • register missed calls;
  • save a message about an incoming or outgoing call to the object's activity stream. If the call was connected, a link to the call recording is added to the message. It can be played in the system interface, and you can also leave a comment containing, for example, a call summary;
  • automatically create a Call app item in the Telephony workspace for each incoming and outgoing call;
  • map IP telephony user accounts to system users for routing and correct display of information about the operator who answered or made the call.

    Limitations

    Before implementing the integration module, note the following API limitations:
  1. Calls are not routed on the system side to avoid conflicts with routing settings on the telephony side and on the system side. Nearly every provider offers this capability: after the call is routed to an employee or group, the incoming call is displayed in the system for the specified users.
  2. Currently, telephony events are processed by delivering messages to a specific HTTP URL
    (webhook). If the telephony provider does not allow specifying a URL for message delivery, you must create an intermediary service that transforms the telephony provider's data into a request to the system server. To do this, you can create a [portable service in the module] (https://brix365.com/en/help/platform/portable-microservices.html).
  3. Only a link to the file is stored for call recordings. The recordings themselves are usually stored by the telephony provider. It provides only the link. You should pay attention to the file retention policy on the telephony provider's servers. Files may be available for a limited time.

    Implementation

    To create the integration module, you need to implement the following functions in the custom module scripts:
    // Check  the telephony connection. Called when the Check Connection button is clicked  on the module page 
    // Returns the connection check status, which will be  displayed to the user on the module page 
    async function  VoipTestConnection(): Promise<VoipTestConnectionResult> { } 
    // Process a  request from the IP telephony provider. Called when a request is made to the  module webhook 
    // The `request` parameter contains information about the  HTTP request, including HTTP headers and request body contents 
    // The  function returns the request processing result, based on which the system  will display notifications 
    // about an incoming call, save the call  recording, and so on. 
    async function VoipParseWebhookRequest (request: FetchRequest): Promise<VoipWebhookParseResult> { } 
    // Get the  list of users from the IP telephony provider side 
    // Called when mapping  telephony users to system users after clicking the Configure button on the  module page 
    // Returns the list of IP telephony provider users 
    async  function VoipGetMembers(): Promise<VoipMember[]> { } 
    // Generate an  outgoing call. The function is called when a system user clicks the call  button 
    // in the context menu of a Phone field or the Call Back button on  the call page. The 
    // `srcPhone` parameter specifies the caller user's  number, `dstPhone` specifies the number to call, 
    // and `ext` is the  extension. It is recommended to configure the PBX or SIP softphone so that  the extension 
    // is automatically dialed using tone dialing after the other  party answers the call. 
    async function VoipGenerateCall (srcPhone: string, dstPhone: string, ext?: string): Promise<void> { } 
    //  Get the call recording link. The function is called when a system user plays  a call recording from the 
    // feed of an App item or when saving the call  recording link in the context of the Call App item 
    // in the Telephony  Workspace. The `callData` parameter contains the data that was specified  when saving 
    // call recording information from the `VoipParseWebhookRequest`  function. The function must return a file link 
    async function  VoipGetCallLink(callData: any): Promise<string> { } 
    // Called automatically  when the webhook URL changes (for example, when the token is updated) 
    //  The `webhookUrl` parameter specifies the absolute URL of the integration  module webhook. Implementing this function is optional 
    async function  VoipOnWebhookUpdated(webhookUrl: string): Promise<void> { } 
    
    After implementing these functions and saving the script on the module page, you will find a new workspace named

Telephony Settings. This workspace will contain the webhook URL—when data is sent to this URL, the
VoipParseWebhookRequest function from the script will be called. In this function, you must process the request and return a VoipWebhookParseResult instance, based on which the system will display incoming call notifications or save call recording information. You must specify the webhook URL itself in your telephony provider settings. The URL processes GET and POST HTTP requests. The URL must also include the required
token parameter.

Implementing support for buttons on the call page

When an incoming call is received or an outgoing call is made, the system user sees a [page with information about the current call] (https://brix365.com/en/help/crm/work-with-call.html) in the lower-right corner. Depending on the call state, different buttons may be displayed on this page: Answer Call, End Call, Reassign, and so on. To display these buttons, implement the following functions in the custom telephony module:

// Answer a call. The function is called  when a system user clicks the **Answer Call** button on the call page 
//  After that, voice data starts being transmitted between the operator and the  customer 
// The `srcPhone` parameter specifies the caller's phone number,  and `dstPhone` specifies the number being called 
async function  VoipAcceptCall(srcPhone: string, dstPhone: string): Promise<void> { } 
//  End a call. The function is called when a system user clicks the **End Call**  button on the call page 
// The call between the operator and the customer  is terminated 
// The `srcPhone` parameter specifies the caller's phone  number, and `dstPhone` specifies the number being called 
async function  VoipHangupCall(srcPhone: string, dstPhone: string): Promise<void> { } 
//  Reassign a call. The function is called when a system user clicks the  Reassign button on the call page 
// The current operator's call ends, and  the customer is connected to another operator 
// The `srcPhone` parameter  specifies the caller's phone number, `dstPhone` specifies the number being  called, 
// and `redirectTo` is the extension of the operator to whom the  call is reassigned 
async function VoipRedirectCall (srcPhone: string, dstPhone: string, redirectTo: string): Promise<void> { }  
// Put a call on hold. The function is called when a system user 
// clicks  the **Put on Hold** button on the call page. Voice data transmission between  the operator and the customer stops without ending the call 
// The  `srcPhone` parameter specifies the caller's phone number, `dstPhone`  specifies the number being called, 
// and `hold` is the value `true`. It  indicates that the call should be put on hold; otherwise, it should be taken  off hold 
async function VoipHoldCall (srcPhone: string, dstPhone: string, hold: boolean): Promise<void> { } 

If a function is not implemented in the custom telephony module, the corresponding button will not be displayed on the call page.

Implementing CTI panel support for browser-based calls

If the telephony provider supports WebRTC technology, calls can be made directly from the browser using the CTI panel built into the system. It lets you work with calls from a single interface. In this case, installing a SIP softphone is not required.

Interaction between the CTI panel and telephony

Telephony integration in the system CTI panel is implemented through a custom module widget. When an employee selects a telephony provider in the CTI panel, a widget instance is created automatically. Its client scripts call the connect function to initialize the connection to the telephony provider. The widget allows you to:

  • establish a communication channel for exchanging signaling protocol messages. For example, Asterisk telephony uses the SIP over WebSocket protocol (RFC 7118), which makes it possible to transmit SIP signaling over a WebSocket connection;
  • process events from the CTI panel and send them to telephony. For example, when a system user clicks the outgoing call button in the CTI panel, the BrowserVoipClient.call method is called. As a result, the widget must initiate an outgoing call using the telephony protocol and return an object with call information to the system. For more details about data types, see the example below;
  • process events from telephony and send them to the CTI panel. For example, when the telephony provider sends a signal about a new incoming call, the widget must call the BrowserVoipEventSink.callReceived method so that the CTI panel displays information about the incoming call;
  • configure the WebRTC connection to transfer the media stream between the user's browser and telephony. In particular, the widget must exchange SDP descriptions and ICE candidates, and it must also monitor the state of the WebRTC session.

    Configuring a custom widget to work with the CTI panel

    To implement a widget for interaction with the CTI panel, follow these steps:
  1. In the custom telephony module, [create a widget] (https://brix365.com/en/help/platform/extention-widegets.html) with the following parameters:
    • Name — add the widget name;
    • Widget code — specify the value browser_voip_client. This code is used by the system to determine which specific widget from the module should be used for integration with the CTI panel;
    • Extension — leave blank.
  2. In the interface designer that opens, on the Scripts tab, add client scripts with interface descriptions for working with the CTI panel:
    • telephony connection data:
      interface  BrowserVoipClientCredentials {
           // Telephony user extension  
           extension: string; 
           // Telephony user login  
           username: string; 
           // Telephony user password  
           password: string; 
      } 
      
    • call management through the CTI panel. All methods of the BrowserVoipClient interface except getCallId are required to be implemented:
      // Established telephony connection 
      // @template TIncomingCall  Incoming call 
      // @template TOutgoingCall Outgoing call 
       interface BrowserVoipClient<TIncomingCall = unknown, TOutgoingCall =  unknown> { 
           // Disconnect from telephony. Called by the system  when the user turns off the CTI panel 
           disconnect():  Promise<void>; 
           // Make an outgoing call. Called by the system  when the user enters a number 
           // in the dialer and clicks the  Make Outgoing Call button. It can also 
           // be called when making  a call through the context menu of the Phone Number field 
           // @ param phoneNumber Phone number to call 
           // @param ext Extension  that must be dialed automatically in tone mode 
           // after the  connection is established 
           // @returns An object representing a  call. In this object, the widget can store 
           // any call data  
           // The object will be passed to other methods of the  `BrowserVoipClient` interface when the CTI panel needs 
           // to  change the call state 
           call(phoneNumber: string, ext?: string):  Promise<TOutgoingCall>; 
           // Answer an incoming call. Called by  the system when the user clicks the 
           // answer incoming call  button. It appears after a telephony event about 
           // a new  incoming call is received. For details, see the description of  `BrowserVoipEventSink.callReceived` 
           // @param invite Incoming  call that was specified in the argument 
           // of the  `BrowserVoipEventSink.callReceived` method 
           accept (invite: TIncomingCall): void; 
           // End a call, that is, hang up.  Also called if an incoming call needs to be rejected 
           // @param  call Call 
           hangup(call: TIncomingCall | TOutgoingCall): void;  
           // Put a call on hold 
           // @param call Call  
           hold(call: TIncomingCall | TOutgoingCall): void; 
           //  Take a call off hold 
           // @param call Call 
           unhold (call: TIncomingCall | TOutgoingCall): void; 
           // Perform a blind  transfer to the specified phone number. As a result of calling this method,  
           // the passed call is expected to be ended 
           // @param  call Call 
           // @param redirectPhoneNumber Phone number to transfer  to 
           blindTransfer (call: TIncomingCall | TOutgoingCall, redirectPhoneNumber: string): void;  
           // Complete an attended transfer. As a result of calling the  method, it is expected that 
           // the two specified parties will be  connected to each other, and the `transferor` call will transition to  
           // the Call Ended state 
           // @param transferor Current  user's call with the customer that needs to be transferred 
           // to  employee `caller` 
           // @param caller Current user's call with the  employee to whom the call will be transferred 
            attendedTransfer(transferor: TIncomingCall | TOutgoingCall, caller:  TIncomingCall | TOutgoingCall): void; 
           // Send a DTMF signal.  Called by the system when the user enters a DTMF code in the CTI panel  interface 
           // @param call Call 
           // @param key Signal  to send. For example: '1', '5', '*', '#' 
           // @returns A Promise  that resolves after the signal is sent 
           sendDtmf (call: TIncomingCall | TOutgoingCall, key: string): Promise<void>;  
           // Get the unique call identifier on the telephony side. This  identifier must be 
           // the same on the client side in the browser  and in the event generated by the 
           // `VoipParseWebhookRequest`  function when processing an event through the webhook. The call ID is used  by the system to 
           // update call information with additional data  from the server. For example, a Call App item from the Telephony Workspace  
           // can be linked to a call in the CTI panel 
           // @ param call Call 
           // @returns The identifier of the call or null  if the identifier is unavailable 
           getCallId? (call: TIncomingCall | TOutgoingCall): string | null; 
      } 
      
    • sending notifications to the CTI panel:
      //  Object through which the telephony connection `BrowserVoipClient` can send  notifications 
      // about call state changes to the CTI panel 
      // @ template TIncomingCall Incoming call 
      // @template TOutgoingCall  Outgoing call 
      interface BrowserVoipEventSink<TIncomingCall = unknown,  TOutgoingCall = unknown> { 
           // Notify about a new incoming call.  The CTI panel will display a prompt to the user 
           // to answer or  reject the incoming call 
           // @param invite Object with  information about the incoming call. This value will be passed as is to  
           // `BrowserVoipClient.accept` or `BrowserVoipClient.hangup`  
           // @param phoneNumber Phone number from which we are receiving  the call 
           callReceived(invite: TIncomingCall, phoneNumber: string) : void; 
           // Notify that the call has ended. In the CTI panel, the  call will transition to the Call Ended state 
           // @param call Call  
           callFinished(call: TIncomingCall | TOutgoingCall): void;  
           // Notify that a connection has been successfully established  between two parties for the specified call. 
           // It is expected  that both parties can hear each other. In the CTI panel, the call will  transition to the 
           // In Progress state 
           // @param  call Call 
           callEstablished(call: TIncomingCall | TOutgoingCall):  void; 
           // Send a telephony connection lost event. This method  should be called when 
           // a critical error has occurred due to  which it is impossible to automatically restore the connection 
           //  to telephony 
           // @param reason Error reason that the user will  see 
           disconnected(reason: string): void; 
      } 
      
  3. Also on the Scripts tab, implement the connect function in the client scripts. It is called automatically after the system user turns on the CTI panel. If the connection to telephony is successful, this function returns an implementation of the BrowserVoipClient interface. Later, the system will call the methods of this object depending on user actions in the CTI panel. The method also accepts a parameter of type BrowserVoipClientCredentials, which contains the extension, login, and password for connecting to telephony. The system administrator fills in this information on the telephony module settings page. The
    BrowserVoipEventSink object is used to send events to the CTI panel, for example, receiving a new incoming call or changing the call state.
    async function connect(credentials:  BrowserVoipClientCredentials, eventSink: BrowserVoipEventSink):  Promise<BrowserVoipClient> {
        const client: BrowserVoipClient = {  
            // Implement the BrowserVoipClient interface 
        }; 
         try { 
            // Connect to the telephony signaling channel using the  data from the `credentials` argument 
            ... 
            // At the  moment the method returns, the `BrowserVoipClient` instance must be  connected to telephony 
            return client; 
        } catch {  
            // In case of a connection error, close all telephony  connections 
            ... 
            // Re-throw the exception so that  the user sees 
            // the telephony connection error message in the  CTI panel 
            throw; 
        } 
    } 
    
  4. Save and publish the widget. After that, when the module is enabled, users will be able to select the telephony provider in the CTI panel.

    Automatic authentication in telephony (SSO)

    To allow employees to connect to telephony through the CTI panel, the system administrator must fill in the login and password for each telephony user in the custom module settings. With a large number of
    employees, this can be labor-intensive. To improve security and reduce the workload on the system administrator, it is recommended to automatically retrieve credentials from telephony or a third-party service. The workflow
    may look as follows:
  5. On the Settings tab of the custom telephony module, create fields with the following codes:
    • voip_supports_single_sign_on with the Yes/No choice data type. Set the default value for this field to Yes or add it to the module settings form. When this field is set to Yes, the system does not display fields for entering the telephony user's login and password when configuring the CTI panel;
    • api_key with the String data type. This field stores the API key for telephony authentication.
  6. Open the CTI panel integration widget in the interface designer and on the Context tab add variables with the following codes:
    • extension with the String data type. Used to pass the user's extension for which a token must be obtained;
    • auth_token with the String data type. Used to obtain a token for connecting to telephony.
  7. On the Scripts tab, add a server function call to the connect method in the client scripts. It is used to send a
    request to obtain credentials for connecting to telephony. The current user can be obtained by calling the System.users.getCurrentUser() method. Save the received credentials in the widget context variable auth_token.
  8. In the client scripts, get the result and use this data to connect to telephony.
    // Client scripts 
    async function  connect(credentials: BrowserVoipClientCredentials, eventSink:  BrowserVoipEventSink): Promise<BrowserVoipClient> { 
     // Save the user's  extension in a variable so that the server script can access it 
      Context.data.extension = credentials.extension; 
     await  Server.rpc.getAuthToken(); 
     // The getAuthToken() call will save the  token in the context variable 
     const token = Context.data.auth_token;  
     if (!token) { 
         throw new Error ('Failed to get authentication token'); 
     } 
     // Use `token` to  connect to telephony 
     ... 
    } 
    // Server scripts 
    async function  getAuthToken(): Promise<void> { 
     const currentUser = await  System.users.getCurrentUser(); 
     const extension = Context.data.extension;  
     // API key for the third-party service, usually filled in on the  module settings page 
     const apiKey = (await Namespace.getParams() ).data.api_key; 
     const response = await fetch('https://example.com/ auth', { 
         method: 'POST', 
         body: JSON.stringify({  
             userId: currentUser.id, 
             extension: extension,  
             apiKey: apiKey, 
         }), 
     }); 
     if (response.ok) {  
         const token = await response.text(); 
          Context.data.auth_token = token; 
     } 
    }