Megadialer IVR Module

Created by Rob Garcia, Modified on Wed, 06 Dec 2023 at 04:36 PM by Daniel Kauffer

Module Overview


Custom IVRs (Interactive Voice Response) can be created within the Megacall IVR module. IVRs can be assigned to a campaign (1). The assignment is for organisational purposes only and has no technical impact on usage.


IVR configuration is done by selecting an already created IVR (2) or by creating a new IVR. 


The configuration page consists of 4 tabs. On the "Preferences" page (3) a title can be specified. The "Audio" page (4) is used to create or upload audio files to be used. The "Script" page (5) contains an editor, where the IVR process can be scripted. On the "Test" page (6) you can make a test call to validate that your IVR works correctly.


Additionally you have a "refresh" button (7) to reload the IVR settings. Deletion of your IVR can be done by pressing the "delete" button (8). You can make a copy of the selected IVR (9) and of course you can save your configuration (10).





Audio Editor


Before configuring a new IVR, you should consider the exact process. Once you know how the IVR should interact with the caller, you can create or upload the necessary audio files. The IVR module supports dynamic switching between different audio variants (1). This can be used, for example, to provide your IVR for different languages. The required language variant can be determined, for example, using the caller ID. You can find more on this topic, e.g. how to switch the language variant dynamically, in the section "IVR script".


The "Settings" section (2) is used to configure your prefered way to create the audio files for the selected variant. You can choose between three different audio sources:


Choose file upload if you already have the audio files and just want to upload them

Choose recording if you want to create the audio files by recording your phrases using a microphone

Choose text to speech if you want the phrases to be generated from text



After you have configured your preferred audio source you can continue to create the actual audio files. Click on the Plus-button (7) to create a new audio file. Give it a meaningful name (3) and either enter a text (4) to generate audio (5) or upload your existing audio file(this option will be visible if you select "file upload"). There is also an option to create all audio files at once (6). This is especially helpful if you already created a complete variant and want to translate it into another language. You can add, remove and sort your audio files using the buttons on the right (7). Sorting is also possible via drag and drop.





IVR script


The IVR script allows you to control the flow of your IVR. When you create a new IVR, you already have the choice between different templates, which contain a functioning basic script that can be extended according to your requirements.


At the top of every IVR script you have to specify the IVR_BASE version to be used (1). This will ensure that your script will still work correctly even after new functions are introduced to the base script. You can just leave it at the default value.


Within the function handleCall(session: Session) the actual sequence of your IVR must be programmed (2). More about how to program your IVR can be found in section "Scripting the IVR session".


At the bottom of every script there needs to be a call of the function initConnection (3). This simply ensures that your handleCall function is called on IVR start.


The last point to mention is that your IVR scripts are versioned. Every time you activate a specific version of your IVR a new version number is automatically created for you. (4) This will give you a history of your changes to the IVR script. If you want to load an earlier version of your script for editing, you can select the desired version via the dropdown menu (4). The currently active script of your IVR is marked with the addition "active".





Scripting the IVR session



The programming language used for IVR scripts is Typescript. If you do not have any experience with Typescript yet, please have a look at the TypeScript Handbook.


All basic functions to control the flow of your IVR are available via the session object, that is available in the handleCall(session: Session) function.


The first thing that has to be done is to answer the incoming call. This will connect the calling person to the IVR.


await session.answer();


The last thing that has to be done is to forward the connected call either to the inbound line the call was initially addressed to


await session.continue();

or to another of your inbound lines


await session.continue({
  "line_id": continueLineID
});


These are the most basic steps an IVR script has to do. Your IVR will already work, but is of course not very useful yet.


One of the first functionalities you might want to have is a DTMF menu, where the IVR prompts the caller for some input and forwards the call to the correct inbound line, based on the callers decisions. There is already a predefined IVR script called "DTMF menu", which contains a very basic DTMF menu.


let continueLineID = ""
    const code = await session.dtmfPrompt({
        audioFile: "dtmf_menu",
        maxRetries: 3,
        allowedDigits: ["1", "2"]
    });
    switch (code) {
        case "1":
            session.log("1 pressed --> Send call to Sales line");
            continueLineID = "ID of Sales line"
            break;
        case "2":
            session.log("2 pressed --> Send call to Support line");
            continueLineID = "ID of Support line"
            break;
    }


DTMF menus can be created by using the following function:


session.dtmfPrompt(audioFile: string, maxRetries: number, allowedDigits?: string[])


  • audioFile specifies the name of the audio file to be played to the caller. Please refer to the section "Audio editor" to learn more about how to create audio files.
  • maxRetries specifies the number of times the IVR should repeat the audiofile in case the caller made an invalid input
  • allowedDigits specifies which inputs are allowed. In most cases it will be something like ["1", "2", "3"]


As a result you will receive the code (digit) that the caller has entered. Based on that code you are now able to forward the call to the correct inbound line using the aforementioned session.continue().


Multistep DTMF menus can be easily created by programming several session.dtmfPrompt() in a series.



Testing your IVR


To activate your IVR script and use it for handling your inbound calls in Megadialer a test call is required. After the call has been hung up a call log will be displayed (1). It will display all your log messages and also script errors, in case you made any mistakes while programming your IVR script.


You have also the option to specify a custom caller ID (2), e.g. to simulate a call from a foreign country. There is "call", "hangup" and "DTMF" button (3). If you have tested your IVR and are satisfied with the result you can "activate" your new IVR script (4). This will automatically create a new version of your script and push the current version into production. From now on all incoming calls will be handled by your activated script.





Using your IVR


After you have created, tested and activated your IVR you can finally use it on any of your inbound lines. Open the "Phone numbers" module (1) and select the inbound line of your choice (2). You will find a section called "IVR Settings" (3), where you can attach your IVR. You can Enable your IVR to handle all of your incoming calls on that line within the service hours. Additionally or alternatively you can configure that your IVR shall handle all calls outside of service hours.





Session object reference



The session object houses all relevant functions required to control the flow of your IVR

Synchronous functions


All synchronous functions return immediately.


log(...data:any): void;


  • Print log statement
  • Arguments:
    • data... message to be logged


error(...data: any): void;


  • Print error statement
  • Arguments:
    • data... message to be logged as error


getAudioURL(filename: string): string;

  • Creates a fully qualified URL from the passed filename
  • Arguments:
    • filename... the name of the audio file
  • Result:
    • a fully qualified URL to the file


setVariant(variantname: string): void;


  • Change the audio variant to be used for the IVR
  • Arguments
    • variantname... the name of the variant



Asynchronous functions


Asynchronous functions execute longrunning tasks like e.g. HTTP requests. If the subsequent control flow of your IVR depends on the result of the executed function then you have to use the keyword await. If you have never heard about asynchronous programming before then please refer to Async/await or any other information source of your choice.


async answer(): Promise<void>;


  • Answer the incoming call
  • This function has to be called at a very early stage of your program to start the interaction with the calling party.


async callStatus(): Promise<any>;


  • Returns the current status data of the call as JSON object (e.g. calling number)
  • Example result:


{
    "id": "77b384529d252ff4d905334790dc58b5",
    "uuid": "6ec27ecc-6b39-402f-b8ec-57eedeb28e57",
    "calling_number": "441134960000",
    "called_number": "14255550123",
    "state": "waiting",
    "call_time": "2022-09-25T06:05:28.674Z",
    "line_id": "XAZRTBSUCVLLX",
    "tenant_id": "ax0v913b",
    "queue_position": 0,
    "estimated_wait_time": 0
}


async continue(data?: any): Promise<void>;


  • Leave IVR and continue call processing in downstream instance (e.g. inbound line)
  • Arguments:
    • data ... optional session data to be updated as JSON object (e.g. "line_id": "ID of the inbound line")



async hangup(sipStatusCode: number, reason: string): Promise<void>;


  • Hangup the call
  • Arguments:
    • sipStatusCode ... SIP-Status-Code
    • reason ... reason string


async reject(sipStatusCode: number, reason: string): Promise<void>;


  • Reject the call
  • Arguments:
    • sipStatusCode (optional)  ... SIP-Status-Code (default 486)
    • reason (optional) ... reason string (default "USER_BUSY")


async prefetchAudio(url: string, global?: boolean): Promise<void>;


  • Prefetch audio URL to get better performance on first use of the IVR
  • Arguments:
    • url ... URL of the audio file to be fetched
    • global


async ready(data?: any): Promise<void>;


  • Set the call to be connectable to an agent immediately
  • Arguments:
    • data ... optional session data to be updated as JSON object (e.g. "line_id": ...)


async setNoise(noise: number): Promise<void>;


  • Set background noise
  • Arguments:
    • noise ... intensity level of the noise to play


async sleep(ms: number): Promise<void>;


  • Sleep for the specified duration
  • Arguments:
    • ms ... duration in milliseconeds


async transfer(params: TransferParameters): Promise<void>;


  • Transfer the call to custom SIP endpoint (e.g. a specfic agent)
  • Arguments:
    • params: TransferParameters
      • targetAddress (required) ... target address to be dialed (e.g.: pstn:+49xxxxx or sip:xxxx@127.0.0.1:xxxx)
      • callerId (optional) ... for calls into PSTN the caller ID has to be registered phone number
      • redirectToIvr (optional) ... if set 'true', a new call leg will be created that must also be handled within this IVR (e.g. for simultaneous ringing on multiple target devices and custom call bridging)
      • maxTalkTimeSecond (optional) ... maximum allowed duration (talktime) of the transferred call


async update(data?: any): Promise<void>;


  • Updates the call session. The data will later be available in Megadialers's call interface
  • Arguments:
    • data ... the session data to be updated as JSON object


async dtmfPrompt(config: DTMFPromptConfiguration): Promise<string>;


  • Play audio and wait for a DTMF input
  • Arguments:
    • config: DTMFPromptConfiguration
      • audioFile ... the name audio file to play
      • maxRetries ... maximum number of replays of the audio file
      • allowedDigits ... string list of allowed input digits
  • Result:
    • the captured DTMF digit as string


async voicePrompt(config: VoicePromptConfiguration): Promise<VoicePromptResult>;


  • Create a voice prompt to the caller and eventually transcribe the answer
  • Arguments:
    • config: VoicePromptConfiguration
      • audioFile ... name of the audio file to be played
      • doASR (optional) ... do a transcription of calling party's voice to text
      • languageCode (optional) ... ISO-639-1 language code (de,en,es,pt,fr)
      • vadTimeout (optional) ... Milliseconds after which the recording should stop (default 120000)
      • vadMinSilence (optional) ... Milliseconds of silence after which the recording should stop (default 3000)
  • Result:
    • VoicePromptResult
      • audio ... captured audio/voice as number[]
      • audioLength ... length of the captured audio/voice
      • text ... transcribed text (if doASR was enabled and a text could be transcribed)


async prompt(config: PromptConfiguration): Promise<CommandResult>;


  • Low level prompt interface (use voicePrompt instead)
  • Arguments:
    • config: PromptConfiguration
      • audioFile (optional)
        • name of the audio file to be played
      • vadMinNoise (optional)
        • minimum time of calling party's voice to recognize it as voice in milliseconds (default 180)
      • vadMinSilence (optional)
        • minimum time of silence to recognize it as silence in milliseconds (default 840)
      • vadMaxNoise (optional)
        • maximum time of calling party's voice to stop the capturing in milliseconds (default 5000)
      • vadMinSNR (optional)
        • Signal to Noise Ratio (default 4)
      • vadMinSilenceLevel (optional)
        • maximum level of noise to be still interpreted as silence (default 10)
      • vadTimeout (optional)
        • maximum time of silence to stop Voice Activity Detection in milliseconds
        • 0 or unset disables vad detection, e.g. for music playback (default 5000)
      • bargeInMinNoise (optional)
        • minimum time of calling party's voice during IVR playback to stop the playback in milliseconds (default 0 == deactivated)
      • maxPlayOverhang (optional)
        • maximum number of milliseconds that calling party's voice and ivr voice may overlap at the end, so that calling party's voice is rated as a valid statement in milliseconds (default 500)
      • dtmfEnabled (optional)
        • enable DTMF detection (default off)
      • dtmfBargeIn (optional)
        • if 0, the first dtmf event will abort the playback (default)
        • if unset, DTMF detection starts with the end of the playback
        • if > 0, DTMF detection starts after this time in milliseconds
      • dtmfTimeout (optional)
        • time to wait for a DTMF input in milliseconds (default 5000)
  • Result:
    • CommandResult
      • bargedIn ... indicates whether the calling party barged in while the audio file was played
      • timedOut ... indicates whether the vad timed out before the calling party said something
      • offsetFromStart ... duration in milliseconds form the start of the playback until calling party responded
      • offsetFromEnd ... duration in milliseconds form the end of the playback until calling party responded


Audio utilities


In order to use the utilities for audio conversion you have to add an additional import atthe top of your IVR script.


const IVR_BASE = "4.x.x" // IVR_BASE version has to be at least "4.x.x"
const AUDIO_UTILS = "1.x.x"


These additional utilities can be used t oconvert the captured audio/voice data in to different sound formats.


Convert to WAV:


let result = await session.voicePrompt({
  audioFile: "audio_prompt"
});
if (result.audio) {
  let wav = await audioUtils.encodeWAV(result.audio)
  await fetch("https://your-webservice.com/audio/recording.wav", {
    method: "POST",
    headers: {
      'Content-Type': 'application/octet-stream'
    },
    body: wav
  })
}


Convert to MP3:


let result = await session.voicePrompt({
  audioFile: "audio_prompt"
});
if (result.audio) {
  let mp3 = await audioUtils.encodeMP3(result.audio)
  await fetch("https://your-webservice.com/audio/recording.mp3", {
    method: "POST",
    headers: {
      'Content-Type': 'application/octet-stream'
    },
    body: mp3
  })
}


Convert to ogg/Vorbis:


let result = await session.voicePrompt({
  audioFile: "audio_prompt"
});
if (result.audio) {
  let ogg = await audioUtils.encodeOgg(result.audio)
  await fetch("https://your-webservice.com/audio/recording.ogg", {
    method: "POST",
    headers: {
      'Content-Type': 'application/octet-stream'
    },
    body: ogg
  })
}


Advanced Features


Calling multiple parties at the same time.


In order to call multiple parties simultaneously you have to create a new call leg for every target you eventually want to reach using session.transfer(). Additionally you have to specifiy that you want the new call leg to be redirected to this IVR. This transfer call behaves like a new outbound call and is subject to the same bidding rules of the bid manager as your other outbound calls.


To distinguish whether it is the initial inbound call or a call leg you created, session.isTransferCallLeg() can be used. Within a call leg, the same functions can be used that are available in a regular inbound call. For example, a message can be played to the transfer target using session.playAudio() that it is a transferred call.


The transfer destination is connected to the initial inbound call using session.bridge(). As soon as the initial inbound call has been bridged to a transfer target, it is no longer available in the IVR session. This can be checked using session.getParentCallObject(). If the ParentCallObject is not available, the call has already been transferred to another destination.



Example:


const IVR_BASE = "5.x.x"

/**
 * IVR main function
 * @param session ...encapsulates the state and functions related to the IVR execution
 */
async function handleCall(session: Session): Promise<void> {
    /** 
     * the audio file variant can be changed at any time
     */ 
    session.setVariant("default") 

    /**   
     * to handle the call you have to answer it at first
     */ 
    await session.answer();

    /** 
     * optionally wait 10 ms to ensure answer command is done 
     */
    await session.sleep(10);

    /**
     * check whether this is the initial call or a transferred call
     */
    if (!session.isTransferCallLeg()) {
        await handleInitialCallLeg(session)
    } else {
        await handleTransferCallLeg(session)
    }
}

/**
 * Handles the initial call of this IVR
 */
async function handleInitialCallLeg(session: Session): Promise<void> {
    /**
     * Transfer this call to a pstn target
     */
    var transfer_result = await session.transfer({
        targetAddress: "pstn:+123456789",
        callerId: "15555555555", // caller-id has to be one of your registered phone numbers in Megadialer (if omitted the called number of this call will be used)
        redirectToIvr: true // this parameter indicates, that the transfer target should be handled by this IVR
    });
    if (transfer_result.error) {
        session.log(`error while starting transfer: ${transfer_result.error}`);
        session.hangup(16, "transfer failed");
        return;
    }

    /**
     * Additionally transfer this call to a second sip target
     */
    transfer_result = await session.transfer({
        targetAddress: "sip:target@my-target-sip-provider.com",
        callerId: "15555555555",
        redirectToIvr: true
    });
    if (transfer_result.error) {
        session.log(`error while starting transfer: ${transfer_result.error}`);
        session.hangup(16, "transfer failed");
        return;
    }
    
    session.log(`transfer started`);
    await session.playAudio("transfer_progress", true);// play message in a loop to the caller that the transfer is in progress (until it will be bridged to the transfer target)

    // wait 30 second (until transfer was eventually established)
    await session.sleep(30000)
}

/**
 * Handles the call leg that was created via session.transfer(...)
 */
async function handleTransferCallLeg(session: Session): Promise<void> {
    /**
     * check whether this call was already bridged to the other call leg
     */
    let parentCall = session.getParentCallObject();
    if (!parentCall) {
        session.log("no parent call");
        await session.hangup(16, "call was answered by other party");
        return;
    }
    session.log("parentCall: " + JSON.stringify(parentCall));

    await session.playAudio("transfer_message", true); // this message is played to the transfer target
    await session.sleep(5000) // optionally wait 5 seconds, so that the transfer target can hear the message
    await session.bridge() // this call connects the initial call with this transfer target
}

/**
 * This handler is beeing called after the call ended
 * @param session ...encapsulates the state and functions related to the IVR execution
 */
 async function handleHangup(session: Session): Promise<void> {
    session.log(`hangup received`);
}

initConnection({
    onCallStart: handleCall,
    onCallEnd: handleHangup
});

Was this article helpful?

That’s Great!

Thank you for your feedback

Sorry! We couldn't be helpful

Thank you for your feedback

Let us know how can we improve this article!

Select atleast one of the reasons

Feedback sent

We appreciate your effort and will try to fix the article