Make a call and basic IVR menu with Node.js

You can quickly see how the Voice API works by calling yourself using the API and playing a basic IVR menu.

What you need to know before you start

Before you can get started, you need the following already set up:

  • Set all Voice API configuration settings.
  • NPM and a familiarity with how to install packages.
  • Node.js and a familiarity with how to create a new app.
  • Ngrok to create a public URL to your localhosted node backend

Set up your Node.js application

Create a new node app with npm.

Copy
Copied
npm init

Accept the defaults for the application.

Install your dependencies

Add the fetch package with npm to generate the necessary dependencies.

Copy
Copied
npm install 'body-parser'
npm install 'express'
npm install 'axios'
npm install 'ngrok'

Create the files you will need

Create a new file named index.js in the project and paste the provided "index.js" code into the file.

index.js

This code is used to make a phone call and play a basic IVR menu.

const express = require('express');
const bodyParser = require("body-parser");
const ngrok = require('ngrok');
const axios = require('axios')

const PORT = 8081;

const app = express();
app.use(bodyParser.json());

const APPLICATION_KEY = ''
const APPLICATION_SECRET = ''
const CLI = ''
const TO_NUMBER = ''
const eq = '\"'

app.post('/', async (req, res) => {
  let eventName = req.body.event;
  //console.log(`:: INCOMING HTTP BODY :: ", req.body)
  switch (eventName){
    case 'pie':
      var value = req.body.menuResult.value;
      console.log(`:: INCOMING EVENT ::`, req.body.event)
      //console.log(`:: INCOMING EVENT BODY ::`, req.body)
      console.log(`:: IVR MENU CHOICE :: Destination: `, TO_NUMBER, `selected value: `, value);
    case 'dice':
      console.log(`:: INCOMING EVENT ::`, req.body.event)
      //console.log(`:: INCOMING EVENT BODY ::`, req.body)
      res.sendStatus(200);
      break;
    default:
      //console.log(`:: INCOMING HTTP BODY::`, req.body)
      console.log(`:: ERROR ::  Sorry, there was an error with your request. Please inspect HTTP body above`);
      res.sendStatus(404);
  }
});

function calloutRunMenu(url) {
    axios({
      method: 'post',
      url: 'https://calling.api.sinch.com/calling/v1/callouts',
      data: {
        "method": "CustomCallout",
        "customCallout": {
          "ice": "{\"action\": {\"name\": \"ConnectPstn\",\"number\": "+eq+TO_NUMBER+eq+",\"cli\": "+eq+CLI+eq+"}}",
          "ace": "{\"action\": {\"name\": \"RunMenu\",\"locale\": \"en-US\",\"menus\": [{\"id\": \"main\",\"mainPrompt\": \"#tts[ Welcome to the main menu. Press 1 to confirm order or 2 to cancel]\",\"timeoutMills\": 5000,\"options\": [ {\"dtmf\": \"1\",\"action\": \"return(confirm)\"}, {\"dtmf\": \"2\",\"action\": \"return(cancel)\"}]}]}}",
          "pie" : url,
          "dice" : url
        }},
      auth: {
          username: APPLICATION_KEY,
          password: APPLICATION_SECRET
        },   
        headers: {
          'content-type': 'application/json'
      }
    }).then((response) => {
        console.log(`:: INFO :: Custom Callout Data: ${response.config.data}`); 
        }, (error) => {
          console.log(error);
        });
};

app.listen(PORT, async () => {
  const url = await ngrok.connect(PORT);
  console.log(`:: INFO :: Node.js local server is publicly-accessible at ${url}/`);
  console.log(`:: INFO :: Listening at http://localhost:` + PORT);
  console.log(`:: INFO :: CustomCallout initiated with outgoing IVR`);
  calloutRunMenu(url);
});

Fill in your parameters

Assign your values to the following parameters:

ParameterYour value
APPLICATION_KEYThe key found on your Sinch dashboard.
APPLICATION_SECRETThe secret found on your Sinch dashboard.
CLIAny purchased number you have with us on your account. Find the number on your Sinch dashboard by clicking on your app, navigating to the Voice and Video tab, and looking in the Inbound Numbers section.
TO_NUMBERThe phone number that you want to call.

Save the file.

CustomCallout RunMenu and parsing the choice made

Now you can execute the code and make your text-to-speech call. Run the following command:

Copy
Copied
node index.js

The node listener will start, you will see your custom data that is sent.

Copy
Copied
INFO :: Node.js local server is publicly-accessible at https://ffff-ff-fff-fff-ff.ngrok.io
INFO :: Listening at http://localhost:8081
INFO :: CustomCallout initiated with outgoing IVR
INFO :: Custom Callout Data:
{ "Method":"CustomCallout","customCallout":{"ice":"{\"action\": {\"destination\": {\"type\": \"number\",\"endpoint\": \"+00000000000\"  },\"cli\": \"+00000000000\",\"name\": \"ConnectPstn\"}}","ace":"{\"action\": {\"name\": \"RunMenu\",\"locale\": \"en-US\",\"menus\": [{\"id\": \"main\",\"mainPrompt\": \"#tts[ Welcome to the main menu. Press 1 to confirm order or 2 to cancel]\",\"timeoutMills\": 5000,\"options\": [ {\"dtmf\": \"1\",\"action\": \"return(confirm)\"}, {\"dtmf\": \"2\",\"action\": \"return(cancel)\"}]}]}},"pie":"https://ffff-ff-fff-fff-ff.ngrok.io","dice":"https://ffff-ff-fff-fff-ff.ngrok.io"}}
You will recieve a callout to your selected number. Once a choice is made the PIE event will be returned to the callback server stated in the pie section of the customCallout. There are two possible choices as per the configured RunMenu (IVR) which are confirm or cancel. You will see in this message what choice was made, the Destination will show the TO_NUMBER you dialed.
Copy
Copied
INCOMING EVENT :: pie
IVR MENU CHOICE :: Destination:  "+00000000000" selected value:  confirm"

Bonus: Inspect the incoming HTTP Call Event bodies

If you uncomment the //console.log(`:: INCOMING HTTP BODY :: ", req.body) lines to see the incoming call requests (example pie event below) This will give you a better visibility whilst developing and debugging your incoming call events.
Copy
Copied
 :: INCOMING HTTP BODY ::  {
   event: 'pie',
   callid: '00000000-dddd-2222-eeee-444444444444',
   timestamp: '2022-02-14T10:53:16Z',
   menuResult: {
     addToContext: [],
     type: 'return',
     value: 'confirm',
     menuId: 'main',
     inputMethod: 'dtmf'
   },
   version: 1,
   applicationKey: '11111111-aaaa-2222-bbbb-333333333333'
 }

Next steps

The code you used in the index.js file sends a POST request to the Sinch API /callouts endpoint to make the call. Click here to read more about the /callouts endpoint.

We'd love to hear from you!
Rate this content:
Still have a question?
 
Ask the community.

index.js

This code is used to make a phone call and play a basic IVR menu.

const express = require('express');
const bodyParser = require("body-parser");
const ngrok = require('ngrok');
const axios = require('axios')

const PORT = 8081;

const app = express();
app.use(bodyParser.json());

const APPLICATION_KEY = ''
const APPLICATION_SECRET = ''
const CLI = ''
const TO_NUMBER = ''
const eq = '\"'

app.post('/', async (req, res) => {
  let eventName = req.body.event;
  //console.log(`:: INCOMING HTTP BODY :: ", req.body)
  switch (eventName){
    case 'pie':
      var value = req.body.menuResult.value;
      console.log(`:: INCOMING EVENT ::`, req.body.event)
      //console.log(`:: INCOMING EVENT BODY ::`, req.body)
      console.log(`:: IVR MENU CHOICE :: Destination: `, TO_NUMBER, `selected value: `, value);
    case 'dice':
      console.log(`:: INCOMING EVENT ::`, req.body.event)
      //console.log(`:: INCOMING EVENT BODY ::`, req.body)
      res.sendStatus(200);
      break;
    default:
      //console.log(`:: INCOMING HTTP BODY::`, req.body)
      console.log(`:: ERROR ::  Sorry, there was an error with your request. Please inspect HTTP body above`);
      res.sendStatus(404);
  }
});

function calloutRunMenu(url) {
    axios({
      method: 'post',
      url: 'https://calling.api.sinch.com/calling/v1/callouts',
      data: {
        "method": "CustomCallout",
        "customCallout": {
          "ice": "{\"action\": {\"name\": \"ConnectPstn\",\"number\": "+eq+TO_NUMBER+eq+",\"cli\": "+eq+CLI+eq+"}}",
          "ace": "{\"action\": {\"name\": \"RunMenu\",\"locale\": \"en-US\",\"menus\": [{\"id\": \"main\",\"mainPrompt\": \"#tts[ Welcome to the main menu. Press 1 to confirm order or 2 to cancel]\",\"timeoutMills\": 5000,\"options\": [ {\"dtmf\": \"1\",\"action\": \"return(confirm)\"}, {\"dtmf\": \"2\",\"action\": \"return(cancel)\"}]}]}}",
          "pie" : url,
          "dice" : url
        }},
      auth: {
          username: APPLICATION_KEY,
          password: APPLICATION_SECRET
        },   
        headers: {
          'content-type': 'application/json'
      }
    }).then((response) => {
        console.log(`:: INFO :: Custom Callout Data: ${response.config.data}`); 
        }, (error) => {
          console.log(error);
        });
};

app.listen(PORT, async () => {
  const url = await ngrok.connect(PORT);
  console.log(`:: INFO :: Node.js local server is publicly-accessible at ${url}/`);
  console.log(`:: INFO :: Listening at http://localhost:` + PORT);
  console.log(`:: INFO :: CustomCallout initiated with outgoing IVR`);
  calloutRunMenu(url);
});