Managing inbound call queues
Manage inbound call queues with the help of a small middleware web application.

The inbound call queue experience is the initial entry point to your service and support teams for all your valuable customers. Being able to manage this experience is a fundamental key to success — for both your clients and your agents! With Aircall, we can accomplish this by using custom messages to level-set caller expectations for typical wait times (based on their position in the queue).

From a user perspective, we can also leverage custom Insight Cards to provide agents additional visibility into the current size of the queue while they continue to receive inbound calls. By utilizing both techniques, we can improve the inbound call queue experience for both parties involved.

Table of contents

Scope & Objectives

The scope of this tutorial will primarily focus on helping you understand how to build a small middleware web application that will keep track of the inbound call queue size for every relevant Classic Aircall phone number.

For this tutorial, we specifically focus on “Classic” Aircall phone numbers, although somewhat similar logic can still be applicable for “IVR” Aircall phone numbers.

Please note, however, that IVRs will impact the application logic, especially when calculating call queue sizes since you will not only have to monitor the queues for the IVR number, but also the “Classic” Aircall phone numbers to which the IVR options are routing to. In addition, certain Aircall webhook events are not applicable for IVRs (e.g. call.ringing_on_agent events) and not applicable for Classic phone numbers if an IVR is an entry point (e.g. call.created events), which will affect how you calculate inbound call queue sizes.

Based on the size of the queue, the app will also dynamically change the welcome message of the Aircall phone number (describing typical wait times for the caller’s current position in the queue), as well as update any agent’s Insight Cards (as long as they are assigned to the Aircall phone number) with the current inbound call queue size to provide them additional visibility into the queue so they can better manage their time servicing customers.

In this tutorial, we will not be discussing dynamic welcome messages for each caller’s position in the queue — rather, to reduce strain on the web app and limit the number of Aircall API calls made, we will instead use “ranges” instead of “exact” positions.

For example, once an Aircall phone number reaches five inbound callers waiting in its queue, we can dynamically change the welcome message to say (for the next inbound caller) something like: “There are between five to ten callers ahead of you. Expected waiting time is 5 minutes”. We could then dynamically change the welcome message again if the queue size were to reach ten inbound callers, etc.

Please also note that this tutorial specifically focuses on the welcome message of an Aircall phone number and does not necessarily apply to other types of music or messages that can be modified via API in Aircall. To be specific, the welcome message is the first message (or music-related file) that is played to an inbound caller when they ring into an Aircall phone number.

Welcome Message Definition

In addition, before you begin your project, ensure that you have gathered the necessary requirements that will help you accurately define how a functional call queue management web application should behave. Below are a few questions that should be answered:

  • What is the typical or average size of an inbound call queue during relevant timeframes?
    • What is the minimum threshold size (of the queue) that should be exceeded before dynamically changing the welcome message?
    • How large can the size (of the queue) typically become?
  • What is the typical agent handle time per customer during relevant timeframes?
    • Can we use this information to estimate waiting times based on queue size?
  • What should be the range or interval size where we would dynamically change the welcome message?
    • Every 5 callers? Every 10 callers?
  • Where should we upload all of our welcome messages so that our web application can access them?
    • You will need one for every range or interval (e.g. one for 5-10 callers, another for 10-15 callers, etc.)
  • What kind of information would be useful to our agents for helping them manage their inbound call queues?
    • Total number of callers waiting? Average waiting time of the entire queue? Individual waiting time per caller in the queue?

Fundamental Concepts

API Calls

Aircall Public API is a classic REST API. It can be used with any programming language and offers fast, reliable, and secure access to information within an Aircall instance. We will be using the API to gather user availabilities and call activities on a scheduled basis.

The root endpoint of Aircall Public API is https://api.aircall.io/v1.

Please note that only HTTPS requests are valid as requests will communicate over SSL connection. Each Public API request must be authenticated and should not exceed the rate limit, so please check the ‘Authentication’ and ‘Rate Limiting’ sections in our public documentation.

To find or create your API ID and API Token (which you will need to access our API endpoints), please navigate to the Account section of your Aircall dashboard.

Create an Aircall API Key

Want to learn more? Aircall provides public documentation to help developers understand and use our public API and webhook events. We also provide various guides and tutorials which can be used as references to get you started with your projects. For more info, click here.

Aircall Webhook Events

Webhooks are an efficient way web applications can communicate with each other. Instead of requesting for information on a scheduled basis (e.g. “did a phone call start yet?”) using an API, webhooks simply tell you when an event occurred in real-time (e.g. “a phone call just started”) so you can react accordingly (e.g. log the event into a database). If subscribed to, Aircall will trigger a specific webhook event to be sent when its corresponding event occurs. The event sent will be entirely dependent on what actions are being performed in Aircall.

Inbound Call Webhook Events

For example, here we have a simple inbound call journey. Depending on where the participant is within the call journey, a different webhook will be triggered: to demonstrate, a call.answered event would be sent if an Aircall user picks up the phone during an inbound call.

Call Webhooks

These types of events are related to phone call activity, such as calls starting (both inbound and outbound), calls ringing on an agent, calls answered, calls declined, calls transferred, calls ended, etc.

Subscribing to Webhooks

You can subscribe to Aircall webhook events from the Integrations tab of our dashboard.

Simply click on the Webhook button to start the process.

Install Webhook

You will find a field to name your process (which will be used only as a reference) and a field to specify which URL you want to send webhook events to — this would be your web application/server that consumes and processes webhook events.

From there, all you need to do is specify what type of Aircall webhook events you want to subscribe to.

Defining a Webhook

In our case, we will want to enable the following Aircall Call Webhook events:

  • call.created
  • call.ringing_on_agent
  • call.answered
  • call.hungup

Insight Cards

Aircall Insight Cards allow you to display custom data in an agent's call view (within their Aircall phone app) during ongoing calls, providing additional context about the conversation. Insight Cards have a many-to-1 relationship with a specific call activity — in other words, every call ID (which uniquely identifies a specific call activity) will have its own set of Insight Cards that can be further updated with custom data.

As the call continues through its journey (e.g. from being created, to ringing on an agent’s phone, to being answered, and finally to being completed), we can use the call’s ID to update its Insight Cards via Aircall Public API at different points in time.

Insight Card Example

Insight cards will only be seen during ongoing calls and are not stored after calls are complete.

Preparation

General Architecture

To create a web application for managing your call queues, you will need to setup several important components for the overall architecture:

Call Queue Management Architecture

  • A back-end application to manage logic of queue management processes
    • Keep track of new inbound calls and inbound calls that have ended
      • Sent to the web app via Aircall Call Webhook Events
    • Update data structure keeping track of all callers currently waiting in queue
      • If queue size exceeds (or goes below) range threshold, use Aircall Number API to update the phone number’s welcome message accordingly
      • Use Aircall Call API to update the Insight Cards of the agent (who the inbound call is ringing on) to display information about the current queue size
  • A data structure to keep track of all callers currently waiting in queue
    • You will need to separate this into two data structures if you want to keep separate track of:
      • All inbound callers still waiting in the queue (not answered by an agent yet) — useful for displaying true queue size in an agent’s Insight Cards
      • All inbound callers ahead of the current caller (either waiting in queue or still speaking with an agent) — useful for comparing against queue size range threshold and changing welcome messages accordingly
    • You can use an array, dictionary, hash, etc.
  • A data structure to store important information for optimizing speed of application
    • Aircall phone number IDs on which this web application should apply its call queue management processes
    • URLs of relevant welcome message MP3s (and their associated thresholds)
    • You can use an array, dictionary, hash, etc.

Important Webhook Events

Before starting, we must first understand which Aircall webhook events are going to be useful for this project.

Inbound Call Started

To keep track of the inbound call queue size for an Aircall phone number, we must first be notified when a new inbound call is made on it. This can easily be done using the Aircall Call Webhook events.

[CALLS]    calls.created

This webhook event will send a message to your web application (in the form of a JSON object) whenever a new call is started. The most important information you will need to collect are:

  • data.number.id: This is the ID which uniquely identifies the Aircall phone number relevant to the inbound call activity
    • Use it to filter on which Aircall phone numbers you want to apply this web application to
  • data.direction: This is the direction of the call activity — make sure it is “inbound”
  • data.id: This is the ID which uniquely identifies the call activity itself
    • Required to insert call activity in data structure (keeping track of all inbound calls in queue)
  • data.raw_digits: This is the external phone number calling in
    • Useful if you would like to display individual phone numbers waiting in queue in the Insight Cards
  • data.started_at: This is the start time (in UNIX epoch format) of the call activity
    • Useful if you would like to calculate the average waiting time of all calls in queue or display individual waiting times for every inbound call in queue in the Insight Cards

Sample Webhook JSON Response Payload

json
{ "resource": "call", "event": "call.created", "timestamp": 1614613908, "token": "822rt73345178abb84119m9m768f99d2", "data": { "id": 123456789, "direct_link": "https://api.aircall.io/v1/calls/123456789", "direction": "inbound", "status": "initial", "missed_call_reason": null, "started_at": 1614613908, "answered_at": null, "ended_at": null, "duration": 0, "cost": "0.0", "hangup_cause": null, "voicemail": null, "recording": null, "asset": null, "raw_digits": "+33 1 11 11 11 11", "number": { "id": 260999, "direct_link": "https://api.aircall.io/v1/numbers/260999", "name": "Sales Team IVR", "digits": "+1 647-222-2222", "country": "CA", "time_zone": "America/Chicago", "open": true, "availability_status": "open", "is_ivr": true, "live_recording_activated": false, "messages": { "welcome": "welcome_msg.mp3", "waiting": "waiting_msg.mp3", "ivr": "ivr_msg.mp3", "voicemail": "voicemail_msg.mp3", "closed": "closed_msg.mp3", "callback_later": "callback_msg.mp3" } }, "archived": false, "teams": [], "comments": [], "tags": [] } }

Inbound Call Ringing on Agent

To show the current state/size of the inbound call queue (for a specific Aircall phone number) to an agent, it is best to display this information within Insight Cards when the phone is actually ringing on the agent.

[CALLS]    call.ringing_on_agent

This webhook event will send a message to your web application (in the form of a JSON object) whenever a call is ringing on an agent. The most important information you will need to collect are:

  • data.number.id: This is the ID which uniquely identifies the Aircall phone number relevant to the inbound call activity
    • Use it to search the data structure for all inbound calls waiting in queue for this specific Aircall phone number (which will be displayed in the Insight Cards)
  • data.number.name: This is the name of the Aircall phone number
    • Useful if you want to use the number name as a header in the Insight Cards for readability
  • data.direction: This is the direction of the call activity — make sure it is “inbound”
  • data.id: This is the ID which uniquely identifies the call activity itself
    • Required to update the specific call activity’s Insight Cards

At this moment, we are unable to dynamically adjust the content of a specific entry within an Insight Card. This means that — primarily for readability purposes — we can only inform the agent of the current inbound call queue size for a specific point in time.

Hence, it is best to delay the calculation of call queue metrics as late as possible: this is why it is best to display call queue metrics when the phone is ringing on an agent.

Sample Webhook JSON Response Payload

json
{ "resource": "call", "event": "call.ringing_on_agent", "timestamp": 123456789, "token": "824cf75217888add54233b9c761f12a9", "data": { "id": 504792660, "direct_link": "https://api.aircall.io/v1/calls/123456789", "direction": "inbound", "status": "initial", "missed_call_reason": null, "started_at": 1623674387, "answered_at": null, "ended_at": null, "duration": 0, "cost": "0.0", "hangup_cause": null, "voicemail": null, "recording": null, "asset": null, "raw_digits": "+33 1 11 11 11 1", "user": { "id": 900000, "direct_link": "https://api.aircall.io/v1/users/900000", "name": "Test User", "email": "test.user@email.com", "available": true, "availability_status": "available", "language": "en-US" }, "number": { "id": 26099, "direct_link": "https://api.aircall.io/v1/numbers/260999", "name": "Sales Team IVR", "digits": "+1 647-222-2222", "country": "CA", "time_zone": "America/Chicago", "open": true, "availability_status": "open", "is_ivr": true, "live_recording_activated": false, "messages": { "welcome": "welcome_msg.mp3", "waiting": "waiting_msg.mp3", "ivr": "ivr_msg.mp3", "voicemail": "voicemail_msg.mp3", "closed": "closed_msg.mp3", "callback_later": "callback_msg.mp3" } }, "archived": false, "teams": [], "comments": [], "tags": [] } }

Inbound Call Answered

In general, the waiting time of an inbound caller will be dependent on:

  • Any inbound callers who are also waiting in queue (in front of the current caller)
  • Any inbound callers (in front of the current caller) who are still on a call with an agent

If you would like to make a distinction between both groups (especially for displaying true call queue size metrics on an agent’s Insight Cards), then it is best to leverage this call webhook event as well.

[CALLS]    call.answered

This webhook event will send a message to your web application (in the form of a JSON object) whenever a call is answered by an agent. The most important information you will need to collect are:

  • data.number.id: This is the ID which uniquely identifies the Aircall phone number relevant to the inbound call activity
    • Use it to search the data structure for all inbound calls waiting in queue for this specific Aircall phone number (which will be displayed in the Insight Cards)
  • data.direction: This is the direction of the call activity — make sure it is “inbound”
  • data.id: This is the ID which uniquely identifies the call activity itself
    • Required to remove this call activity from the data structure that keeps track of all inbound calls waiting in queue (i.e. callers not answered by an agent yet)
    • Does not delete call activity from the data structure that generally keeps track of all inbound callers ahead of the current caller (i.e. both the callers still waiting in queue and the callers who are still speaking with an agent)

Sample Webhook JSON Response Payload

json
{ "resource": "call", "event": "call.answered", "timestamp": 1623674412, "token": "814ce756473178add84311b9a761f18d2", "data": { "id": 123456789, "direct_link": "https://api.aircall.io/v1/calls/123456789", "direction": "inbound", "status": "answered", "missed_call_reason": null, "started_at": 1623674387, "answered_at": 1623674411, "ended_at": null, "duration": 0, "cost": "0.0", "hangup_cause": null, "voicemail": null, "recording": null, "asset": null, "raw_digits": "+33 1 11 11 11 1", "user": { "id": 900000, "direct_link": "https://api.aircall.io/v1/users/900000", "name": "Test User", "email": "test.user@email.com", "available": true, "availability_status": "available", "language": "en-US" }, "number": { "id": 26099, "direct_link": "https://api.aircall.io/v1/numbers/260999", "name": "Sales Team IVR", "digits": "+1 647-222-2222", "country": "CA", "time_zone": "America/Chicago", "open": true, "availability_status": "open", "is_ivr": true, "live_recording_activated": false, "messages": { "welcome": "welcome_msg.mp3", "waiting": "waiting_msg.mp3", "ivr": "ivr_msg.mp3", "voicemail": "voicemail_msg.mp3", "closed": "closed_msg.mp3", "callback_later": "callback_msg.mp3" } }, "archived": false, "teams": [], "comments": [], "tags": [] } }

Inbound Call Hungup

To keep track of the inbound call queue size for an Aircall phone number, we must finally be notified when a new inbound call is hungup.

[CALLS]    call.hungup

This webhook event will send a message to your web application (in the form of a JSON object) whenever a call is hungup. The most important information you will need to collect are:

  • data.number.id: This is the ID which uniquely identifies the Aircall phone number relevant to the inbound call activity
    • Use it to search the data structure for all inbound calls waiting in queue for this specific Aircall phone number (which will be displayed in the Insight Cards)
  • data.direction: This is the direction of the call activity — make sure it is “inbound”
  • data.id: This is the ID which uniquely identifies the call activity itself
    • Required to remove this call activity from the data structure that keeps track of all inbound calls waiting in queue (i.e. callers not answered by an agent yet)
    • Required to also remove this call activity from the data structure that keeps track of all inbound callers ahead of the current caller (i.e. both the callers still waiting in queue and the callers who are still speaking with an agent)

Sample Webhook JSON Response Payload

json
{ "resource": "call", "event": "call.hungup", "timestamp": 123456789, "token": "878cf12317178add86611b9c123e19d8", "data": { "id": 504792660, "direct_link": "https://api.aircall.io/v1/calls/123456789", "direction": "inbound", "status": "done", "missed_call_reason": null, "started_at": 1623674387, "answered_at": 1623674411, "ended_at": 1623674423, "duration": 36, "cost": "0.0", "hangup_cause": null, "voicemail": null, "recording": null, "asset": null, "raw_digits": "+33 1 76 42 10 50", "user": { "id": 900000, "direct_link": "https://api.aircall.io/v1/users/900000", "name": "Test User", "email": "test.user@email.com", "available": true, "availability_status": "available", "language": "en-US" }, "number": { "id": 26099, "direct_link": "https://api.aircall.io/v1/numbers/260999", "name": "Sales Team IVR", "digits": "+1 647-222-2222", "country": "CA", "time_zone": "America/Chicago", "open": true, "availability_status": "open", "is_ivr": true, "live_recording_activated": false, "messages": { "welcome": "welcome_msg.mp3", "waiting": "waiting_msg.mp3", "ivr": "ivr_msg.mp3", "voicemail": "voicemail_msg.mp3", "closed": "closed_msg.mp3", "callback_later": "callback_msg.mp3" } }, "archived": false, "teams": [], "comments": [], "tags": [] } }

Important API Endpoints

In addition, we must also recognize which Aircall API endpoints are going to be leveraged in this project.

Displaying Call Queue Metrics to Agent

If we would like to give agent’s additional visibility into the inbound call queue of an Aircall phone number, we can do so by providing call queue metrics in the Insight Cards of the call activity (i.e. the call activity which the agent is currently associated with). This will most likely mean updating the Insight Card when the agent’s phone is ringing as this is the first moment in the call journey where an agent can review its Insight Cards.

As mentioned previously, we are unable to dynamically adjust the content of a specific entry within an Insight Card at this moment. This means that — primarily for readability purposes — we can only inform the agent of the current inbound call queue size for a specific point in time.

Hence, it is best to delay the calculation of call queue metrics as late as possible: this is why it is best to display call queue metrics when the phone is ringing on an agent.

Insight Cards

[POST]    https://api.aircall.io/v1/calls//insight_cards

This endpoint will update the Insight Cards of a call activity (with call ID <call_id>). A JSON object — representing the information you would like to display in the Insight Cards — will need to be included with the request.

Three types of JSON objects can be utilized to display information in Insight Cards:

  • type: title is a large text (think of it as a header for an entry in the Insight Cards)
    • text: The text to display in the title
    • link: URL to make the text a clickable link (opens a new tab in the browser)
  • type: shortText is a normal line in an Insight Cards entry
    • label: Label of the text
    • text: The text to display
    • link: URL to make the text a clickable link (opens a new tab in the browser)
  • type: user is a user card in the Insight Card section displaying the name and availability of the user
  • label: Label of the text
  • user_id: The ID of the Aircall user

We can use a combination of the above JSON object types to organize and display call queue metrics in Insight Cards.

Sample JSON Request (Data)

json
{ "contents": [ { "type": "title", "text": "Call Queue - Martin Test US" }, { "type": "shortText", "label": "Queue Size", "text": "1 calls" }, { "type": "shortText", "label": "Avg. Waiting Time", "text": "7s" }, { "type": "shortText", "label": "Waiting for 10s...", "text": "+1 213-67-3928" } ] }

Updated Insight Cards

Retrieving Aircall Phone Numbers from Instance

In order to keep track of the inbound call queue size for each Aircall phone number, we would need to first collect all Aircall phone numbers (from the same Aircall instance) as a pre-requirement.

By querying Aircall for the current list of Aircall phone numbers, we are able to keep our web application dynamic. Once the list of numbers is collected, we can initialize a data structure for each Aircall phone number to keep track of its inbound call queue size.

List All Numbers

[GET]    https://api.aircall.io/v1/numbers

This endpoint will retrieve all the Aircall phone numbers in your Aircall instance. The most important information you will need to collect is:

  • numbers[N].id: This is the ID which uniquely identifies the Aircall phone number
  • numbers[N].name: This is the name of the Aircall phone number (easier to remember than the digits themselves)

Sample API JSON Response Payload

json
{ "meta": { "count": 3, "total": 2, "current_page": 1, "per_page": 20, "next_page_link": null, "previous_page_link": null }, "numbers": [ { "id": 1234, "direct_link": "https://api.aircall.io/v1/numbers/1234", "name": "French Office", "digits": "+33 1 76 11 11 11", "created_at": "2020-01-02T11:41:01.000Z", "country": "FR", "time_zone": "Europe/Paris", "open": true, "availability_status": "custom", "is_ivr": true, "live_recording_activated": true, "messages": { "welcome": "https://example.com/welcome.mp3", "waiting": "https://example.com/waiting_music.mp3", "ivr": "https://example.com/ivr_message.mp3", "voicemail": "https://example.com/voicemail.mp3", "closed": "https://example.com/closed_message.mp3", "callback_later": "https://example.com/callback_later.mp3", "unanswered_call": "https://example.com/unanswered_call.mp3", "after_hours": "https://example.com/closed_message.mp3", "ringing_tone": "https://example.com/waiting_music.mp3" } }, { "id": 2345, "direct_link": "https://api.aircall.io/v1/numbers/2345", "name": "UK office", "digits": "+44 20 0000 0000", "created_at": "2020-01-04T19:28:58.000Z", "country": "GB", "time_zone": "Europe/London", "open": true, "availability_status": "custom", "is_ivr": true, "live_recording_activated": false, "messages": { "welcome": "https://example.com/welcome.mp3", "waiting": "https://example.com/waiting_music.mp3", "ivr": "https://example.com/ivr_message.mp3", "voicemail": "https://example.com/voicemail.mp3", "closed": "https://example.com/closed_message.mp3", "callback_later": "https://example.com/callback_later.mp3", "unanswered_call": "https://example.com/unanswered_call.mp3", "after_hours": "https://example.com/closed_message.mp3", "ringing_tone": "https://example.com/waiting_music.mp3" } } ] }

Updating Welcome Messages to Level-Set Expectations

Aircall gives us the ability to dynamically change the welcome message (in MP3 format) for any Aircall phone number using our Public API. This implies that we can leverage this capability to adjust the welcome message according to various conditions — one of them being call queue size.

By changing the welcome message when a queue size threshold is exceeded, we can help set customer expectations about waiting times. Although it does not resolve the underlying issue of longer waiting times, it will help improve the inbound caller experience.

Update Music and Messages

[PUT]    https://api.aircall.io/v1/numbers/

This endpoint will update the music and messages (with call ID <call_id>) of an Aircall phone number. A JSON object — representing the music or messages you would like to update — will need to be included with the request.

  • welcome: The URL of the welcome message (in MP3 format) that you would like to update the number with

Sample JSON Request (Data)

json
{ "messages": { "welcome": "https://test.product.com/mp3s/welcome_message_lvl1.mp3" } }

Technical Steps

In this section, we will walk through the technical steps and logic of how to build a web application to manage your call queues. This will generally involve listening to Aircall Call Webhooks for important events in the inbound call journey, storing call queue information so we can monitor the size, and calling both Aircall Numbers API and Calls API to dynamically update the welcome message and Insight Cards (respectively).

  1. As mentioned in the Fundamental Concepts section of the tutorial, please subscribe to the following Aircall Call Webhook events and send them to your web application:

    • call.created
    • call.ringing_on_agent
    • call.answered
    • call.hungup
  2. Start by defining various constants and data structures/variables which will store important information

    • Data structure to store call queues for every relevant Aircall phone number
      • You will need to create two different data structures if you need to separately track...
        • Inbound callers who are waiting to be answered by an agent (important for displaying call queue metrics in an agent’s Insight Cards)
        • Inbound callers who were answered by an agent but still have not finished the call (important for estimating waiting times based on all callers ahead of the current caller)
      • We recommend using a dictionary:

        Key: Aircall Phone Number ID
        • Value:
        • Key: “general”
          • Value: JSON object containing all callers ahead of the current caller (keys = call IDs)
        • Key: “waiting”
          • Value: JSON object containing all callers who are still waiting in the call queue (keys = call IDs)
    • Data structure to store URLs of all welcome message MP3s per phone number and per range/interval
      • We recommend using a dictionary:

        Key: Aircall Phone Number ID
        • Value:
        • Key: “DEFAULT”
          • Value: MP3 URL
        • Key: “LVL1”
          • Value: MP3 URL
        • Key: “LVL2”
          • Value: MP3 URL
        • Etc.
    • Constants to keep track of different queue size thresholds
      • One for exceeding the first threshold
      • One for exceeding the second threshold
      • Etc.
    • Data structure to keep track of which call activities already have their Insight Cards modified (to show call queue size metrics)
      • You want to avoid accidentally creating duplicate entries in Insight Cards if the call journey rings on more than one agent (for example, if the first agent does not pick up) — in other words, once you push information to the Insight Cards of a call activity, there is no need to push information again via another API request
    json
    /* Import various Node JS modules for our application. */ const express = require('express'); const router = express.Router(); const request = require('request'); const bodyParser = require('body-parser'); const app = express(); app.use(bodyParser.json()); /* Define various constants which will be used across all functions for ease-of-use and readability. */ // General app constants const PORT = 3000; // First and second threshold for number of calls in an Aircall phone number's queue // (which contains calls that have not yet ended) const QUEUE_SIZE_THRESHOLD_1 = 5; const QUEUE_SIZE_THRESHOLD_2 = 10; // Global JSON object to keep track of welcome messages for each Aircall phone number const WELCOME_MESSAGES = { "242679": { "DEFAULT": 'welcome.mp3', "LVL1": 'lvl1.mp3', "LVL2": 'lvl2.mp3' } }; // Global JSON object which will store the real-time call queues for each Aircall // phone number. Each number will have a "general" and "waiting" queue: // - "General" queue: All calls that have not yet ended (could be waiting to be // picked up by an agent or already answered) // - "Waiting" queue: All calls that are still waiting to be picked up (have not // yet been answered) // Structure: { // "Number ID 1": { // "general": { // "Call ID A": { // "started_at": , // "phone_num": // }, // "Call ID B": { // "started_at": , // "phone_num": // }, // ... // }, // "waiting": { // "Call ID B": { // "started_at": , // "phone_num": // }, // ... // }, // }, // "Number ID 2": { // ... // }, // ... // } var callQueues = {}; // Global JSON objects which will store which call activities already have their // Insight Cards updated (either with info about the current call queue size or // call history about the external phone number involved). // Structure: { // "Call ID A": true, // "Call ID B": true, // "Call ID C": true, // ... // } var insightCardsQueueUpdated = {};
  3. Start the web application on a chosen port and create a function to initialize the various data structures we have created (if they have not already been initialized and populated with some data)

    • Initialize the data structure containing the call queues for each relevant Aircall phone number
      • You could use Aircall Numbers API to query Aircall for the latest list of Aircall phone numbers in your instance
    • If you want to dynamically pull the latest set of welcome message MP3s from an external source (e.g. a database), you can do that here as well
    json
    /* Define various functions that will be re-used to help improve portability and readability. */ const callQueueMgmtApp = { /* * Function that initializes the various data structures. */ initialize: () => { console.log("Populating the queues for each Aircall number..."); // Create the options for the API request var numbersQuery = { 'method': 'GET', 'url': 'https://api.aircall.io/v1/numbers?per_page=50', 'headers': { 'Content-Type': 'application/json', 'Authorization': 'Basic ' + API_KEY } }; // Make API request (TO-DO: paginate if results are too large) request(numbersQuery, (err, res) => { if (err) throw new Error(err); // Contains the JSON-formatted data from response const data = JSON.parse(res.body); // For each Aircall phone number returned... data.numbers.forEach((number) => { // Keep track of the "general" call queue and "waiting" call queue console.log("Creating a queue for Aircall Number ID " + number.id + " (" + number.name + ")"); callQueues[number.id] = {'general': {}, 'waiting': {}}; }); }); // TO-DO: If you need to pull the URL MP3s for welcome messages from // another source, you can do it here } }; // Initialize the queues for each Aircall phone number in this instance callQueueMgmtApp.initialize(); // Start listening to specific port app.listen(PORT, () => { console.log("Starting app at port: " + PORT); });
  4. Create helper functions to assist with readability and reusability:

    • Helper function to monitor each call queues’ size
    • Helper function to update the associated Aircall phone number’s welcome message when a threshold is exceeded (or when the queue size goes below a certain threshold)
    • Helper function to create a new Insight Card for a call activity containing call queue metrics
    json
    /* Define various functions that will be re-used to help improve portability and readability. */ const callQueueMgmtApp = { /* * Function that initializes the various data structures. */ initialize: () => { console.log("Populating the queues for each Aircall number..."); // Create the options for the API request var numbersQuery = { 'method': 'GET', 'url': 'https://api.aircall.io/v1/numbers?per_page=50', 'headers': { 'Content-Type': 'application/json', 'Authorization': 'Basic ' + API_KEY } }; // Make API request (TO-DO: paginate if results are too large) request(numbersQuery, (err, res) => { if (err) throw new Error(err); // Contains the JSON-formatted data from response const data = JSON.parse(res.body); // For each Aircall phone number returned... data.numbers.forEach((number) => { // Keep track of the "general" call queue and "waiting" call queue console.log("Creating a queue for Aircall Number ID " + number.id + " (" + number.name + ")"); callQueues[number.id] = {'general': {}, 'waiting': {}}; }); }); // TO-DO: If you need to pull the URL MP3s for welcome messages from // another source, you can do it here }, /* * Function that checks the queue size of an Aircall phone number and updates the * welcome message if size threshold is exceeded (or if size falls * below threshold). * @param {String} numberId ID of the Aircall phone number * {String} event Call webhook event */ checkQueueSize: (numberId, event) => { // If a new call was initiated... if (event == "call.created") { let queueSize = Object.keys(callQueues[numberId].general).length; // If the first threshold of a call queue size was exceeded, update the // welcome message accordingly... if ((queueSize - 1) < QUEUE_SIZE_THRESHOLD_1 && queueSize == QUEUE_SIZE_THRESHOLD_1) { callQueueMgmtApp.updateWelcomeMessage(numberId, WELCOME_MESSAGES[numberId]["LVL1"]); // ...else, if the second threshold of a call queue size was exceeded, // update the welcome message accordingly... } else if ((queueSize - 1) < QUEUE_SIZE_THRESHOLD_2 && queueSize == QUEUE_SIZE_THRESHOLD_2) { callQueueMgmtApp.updateWelcomeMessage(numberId, WELCOME_MESSAGES[numberId]["LVL2"]); } // ...else, if a call ended... } else if (event == "call.hungup") { let queueSize = Object.keys(callQueues[numberId].general).length; // If the call queue size went below the second threshold, update the welcome // message accordingly... if (queueSize < QUEUE_SIZE_THRESHOLD_2 && (queueSize + 1) == QUEUE_SIZE_THRESHOLD_2) { callQueueMgmtApp.updateWelcomeMessage(numberId, WELCOME_MESSAGES[numberId]["LVL1"]); // ...else, if the call queue size went below the first threshold, // update the welcome message accordingly... } else if (queueSize < QUEUE_SIZE_THRESHOLD_1 && (queueSize + 1) == QUEUE_SIZE_THRESHOLD_1) { callQueueMgmtApp.updateWelcomeMessage(numberId, WELCOME_MESSAGES[numberId]["DEFAULT"]); } } }, /* * Function that updates the welcome message of an Aircall phone number. * @param {String} numberId ID of the Aircall phone number * {String} welcomeMsg URL of the MP3 for the welcome message */ updateWelcomeMessage: (numberId, welcomeMsg) => { // Create the options for the API request let updateNumberQuery = { 'method': 'PUT', 'url': 'https://api.aircall.io/v1/numbers/' + numberId, 'headers': { 'Content-Type': 'application/json', 'Authorization': 'Basic ' + API_KEY }, 'body': JSON.stringify({ 'messages': { 'welcome': welcomeMsg } }) }; // Make the API call to update the Aircall phone number's welcome message request(updateNumberQuery, (err, res) => { if (err) throw new Error(err); console.log("Updated welcome message of " + numberId + "..."); }); }, /* * Function that displays the current queue size (calls waiting to be picked up) * for a particular Aircall phone number via Insight Cards. * @param {String} numberId ID of the Aircall phone number * {String} numberName Name of the Aircall phone number * {String} currentCallId ID of the call activity */ displayQueue: (numberId, numberName, currentCallId) => { // Create a JSON object to set the title of an entry in the call activity's // Insight Cards let title = { "type": "title", "text": "Call Queue - " + numberName }; // Create a JSON object to display the total call queue size let queueSize = { "type": "shortText", "label": "Queue Size", "text": "N/A", } // Create a JSON object to display the average waiting time of all calls in // the call queue let avgWait = { "type": "shortText", "label": "Avg. Waiting Time", "text": "N/A", } // Start creating the array of Insight Cards data, which will be used as part // of the body of the API request let insightCardsData = [title, queueSize, avgWait]; let calcAvgWaitTime = 0; let calcQueueSize = 0; // For each call waiting in-queue for this Aircall phone number... Object.entries(callQueues[numberId].waiting).forEach((call) => { const [callId, callIdData] = call; // If the call waiting in queue is not the same call activity which we will // be updating the Insight Cards for... if (callId != currentCallId) { // Calculate current waiting time of call let waitTime = (parseInt((new Date().getTime())/1000) - callIdData.started_at); // Gather information for average wait time and total call queue size calcAvgWaitTime += waitTime; calcQueueSize += 1; // Push an entry into the array of Insights Card data related to this call // waiting in queue insightCardsData.push({ "type": "shortText", "label": `Waiting for ${waitTime}s...`, "text": callIdData.phone_num }); } }); // Calculate the final average waiting time calcAvgWaitTime = parseInt(calcAvgWaitTime/calcQueueSize); // Set the final total call queue size insightCardsData[1].text = calcQueueSize.toString() + " calls"; // If there are calls waiting in queue, set the average waiting time if (calcQueueSize) { insightCardsData[2].text = calcAvgWaitTime.toString() + "s"; } // Create the options for the API request let updateInsightCardsQuery = { 'method': 'POST', 'url': `https://api.aircall.io/v1/calls/${currentCallId}/insight_cards`, 'headers': { 'Content-Type': 'application/json', 'Authorization': 'Basic ' + API_KEY }, 'body': JSON.stringify({ 'contents': insightCardsData }) }; // Make the API request to create a new Insight Card for the call activity request(updateInsightCardsQuery, (err, res) => { if (err) throw new Error(err); // Keep track of the fact that we have already updated the Insight // Cards of this specific call activity insightCardsQueueUpdated[currentCallId] = true; }); } };
  5. Set up an endpoint in the web application to consume Aircall Call Webhook events. First, we will create logic to handle inbound calls that have just started.

    • As soon as a call starts, we will immediately add them to both our data structures tracking:
      • Inbound callers that are either waiting to be picked up by an agent or have been answered by an agent and are not finished the call yet (“general”)
      • Inbound callers waiting to be picked up by an agent (“waiting”)
    • Call helper function to check queue sizes and determine whether we need to update the welcome message or not
    json
    // Create a route to handle the live queues (for each Aircall phone number) // when a phone call activity starts or ends app.post('/queuemgmt/calls', (req, res) => { // If a call starts... if (req.body.event == "call.created" && req.body.data.direction == "inbound") { var data = req.body.data; var numberId = data.number.id; var numberName = data.number.name; var rawDigits = data.raw_digits; // Populate both the "general" and "waiting" call queues for // the relevant Aircall phone number with the call ID and start time callQueues[numberId].general[data.id] = { 'started_at': data.started_at, 'phone_num': rawDigits }; callQueues[numberId].waiting[data.id] = { 'started_at': data.started_at, 'phone_num': rawDigits }; // Check the queue size to determine whether we need to modify // the welcome message callQueueMgmtApp.checkQueueSize(numberId, "call.created"); } res.sendStatus(200); });
  6. Next, we will continue adding to this endpoint by creating logic to handle inbound calls that have started ringing on an agent

    • This point in the call journey is the best time to display information about the current call queue size to an agent, so we will call the helper function which will calculate these metrics and use Aircall Calls API to update the call activity’s Insight Cards
      • Make sure not to update the Insight Cards of the call activity if it has already been previously modified (for example, if this is the second agent in a call distribution workflow) — this will eliminate redundant entries that will only further clutter the interface

    As mentioned previously, we are unable to dynamically adjust the content of a specific entry within an Insight Card at this moment. This means that — primarily for readability purposes — we can only inform the agent of the current inbound call queue size for a specific point in time.

    Hence, it is best to delay the calculation of call queue metrics as late as possible: this is why it is best to display call queue metrics when the phone is ringing on an agent.

    json
    // Create a route to handle the live queues (for each Aircall phone number) // when a phone call activity starts or ends app.post('/queuemgmt/calls', (req, res) => { // If a call starts... if (req.body.event == "call.created" && req.body.data.direction == "inbound") { var data = req.body.data; var numberId = data.number.id; var numberName = data.number.name; var rawDigits = data.raw_digits; // Populate both the "general" and "waiting" call queues for // the relevant Aircall phone number with the call ID and start time callQueues[numberId].general[data.id] = { 'started_at': data.started_at, 'phone_num': rawDigits }; callQueues[numberId].waiting[data.id] = { 'started_at': data.started_at, 'phone_num': rawDigits }; // Check the queue size to determine whether we need to modify // the welcome message callQueueMgmtApp.checkQueueSize(numberId, "call.created"); // ...else, if a call is ringing on an agent... } else if (req.body.event == "call.ringing_on_agent" && req.body.data.direction == "inbound") { var data = req.body.data; var numberId = data.number.id; var numberName = data.number.name; var rawDigits = data.raw_digits; // If we have not already updated this call activity's Insight Cards // about the current call queue size... if (!(insightCardsQueueUpdated[data.id])) { // Display the current call queue size (calls waiting to be picked up) in // the Insight Cards of this call activity callQueueMgmtApp.displayQueue(numberId, numberName, data.id); } } res.sendStatus(200); });
  7. Next, we will continue adding to this endpoint by creating logic to handle inbound calls that have been answered by an agent

    • Now that this inbound call has been answered, we should remove it from the data structure specifically tracking all inbound callers waiting to be picked up by an agent
    json
    // Create a route to handle the live queues (for each Aircall phone number) // when a phone call activity starts or ends app.post('/queuemgmt/calls', (req, res) => { // If a call starts... if (req.body.event == "call.created" && req.body.data.direction == "inbound") { var data = req.body.data; var numberId = data.number.id; var numberName = data.number.name; var rawDigits = data.raw_digits; // Populate both the "general" and "waiting" call queues for // the relevant Aircall phone number with the call ID and start time callQueues[numberId].general[data.id] = { 'started_at': data.started_at, 'phone_num': rawDigits }; callQueues[numberId].waiting[data.id] = { 'started_at': data.started_at, 'phone_num': rawDigits }; // Check the queue size to determine whether we need to modify // the welcome message callQueueMgmtApp.checkQueueSize(numberId, "call.created"); // ...else, if a call is ringing on an agent... } else if (req.body.event == "call.ringing_on_agent" && req.body.data.direction == "inbound") { var data = req.body.data; var numberId = data.number.id; var numberName = data.number.name; var rawDigits = data.raw_digits; // If we have not already updated this call activity's Insight Cards // about the current call queue size... if (!(insightCardsQueueUpdated[data.id])) { // Display the current call queue size (calls waiting to be picked up) in // the Insight Cards of this call activity callQueueMgmtApp.displayQueue(numberId, numberName, data.id); } // ...else, if a call was answered... } else if (req.body.event == "call.answered" && req.body.data.direction == "inbound") { var data = req.body.data; var numberId = data.number.id; // Remove the call entry from the "waiting" call queue for the relevant Aircall // phone number delete callQueues[numberId].waiting[data.id]; } res.sendStatus(200); });
  8. Finally, we will complete this endpoint by creating logic to handle inbound calls that have ended — this will also handle inbound callers that have abandoned the call while waiting in the queue

    • Now that this inbound call has ended, we can delete this call activity from all data structures tracking call queue sizes (“general” and “waiting”)
    • We can also forget that we updated this call activity’s Insight Cards since it no longer makes sense to track this (now that the call is finished)
    • Call helper function to check queue sizes and determine whether we need to update the welcome message or not
    json
    // Create a route to handle the live queues (for each Aircall phone number) // when a phone call activity starts or ends app.post('/queuemgmt/calls', (req, res) => { // If a call starts... if (req.body.event == "call.created" && req.body.data.direction == "inbound") { var data = req.body.data; var numberId = data.number.id; var numberName = data.number.name; var rawDigits = data.raw_digits; // Populate both the "general" and "waiting" call queues for // the relevant Aircall phone number with the call ID and start time callQueues[numberId].general[data.id] = { 'started_at': data.started_at, 'phone_num': rawDigits }; callQueues[numberId].waiting[data.id] = { 'started_at': data.started_at, 'phone_num': rawDigits }; // Check the queue size to determine whether we need to modify // the welcome message callQueueMgmtApp.checkQueueSize(numberId, "call.created"); // ...else, if a call is ringing on an agent... } else if (req.body.event == "call.ringing_on_agent" && req.body.data.direction == "inbound") { var data = req.body.data; var numberId = data.number.id; var numberName = data.number.name; var rawDigits = data.raw_digits; // If we have not already updated this call activity's Insight Cards // about the current call queue size... if (!(insightCardsQueueUpdated[data.id])) { // Display the current call queue size (calls waiting to be picked up) in // the Insight Cards of this call activity callQueueMgmtApp.displayQueue(numberId, numberName, data.id); } // ...else, if a call was answered... } else if (req.body.event == "call.answered" && req.body.data.direction == "inbound") { var data = req.body.data; var numberId = data.number.id; // Remove the call entry from the "waiting" call queue for the relevant Aircall // phone number delete callQueues[numberId].waiting[data.id]; // ...else, if a call was hungup... } else if (req.body.event == "call.hungup" && req.body.data.direction == "inbound") { var data = req.body.data; var numberId = data.number.id; // Remove the call entry from both the "general" and "waiting" call queues // for the relevant Aircall phone number delete callQueues[numberId].general[data.id]; delete callQueues[numberId].waiting[data.id]; // No longer need to keep track of the fact that we have updated // the Insight Cards of this call activity delete insightCardsQueueUpdated[data.id]; // Check the queue size to determine whether we need to // modify the welcome message callQueueMgmtApp.checkQueueSize(numberId, "call.hungup"); } res.sendStatus(200); });

Tips & Tricks

Performance Considerations

Updating Data Structures

To be able to update welcome messages in a timely manner, the speed at which we adjust local data structures tracking our call queue sizes should be as fast as possible. Inserting data into the structure is usually not the bottleneck (i.e. keeping track of a brand new inbound call) — rather, any search operation (i.e. to display individual inbound call information in an agent’s Insight Cards) or deletion operation (i.e. removing an inbound call that finished) on the structure will be a more important determinant of app performance.

Hence, we recommend the use of dictionaries (with key-value pairs) to help improve search and deletion performance, as the key benefit of these types of structures is optimized lookup speed.

Displaying Info in Insight Cards

For our purposes, we have decided the best time during the call journey to inform an agent of the current state of the inbound call queue is when the phone is ringing on the agent — this is the first time, in fact, during the call journey where an agent can be notified of key information using Insight Cards.

However, this implies that we have a smaller window of time to calculate various call queue metrics (e.g. size, average wait time, individual wait times of each caller in the queue, etc.) before updating the agent’s Insight Cards. The main factors that will affect performance here are the amount of inbound calls in the queue and the number (and complexity) of metrics you choose to display in the Insight Cards, so please keep this in mind when evaluating what type of information you want to share with your agents.

Technically, you can update Insight Cards for a call as soon as the call starts (via the call.created webhook event). However, if the inbound caller has to wait for some time before their call starts ringing on an agent, the call queue metrics displaying in the Insight Cards will most likely be already out of date (i.e. new inbound callers could have joined or been removed from the queue).

Hence, it is best to push the calculation of call queue size metrics (so as to display them in Insight Cards) as late as possible in the call journey. We believe that moment is when the call begins ringing on an agent’s phone.

Rate Limiting

Be aware that the maximum number of API calls your company can make per minute is 60*.

Please consult our developer portal for more information about rate limiting vy clicking here.

(*) If you have a special use case that requires an increase in the general API rate limit, please speak with your Aircall representative or Aircall Support.

Troubleshooting

Ghost Inbound Callers in Queue

There may be instances where the data structures tracking your call queue sizes do not reflect the actual queue in real life. This may be caused by factors such as web server load (i.e. Aircall sends a webhook event to the web server but it gets lost/dropped due to overload).

You may want to put in-place a “cleanup” mechanism that runs on a scheduled basis, pulling information from Aircall about your call history during a specific activity period (click for more info here) and comparing it to the information stored in your data structures for validation.

For example, you could traverse through the returned call history results from Aircall and check, for every call ID, whether the same call ID in your data structure matches the correct state in its call journey. Here is an example logical workflow:

  • App pulls call history from Aircall via API
  • For each call in call history...
    • Retrieve call ID and “status” (could either be “initial” for call waiting, “answered” for call answered, or “done” for call complete)
    • Search for call ID in data structure
      • If call ID is found, this implies the call from our call history results should also have “status” set to either “initial” or “answered”
        • If it does not, remove the call from the data structure because it is already finished
        • If it does, keep it in the data structure because both states match
      • If call ID is not found, this implies the call was finished and was correctly removed from the data structure

If you choose to pursue this kind of functionality, you will have to implement checks and balances within your code to ensure race conditions are not breaking your app (e.g. cleanup process deletes information from your data structure at the same time that your app was also trying to read it for displaying in Insight Cards).

Redundant Information in Insight Cards

If you find that there are repeating entries in your Insight Cards showing the same call queue metrics, it is most likely due to the following reasons:

  • You are updating a call activity’s Insight Cards in more than one call journey state (i.e. during more than one call.* webhook event)
  • You are updating a call activity’s Insight Cards in a call journey state that can repeat more than once in the same journey (i.e. call.ringing_on_agent is most likely the culprit as this event will be triggered every time the call rings on an agent in a call distribution workflow)

Make sure you have mechanisms in place to help you validate whether a specific call activity’s Insight Cards has already been updated with call queue metrics.

Conclusion

You should now have all the technical knowledge you need to begin scoping and building your own call queue management application using Aircall Public API and Webhooks!

We wrote other tutorials to help you out
building awesome integrations

Discover them now