Self Service Using Keypad Input

In this tutorial, we will guide you through implementing a basic self-service IVR application with FreeClimb that utilizes user DTMF (e.g. keypad) inputs. This system will perform the following actions:

  • Receive an incoming call via a FreeClimb application
  • Get user DTMF (e.g. keypad) input
  • Redirect a user to the appropriate endpoint
  • Return information to the user based on previous input
You can also find the code for this sample app on GitHub

👍

You're ready for this tutorial if you have:

Followed the IVR sample app set-up instructions


Step 1: Create the express server

The server will provide endpoints where user input can be captured. Create an index.js
file in your project directory, import the needed dependencies, and create/configure the Express application:

require('dotenv-safe').config()
const express = require('express')
const bodyParser = require('body-parser')
const app = express()
app.use(bodyParser.urlencoded({ extended: true }))
app.use(bodyParser.json())
const freeclimbSDK = require('@freeclimb/sdk')

const port = process.env.PORT || 3000

Step 2: Handle an incoming call

The first step in our call routing application will be to handle incoming calls. To do this, add the following to index.js:

const host = process.env.HOST
const accountId = process.env.ACCOUNT_ID
const apiKey = process.env.API_KEY
const freeclimb = freeclimbSDK(accountId, apiKey)
 
 
app.post('/incomingCall', (req, res) => {
  res
    .status(200)
    .json(
      freeclimb.percl.build( // return PerCL commands for greeting and redirect to the main menu endpoint
        freeclimb.percl.say('Welcome to the Node self service IVR.'),
        freeclimb.percl.pause(100),
        freeclimb.percl.redirect(`${host}/mainMenuPrompt`)
      )
    )
})
 
 
app.listen(port, () => {
    console.log(`Starting server on port ${port}`)
})

module.exports = { app }

We'll also use index.js to contain the routes for handling call end and transfer. To create these add the following to index.js after /incomingCall but before app.listen:

app.post('/transfer', (req, res) => {
  res
    .status(200)
    .json(
      freeclimb.percl.build(
        freeclimb.percl.say('there are no operators available at this time'),
        freeclimb.percl.redirect(`${host}/endCall`)
      )
    )
})

app.post('/endCall', (req, res) => {
  res
    .status(200)
    .json(
      freeclimb.percl.build(
        freeclimb.percl.say(
          'Thank you for calling the Node self service IVR , have a nice day!'
        ),
        freeclimb.percl.hangup()
      )
    )
})

Step 3: Collect digits via a main menu

Next we'll create a main menu for collecting DTMF input from the user and routing their call appropriately. To do this create a new file in your project directory called mainMenu.js and add the following:

require('dotenv-safe').config()
const express = require('express')
const freeclimbSDK = require('@freeclimb/sdk')
const host = process.env.HOST
const accountId = process.env.ACCOUNT_ID
const apiKey = process.env.API_KEY
const freeclimb = freeclimbSDK(accountId, apiKey)

router = express.Router()

let mainMenuErrCount = 0

router.post('/mainMenuPrompt', (req, res) => {
  res.status(200).json(
    freeclimb.percl.build(
      freeclimb.percl.getDigits(`${host}/mainMenu`, { // set actionURL -> will redirect here on user input
        prompts: [
          freeclimb.percl.say(
            'Press 1 for existing orders, 2 for new orders, or 0 to speak to an operator'
          )
        ],
        maxDigits: 1,
        minDigits: 1,
        flushBuffer: true
      })
    )
  )
})

router.post('/mainMenu', (req, res) => {
  const getDigitsResponse = req.body
  const digits = getDigitsResponse.digits // get digit input from request body
  const menuOpts = new Map([
    [
      '1',
      {
        script: 'Redirecting your call to existing orders.',
        redirect: `${host}/accountNumberPrompt`
      }
    ],
    [
      '2',
      {
        script: 'Redirecting your call to new orders.',
        redirect: `${host}/transfer`
      }
    ],
    [
      '0',
      { script: 'Redirecting you to an operator', redirect: `${host}/transfer` }
    ]
  ])
  if ((!digits || !menuOpts.get(digits)) && mainMenuErrCount < 3) {
    mainMenuErrCount++
    res
      .status(200)
      .json( 
        freeclimb.percl.build( // if no or incorrect input and errors below max allowed limit return PerCL commands for redirect back to getDigits collection at /mainMenuPrompt
          freeclimb.percl.say('Error, please try again'),
          freeclimb.percl.redirect(`${host}/mainMenuPrompt`)
        )
      )
  } else if (mainMenuErrCount >= 3) { // creating a max error limit stops good actors from getting stuck and bad actors from circling endlessly in your IVR
    mainMenuErrCount = 0
    res
      .status(200)
      .json(
        freeclimb.percl.build( // if max error limit reached return PerCL commands for redirect to /endCall
          freeclimb.percl.say('Max retry limit reached'),
          freeclimb.percl.pause(100),
          freeclimb.percl.redirect(`${host}/endCall`)
        )
      )
  } else {
    mainMenuErrCount = 0
    res
      .status(200)
      .json(
        freeclimb.percl.build( // on successful input redirect return say and redirect PerCL commands based on option selected by the user
          freeclimb.percl.say(menuOpts.get(digits).script),
          freeclimb.percl.redirect(menuOpts.get(digits).redirect)
        )
      )
  }
})

module.exports = router

To make these routes available from our /incomingCall endpoint defined in index.js, add the following content to your index.js file before the /incomingCall endpoint:

const mainMenuRoutes = require('./mainMenu')

app.use('/', mainMenuRoutes)

Step 4: Get an account number

Next we'll create routes to ask the user for an account number and redirect according to their input. To do this, create a new file in your project directory called accountNumberEntry.js and add the following:

require('dotenv-safe').config()
const express = require('express')
const freeclimbSDK = require('@freeclimb/sdk')
const host = process.env.HOST
const accountId = process.env.ACCOUNT_ID
const apiKey = process.env.API_KEY
const freeclimb = freeclimbSDK(accountId, apiKey)

router = express.Router()
let acctMenuErrCount = 0

router.post('/accountNumberPrompt', (req, res) => {
  res.status(200).json(
    freeclimb.percl.build(
      freeclimb.percl.getDigits(`${host}/accountNumber`, { // on digit collection complete redirect to /accountNumber
        prompts: [freeclimb.percl.say('Please Enter your account number')], // prompt for acct number
        maxDigits: 6, //set 6 digit max for 6 digit acct number
        minDigits: 1,
        flushBuffer: true
      })
    )
  )
})

router.post('/accountNumber', (req, res) => {
  const getDigitsResponse = req.body
  const digits = getDigitsResponse.digits // get digit input from request body
  if ((!digits || digits.length < 6) && acctMenuErrCount < 2) {
    acctMenuErrCount++
    res
      .status(200)
      .json(
        freeclimb.percl.build( // if no or incorrect input and errors < max error limit return PerCL commands for redirect back to /accountNumberPrompt for acct number re-entry
          freeclimb.percl.say(
            'Error, please enter your six digit account number or press 0 to speak with an operator'
          ),
          freeclimb.percl.pause(100),
          freeclimb.percl.redirect(`${host}/accountNumberPrompt`)
        )
      )
  } else if (acctMenuErrCount >= 2) {
    acctMenuErrCount = 0
    res
      .status(200)
      .json(
        freeclimb.percl.build( // if max error limit reached redirect return percl commands to redirect to operator at /transfer
          freeclimb.percl.say(
            'Max retry limit reached, please wait while we connect you to an operator'
          ),
          freeclimb.percl.pause(100),
          freeclimb.percl.redirect(`${host}/transfer`)
        )
      )
  } else {
    acctMenuErrCount = 0
    res
      .status(200)
      .json(
        freeclimb.percl.build(
          freeclimb.percl.redirect(
            `${host}/confirmAccountNumberPrompt?acct=${digits}`
          )
        )
      )
  }
})
module.exports = router

Then, make the routes available by adding the following to your index.js file before the /incomingCall endpoint:

const accountNumberEntryRoutes = require('./accountNumberEntry')

app.use('/', accountNumberEntryRoutes)

Step 5: Confirm the account number

Next we'll create routes to :

  • read the input from the user back to them and
  • ask the to confirm their previous entry

To do this, create a file in your project directory called accountNumberConfirmation.js and add the following:

require('dotenv-safe').config()
const express = require('express')
const freeclimbSDK = require('@freeclimb/sdk')
const host = process.env.HOST
const accountId = process.env.ACCOUNT_ID
const apiKey = process.env.API_KEY
const freeclimb = freeclimbSDK(accountId, apiKey)

router = express.Router()

let confirmNumberErrCount = 0
let retries = 0

router.post('/confirmAccountNumberPrompt', (req, res) => {
  res.status(200).json(
    freeclimb.percl.build(
      freeclimb.percl.getDigits(
        `${host}/confirmAccountNumber?acct=${req.param('acct')}`, // redirect to /confirmAccountNumber with acct number from incoming request
        {
          prompts: [
            freeclimb.percl.say(
              `You entered ${req.param(
                'acct'
              )} is that correct? Press 1 to confirm your account number or 2 to try again`
            )
          ],
          maxDigits: 1,
          minDigits: 1,
          flushBuffer: true
        }
      )
    )
  )
})

router.post('/confirmAccountNumber', (req, res) => {
  const getDigitsResponse = req.body
  const digits = getDigitsResponse.digits // get digits input from request body
  const menuOpts = new Map([ // map valid inputs to options
    [
      '1',
      {
        script: 'proceeding to account number lookup.',
        redirect: `${host}/accountLookup?acct=${req.param('acct')}`
      }
    ],
    [
      '2',
      {
        script: 'Ok',
        redirect: `${host}/accountNumberPrompt`
      }
    ],
    [
      '0',
      { script: 'Redirecting you to an operator', redirect: `${host}/transfer` }
    ]
  ])
  if ((!digits || !menuOpts.get(digits)) && confirmNumberErrCount < 3) {
    confirmNumberErrCount++
    res
      .status(200)
      .json(
        freeclimb.percl.build( // if no or incorrect input and errors < max error limit return PerCL commands to redirect back to /confirmAccountNumberPrompt for retry
          freeclimb.percl.say('Error'),
          freeclimb.percl.redirect(
            `${host}/confirmAccountNumberPrompt?acct=${req.param('acct')}`
          )
        )
      )
  } else if (confirmNumberErrCount >= 3 || retries >= 2) {
    confirmNumberErrCount = 0
    res
      .status(200)
      .json(
        freeclimb.percl.build(
          freeclimb.percl.say(
            'Please wait while we connect you to an operator'
          ),
          freeclimb.percl.pause(100),
          freeclimb.percl.redirect(`${host}/transfer`)
        )
      )
  } else {
    confirmNumberErrCount = 0
    if (digits === '2') { // increment tracker for user selected retries if selected
      retries++ // retries tracked separately from input errors
    } else if (digits === '1') {
      retries = 0 // reset user selected retries once this menu has been successfully passed
    }
    res
      .status(200)
      .json(
        freeclimb.percl.build( // return PerCL say and redirect commands based on user choice
          freeclimb.percl.say(menuOpts.get(digits).script),
          freeclimb.percl.redirect(menuOpts.get(digits).redirect)
        )
      )
  }
})

module.exports = router

Then, make the routes available by adding the following to your index.js file before the /incomingCall endpoint:

const accountNumberConfirmationRoutes = require('./accountNumberConfirmation')

app.use('/', accountNumberConfirmationRoutes)

Step 6: Create an accounts file for testing

In order to simulate having an accounts service that returns accounts with several different statuses, we'll create a new file for testing purposes. To do this, create a new file called accounts.js in your main directory and add the following:

const accounts = new Map([
  [
    '111222',
    {
      open: true,
      frequentBuyer: true,
      name: 'John Smith',
      mostRecentOrderDate: 'March 30th 2020'
    }
  ],
  [
    '222333',
    {
      open: true,
      frequentBuyer: false,
      name: 'Jane Smith',
      mostRecentOrderDate: 'March 30th 2020'
    }
  ],
  [
    '333444',
    {
      open: false,
      frequentBuyer: true,
      name: 'Sam Smith',
      mostRecentOrderDate: 'March 30th 2020'
    }
  ]
])

module.exports = accounts

Step 7: Lookup the account number

Next we'll create a route to take the account number entered and confirmed by the user in steps 4 and 5 and search for it in the data structure we created in step 6. We'll also direct the call based on the status of the account.

To do this create a new file in your project directory called accountLookup.js and add the following content:

require('dotenv-safe').config()
const express = require('express')
const freeclimbSDK = require('@freeclimb/sdk')
const accounts = require('./accounts')
const host = process.env.HOST
const accountId = process.env.ACCOUNT_ID
const apiKey = process.env.API_KEY
const freeclimb = freeclimbSDK(accountId, apiKey)

router = express.Router()
let retries = 0

router.post('/accountLookup', (req, res) => {
  if (!accounts.get(req.param('acct')) && retries < 2) {
    retries++
    res
      .status(200)
      .json(
        freeclimb.percl.build( // if account doesnt exist return redirect to /accountNumber prompt to allow for retry
          freeclimb.percl.say('Sorry, we couldnt find that account number.'),
          freeclimb.percl.redirect(`${host}/accountNumberPrompt`)
        )
      )
  } else if (retries >= 2) {
    retries = 0
    res
      .status(200)
      .json(
        freeclimb.percl.build( // if max retry reached return PerCl commands for redirect to operator at /transfer
          freeclimb.percl.say(
            'Max retry limit reached, please wait while we connect you to an operator'
          ),
          freeclimb.percl.pause(100),
          freeclimb.percl.redirect(`${host}/transfer`)
        )
      )
  } else {
    res
      .status(200)
      .json(
        freeclimb.percl.build( //on successful lookup of acct return PerCL commands to redirect to /accountRead and pass account number as param
          freeclimb.percl.redirect(
            `${host}/accountRead?acct=${req.param('acct')}`
          )
        )
      )
  }
})

module.exports = router

Then, make the routes available by adding the following to your index.js file before the /incomingCall endpoint:

const accountLookupRoutes = require('./accountLookup')

app.use('/', accountLookupRoutes)

Step 8: Read account information back to caller

Finally, we'll add the route for reading information about the user's account back to the caller. To do this, create a new file called accountRead.js and add the following:

require('dotenv-safe').config()
const express = require('express')
const freeclimbSDK = require('@freeclimb/sdk')
const accounts = require('./accounts')
const host = process.env.HOST
const accountId = process.env.ACCOUNT_ID
const apiKey = process.env.API_KEY
const freeclimb = freeclimbSDK(accountId, apiKey)

router = express.Router()

router.post('/accountRead', (req, res) => {
  account = accounts.get(req.param('acct'))

  if (account.open) {
    if (account.frequentBuyer) {
      res
        .status(200)
        .json(
          freeclimb.percl.build( // return PerCl commands for say w/frequent buyer script and redirect to /transfer
            freeclimb.percl.say(
              'Welcome back platinum member, please wait while we connect you with a customer service representative.'
            ),
            freeclimb.percl.pause(100),
            freeclimb.percl.redirect(`${host}/transfer`)
          )
        )
    } else {
      res
        .status(200)
        .json(
          freeclimb.percl.build( // return PerCL commands for say w/ normal customer script and redirect to /transfer
            freeclimb.percl.say(
              `Welcome back ${account.name}, I've found your most recent order from ${account.mostRecentOrderDate}, please hold while I connect you with a customer service representative. `
            ),
            freeclimb.percl.pause(100),
            freeclimb.percl.redirect(`${host}/transfer`)
          )
        )
    }
  } else {
    res
      .status(200)
      .json(
        freeclimb.percl.build( // return PerCL commands for say w/ error script and redirect to /transfer
          freeclimb.percl.say(
            'This account appears to be closed please wait while we transfer you to an operator for asistance'
          ),
          freeclimb.percl.redirect(`${host}/transfer`)
        )
      )
  }
})

module.exports = router

Then, make the routes available by adding the following to your index.js file before the /incomingCall endpoint:

const accountReadRoutes = require('./accountRead')

app.use('/', accountReadRoutes)

Step 9: Run your app

To hear your IVR DTMF self service app in action, run the following command at the command line:

yarn start

Once you do this, call the FreeClimb number associated with your application. From there you should be able to experience your self service application.

Congrats! You can now build your own custom IVR DTMF self service application. 🥳🥳

Or, to add voice-enabled capabilities, continue with the our Self Service Using Text-to-Speech tutorial.

For a more advanced IVR tutorial, try out our Pay by Phone with Voice Calling sample app.