Pay by Phone with Voice Calling

Use the Say PerCL command and privacymode to process PCI compliant payments over voice call

In this tutorial, we will guide you through implementing a pay by phone IVR system using FreeClimb. This system will perform the following actions:

  • Receive an incoming call via a FreeClimb application
  • Route users via DTMF (e.g. keypad) input
  • Collect and process credit card payment information using privacymode
  • Use PerCL to send an SMS payment confirmation message to user
You can also find the code for this sample app on GitHub

👍

You're ready for this tutorial if you have the following:

A FreeClimb account
A registered application with a named alias
A configured FreeClimb number
Trial accounts: A verified number
Your language and tools:


Step 1: App set up

Make your server locally accessible

The fastest way to start testing your FreeClimb application is to temporarily make your local server publicly accessible through a tunneling service. We'll use ngrok to do this.

Once you have downloaded ngrok and unzipped the file to install it, open your terminal and navigate to the directory where you've unzipped ngrok. Use the following command to start a HTTP tunnel on port 3000:

ngrok http 3000

Once you run ngrok using the above command, you should receive a response with a public URL that looks something like this:

ngrok by @inconshreveable
 
Tunnel Status online
Version 2.0/2.0
Web Interface http://127.0.0.1:4040
Forwarding http://92832de0.ngrok.io -> localhost:3000
Forwarding https://92832de0.ngrok.io -> localhost:3000
 
Connnections ttl opn rt1 rt5 p50 p90
0 0 0.00 0.00 0.00 0.00

The forwarding URLs provided point to your local server. Save the URLs and move on to the next step. Make sure to keep ngrok open as your continue to build and run your app.

Configure your application's endpoints

Now that you've got a public URL, you're ready to configure your application's endpoints. We'll be configuring the voiceUrl using your ngrok URL and the route reference /incomingCall.

Go to the Apps page in your dashboard. You should see your registered FreeClimb app.

3374

Your Apps page with a registered app.

Click Edit Config to enter the ngrok forwarding URL into your registered app's voiceUrl field and add the route /incomingCall to the end of it. When you're done, the App Config should look something like this:

1654

Example of a completed App Config.

Save your updated App Config and proceed with the next steps.

Get dependencies

To start, create a new directory for the project. Within this directory create a file named package.json and add the following content:

{
    "name": "Node_Pay_By_Phone",
    "version": "1.0.0",
    "description": "Simple pay-by-phone application tutorial",
    "main": "index.js",
    "license": "MIT",
    "dependencies": {
        "@freeclimb/sdk": "^1.1.1",
        "body-parser": "^1.19.0",
        "dotenv": "^8.2.0",
        "dotenv-safe": "^8.2.0",
        "express": "^4.17.1"
    },
    "scripts": {
        "start": "DEBUG=express:* node ."
    }
}

These dependencies can be installed from the command line by using the following command:

yarn install

Set environment variables

In order to authenticate your requests with the FreeClimb platform, we need to include your API credentials (account ID and API key) in every request. Your account ID and API key can be found in the FreeClimb dashboard.

📘

Why put API credentials in environment variables?

If you were to push your code to a public repository, those credentials would be public and an attacker could steal your account. To prevent this, we create environment variables with your API credentials and import them into your code using dotenv-safe.

This sample app uses dotenv-safe to read in your credentials, and anything else you save, as environment variables. As required by dotenv-safe, before creating our .env file we must create a .env.example file that outlines the environment variables that will need to be present in the actual .env file.

🚧

You must include both a .env.example file and a .env file in order for dotenv-safe to work properly.

To do so, create a file in your project's directory named .env.example and add the following content:

ACCOUNT_ID=
API_KEY=
HOST=
PORT=
FC_NUMBER=

Once you've created your .env.example file, create a .env file in the root directory of your repo. Make sure to add the file to your .gitignore file. Add your account ID and API key to your .env file.

We'll also include the host, port, and FreeClimb number.

If you've set up ngrok to expose a specific port from your local machine, use the public facing link provided by ngrok as your HOST variable.

The port number will allow you to specify the port on which the app will run. For example, if you were to enter PORT=3000 to view the app, you would direct your browser to http://localhost:3000.

If you haven't already, make sure to assign the registered application you configured with your ngrok URL to the configured FreeClimb number you will be using for this sample application. Use this phone number as your FC_NUMBER variable.

Your .env file should look as follows:

ACCOUNT_ID="YOUR-ACCOUNT-ID"
API_KEY="YOUR-API-KEY"
HOST="YOUR-HOST"
PORT="YOUR-PORT-NUMBER"
FC_NUMBER="+15555550010" // make sure your FreeClimb number is in E.164 format

Step 2: Create and share a FreeClimb SDK instance

Throughout this application, we will be using a FreeClimb SDK object to generate our PerCL responses. Rather than re-create this object in every file that needs it, we will instread create one instance and pass it using require where needed.

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

const freeclimbSDK = require('@freeclimb/sdk')
const accountId = process.env.ACCOUNT_ID
const apiKey = process.env.API_KEY
const freeclimb = freeclimbSDK(accountId, apiKey)

module.exports = freeclimb

Step 3: Handle incoming calls

The first step in our application will be handling incoming calls and redirecting them to the appropriate endpoint. Additionally, we also need to define behavior for ending a call. To do so, create a file called index.js in your project directory and add the following:

require('dotenv-safe').config()
 
const express = require('express')
const bodyParser = require('body-parser')
const freeclimb = require('./freeclimb')
 
const app = express()
 
app.use(bodyParser.urlencoded({ extended: true }))
app.use(bodyParser.json())
 
 
const port = process.env.PORT || 3000
const host = process.env.HOST

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 Pay By Phone Sample App.'),
            freeclimb.percl.pause(100),
            freeclimb.percl.redirect(`${host}/mainMenuPrompt`)
        )
    )
})

app.post('/transfer', (req, res) => { // mimic transfer to an operator
    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) => { // end call with script + hangup
    res.status(200).json(
        freeclimb.percl.build(
            freeclimb.percl.say(
                'Thank you for calling the Node Pay By Phone sample app , have a nice day!'
            ),
            freeclimb.percl.hangup()
        )
    )
})

if (process.env.NODE_ENV !== 'test') {
    app.listen(port, () => {
        console.log(`Starting server on port ${port}`)
    })
}

module.exports = { app }

Next, we'll create a main menu that will give callers the option to proceed with their payment or transfer to an operator using the /transfer endpoint defined in index.js. To do this, create a file called mainMenu.js in your project directory and add the following:

const express = require('express')
const freeclimb = require('./freeclimb')

const host = process.env.HOST

const router = express.Router()

let mainMenuErrCount = 0

router.post('/mainMenuPrompt', (req, res) => {
    res.status(200).json(
        freeclimb.percl.build(
            freeclimb.percl.getDigits(`${host}/mainMenu`, { // once dtmf input is collected redirect to main menu for further routing
                prompts: [
                    freeclimb.percl.say(
                        'To make a one time payment please press 1, or press 0 to speak with 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',
            {
                redirect: `${host}/ccAmountPrompt`
            }
        ],
        [
            '0',
            {
                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( // on successful input redirect return redirect PerCL commands based on option selected by the user
            freeclimb.percl.build(freeclimb.percl.redirect(menuOpts.get(digits).redirect))
        )
    }
})

module.exports = router

📘

Interested in routing callers via speech input?

Check out our Call Routing Using Voice Input tutorial to learn more about how to collect speech input from callers and route them accordingly.


Step 4: Handle customer data

Throughout this application, we'll be prompting the caller with various scripts in order to collect information from them. Unlike our other IVR tutorials, this tutorial also tracks whether or not the caller has already heard certain prompts and will play shorter, more succint messages if the caller is hearing them a second time. This aligns with industry best practices for IVRs and VUIs (Voice User Interfaces).

Adding customer.js and caller.js files to our project allows us to handle customer data within the IVR system for this purpose.

📘

Why have secondary prompts in your IVR system?

Customers sometimes make mistakes or need to re-enter information via an IVR. When they do so, they don't necessarily need to hear the same lengthy prompts again. To provide a more efficient user experience, we play shorter, more succinct prompts for repeat interactions with the system in the same call (e.g. re-entering credit card information after making a mistake).

In order to track callers, we'll create and use a Set and add the phone number of each incoming caller to it. Create a new file called customers.js and add the following:

const customers = new Set()
module.exports = customers

We'll also need a data structure to capture important information about the caller. We'll do this by creating a caller object and passing it to our various routing files as needed. To do this, create another file called caller.js and add the following:

let caller = {}
module.exports = caller

Step 5: Collect and confirm payment amount from caller

Next, we'll

  • create routes to prompt the caller for their desired payment amount
  • handle their DTMF input
  • confirm their DTMF input
  • handle any potential errors

To do this, create a new file called ccAmount.js and add the following:

const express = require('express')
const customers = require('./customers')
const freeclimb = require('./freeclimb')

const host = process.env.HOST

const router = express.Router()

let errCount = 0

router.post('/ccAmountPrompt', (req, res) => {
    const incoming = req.body.from
    let script
    if (customers.has(incoming)) {
        script = 'So how much would you like to pay?' // this prompt is for customers interacting with a prompt for the second time
    } else {
        script =
            'How much would you like to pay? Just key in the payment amount in US Dollars. For example, to make a payment of twenty dollars press two zero. To speak to an agent, press zero.'
    }

    res.status(200).json(
        freeclimb.percl.build(
            freeclimb.percl.getDigits(`${host}/ccAmount`, { 
                prompts: [
                    freeclimb.percl.say(script),
                    freeclimb.percl.say('The maximum amount is 100 dollars.') // limit the amount callers can choose to pay via phone 
                ],
                maxDigits: 3,
                minDigits: 1,
                flushBuffer: true
            })
        )
    )
})

router.post('/ccAmount', (req, res) => {
    const getDigitsResponse = req.body
    const digits = getDigitsResponse.digits
    const price = parseInt(digits)

    if (digits == '0') {
        res.status(200).json(freeclimb.percl.build(freeclimb.percl.redirect(`${host}/transfer`)))
    } else if (price < 100) {
        res.status(200).json(
            freeclimb.percl.build(
                freeclimb.percl.redirect(`${host}/ccAmountConfirmationPrompt?amt=${price}`)
            )
        )
    } else if (errCount >= 3) {
        res.status(200).json(
            freeclimb.percl.build(
                freeclimb.percl.say(
                    'You have exceeded the maximum number of retries allowed. Please wait while we connect you to an operator'
                ),
                freeclimb.percl.redirect(`${host}/transfer`)
            )
        )
    } else {
        errCount++
        res.status(200).json(
            freeclimb.percl.build(
                freeclimb.percl.say('Sorry the number you entered was invalid please try again'),
                freeclimb.percl.redirect(`${host}/ccAmountPrompt`)
            )
        )
    }
})

module.exports = router

To confirm the DTMF input from the caller and route them to based on their confirmation, create a new file called ccAmountConfirmation.js and add the following:

const express = require('express')
const freeclimb = require('./freeclimb')
const caller = require('./caller')

const host = process.env.HOST

const router = express.Router()

let errCount = 0
let retries = 0

router.post('/ccAmountConfirmationPrompt', (req, res) => {
    res.status(200).json(
        freeclimb.percl.build(
            freeclimb.percl.getDigits(`${host}/ccAmountConfirmation?amt=${req.param('amt')}`, {
                prompts: [
                    freeclimb.percl.say(
                        `Just to be sure thats ${req.param(
                            'amt'
                        )} dollars is that correct? Press 1 for yes and 2 for no`
                    )
                ],
                maxDigits: 1,
                minDigits: 1,
                flushBuffer: true
            })
        )
    )
})

router.post('/ccAmountConfirmation', (req, res) => {
    const getDigitsResponse = req.body
    const digits = getDigitsResponse.digits
    const menuOpts = new Map([
        [
            '1',
            {
                script: 'Thank you',
                redirect: `${host}/ccNumberPrompt`
            }
        ],
        [
            '2',
            {
                script: 'Ok',
                redirect: `${host}/ccAmountPrompt`
            }
        ],
        ['0', { script: 'Redirecting you to an operator', redirect: `${host}/transfer` }]
    ])
    if ((!digits || !menuOpts.get(digits)) && errCount < 3) {
        errCount++
        res.status(200).json(
            freeclimb.percl.build(
                freeclimb.percl.say('Error'),
                freeclimb.percl.redirect(
                    `${host}/ccAmountConfirmationPrompt?amt=${req.param('amt')}`
                )
            )
        )
    } else if (errCount >= 3 || retries >= 2) {
        errCount = 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 {
        errCount = 0
        if (digits === '2') {
            retries++ // retries tracked separately from input errors
        } else if (digits === '1') {
            retries = 0
            caller.paymentAmt = req.param('amt')
        }
        res.status(200).json(
            freeclimb.percl.build(
                freeclimb.percl.say(menuOpts.get(digits).script),
                freeclimb.percl.redirect(menuOpts.get(digits).redirect)
            )
        )
    }
})

module.exports = router

Step 6: Collect credit card information from caller

Next we'll need to create several files to collect all the credit card information we will need from the caller in order to properly process the payment. In this step we will collect:

  • credit card number
  • card expiry date
  • card CVV
  • card ZIP code

🚧

Make sure to secure your applications when collecting sensistive user data

Use privacymode to collect sensitive information, such as credit card data or health information, in order to secure customers' data and maintain PCI compliance.

Collect a credit card number

In this step, we'll prompt the caller for a credit card number and verify it. To do this, create a new file called ccNumber.js and add the following:

const express = require('express')
const cardValidator = require('card-validator')
const customers = require('./customers')
const caller = require('./caller')
const freeclimb = require('./freeclimb')

const host = process.env.HOST

const router = express.Router()

let errCount = 0

router.post('/ccNumberPrompt', (req, res) => {
    const incoming = req.body.from
    let script
    if (customers.has(incoming)) {
        script = 'Okay, whats that card number?' // this prompt is for customers interacting with a prompt for the second time
    } else {
        script = 'To make a payment with a credit card, please enter the card number'
    }

    res.status(200).json(
        freeclimb.percl.build(
            freeclimb.percl.getDigits(`${host}/ccNumber`, {
                prompts: [freeclimb.percl.say(script)],
                maxDigits: 19,
                minDigits: 1,
                flushBuffer: true,
                privacyMode: true // privacymode hides important information to maintain PCI compliance and avoid logging sensitive info
            })
        )
    )
})

router.post('/ccNumber', (req, res) => {
    const getDigitsResponse = req.body
    const digits = getDigitsResponse.digits
    const ccValidation = cardValidator.number(digits)

    if (ccValidation.isValid) {
        // ccNumber checked against a 3rd party library using the Luhn algorithm
        caller.CVVType = ccValidation.card.code.size
        caller.ccNum = digits
        res.status(200).json(
            freeclimb.percl.build(freeclimb.percl.redirect(`${host}/ccExpiryPrompt`))
        )
    } else if (digits == '0') {
        res.status(200).json(freeclimb.percl.build(freeclimb.percl.redirect(`${host}/transfer`)))
    } else if (errCount > 3) {
        res.status(200).json(
            freeclimb.percl.build(
                freeclimb.percl.say(
                    'You have exceeded the maximum number of retries allowed, please wait while we connect you to an operator'
                ),
                freeclimb.percl.redirect(`${host}/transfer`)
            )
        )
    } else if (errCount >= 3) {
        res.status(200).json(
            freeclimb.percl.build(
                freeclimb.percl.say(
                    'You have exceeded the maximum number of retries allowed, please wait while we connect you to an operator'
                ),
                freeclimb.percl.redirect(`${host}/transfer`)
            )
        )
    } else {
        errCount++
        res.status(200).json(
            freeclimb.percl.build(
                freeclimb.percl.say('Sorry the number you entered was invalid please try again'),
                freeclimb.percl.redirect(`${host}/ccNumberPrompt`)
            )
        )
    }
})

module.exports = router

Collect credit card expiry date

In this step, we will prompt the user for their credit card expiry date and validate it. To create the routes that will handle this, create a new file called ccExpiry.js and add the following:

const express = require('express')
const customers = require('./customers')
const caller = require('./caller')
const freeclimb = require('./freeclimb')

const host = process.env.HOST

const router = express.Router()

let errCount = 0

router.post('/ccExpiryPrompt', (req, res) => {
    const incoming = req.body.from
    let script
    if (customers.has(incoming)) {
        script = 'Okay, whats the four digit expiration date?' // this prompt is for customers interacting with a prompt for the second time
    } else {
        script =
            'Enter the expiration date using two digits for the month and two digits for the year'
    }

    res.status(200).json(
        freeclimb.percl.build(
            freeclimb.percl.getDigits(`${host}/ccExpiry`, {
                prompts: [freeclimb.percl.say(script)],
                maxDigits: 4,
                minDigits: 1,
                flushBuffer: true,
                privacyMode: true
            })
        )
    )
})

router.post('/ccExpiry', (req, res) => {
    const getDigitsResponse = req.body
    const digits = getDigitsResponse.digits

    if (digits.length == 4 && dateCheck(digits)) {
        caller.ccExp = digits
        res.status(200).json(freeclimb.percl.build(freeclimb.percl.redirect(`${host}/ccCVVPrompt`)))
    } else if (digits == '0') {
        res.status(200).json(freeclimb.percl.build(freeclimb.percl.redirect(`${host}/transfer`)))
    } else if (errCount > 3) {
        res.status(200).json(
            freeclimb.percl.build(
                freeclimb.percl.say(
                    'You have exceeded the maximum number of retries allowed, please wait while we connect you to an operator'
                ),
                freeclimb.percl.redirect(`${host}/transfer`)
            )
        )
    } else if (errCount >= 3) {
        res.status(200).json(
            freeclimb.percl.build(
                freeclimb.percl.say(
                    'You have exceeded the maximum number of retries allowed, please wait while we connect you to an operator'
                ),
                freeclimb.percl.redirect(`${host}/transfer`)
            )
        )
    } else {
        errCount++
        res.status(200).json(
            freeclimb.percl.build(
                freeclimb.percl.say('Sorry the date you entered was invalid please try again'),
                freeclimb.percl.redirect(`${host}/ccExpiryPrompt`)
            )
        )
    }
})

const dateCheck = digits => { // check that numbers entered are a valid date
    const month = 1 + parseInt(digits.substring(0, 2))
    const year = 2000 + parseInt(digits.substring(2))
    const expDate = new Date(year, month)
    const now = new Date()
    return expDate - now >= 0
}

module.exports = router

Collect credit card CVV

Then, we will collect the credit card CVV and check to make sure it follows proper CVV format. To do this, create another file called ccCVV.js and adding the following:

const express = require('express')
const customers = require('./customers')
const caller = require('./caller')
const freeclimb = require('./freeclimb')

const host = process.env.HOST

const router = express.Router()

let errCount = 0

router.post('/ccCVVPrompt', (req, res) => {
    const incoming = req.body.from
    let script
    if (customers.has(incoming) || errCount > 0) {
        script = 'Okay, whats the security code?' // this prompt is for customers interacting with a prompt for the second time
    } else {
        script = 'Almost done. Whats the security code?'
    }

    res.status(200).json(
        freeclimb.percl.build(
            freeclimb.percl.getDigits(`${host}/ccCVV`, {
                prompts: [freeclimb.percl.say(script)],
                maxDigits: 4, // most CVVs are 3 digits, with American Express having 4 digits
                minDigits: 1,
                flushBuffer: true,
                privacyMode: true
            })
        )
    )
})

router.post('/ccCVV', (req, res) => {
    const getDigitsResponse = req.body
    const digits = getDigitsResponse.digits

    if (digits.length == caller.CVVType) {
        caller.CVV = digits
        res.status(200).json(freeclimb.percl.build(freeclimb.percl.redirect(`${host}/ccZipPrompt`)))
    } else if (digits == '0') {
        res.status(200).json(freeclimb.percl.build(freeclimb.percl.redirect(`${host}/transfer`)))
    } else if (errCount > 3) {
        res.status(200).json(
            freeclimb.percl.build(
                freeclimb.percl.say(
                    'You have exceeded the maximum number of retries allowed, please wait while we connect you to an operator'
                ),
                freeclimb.percl.redirect(`${host}/transfer`)
            )
        )
    } else if (errCount >= 3) {
        res.status(200).json(
            freeclimb.percl.build(
                freeclimb.percl.say(
                    'You have exceeded the maximum number of retries allowed, please wait while we connect you to an operator'
                ),
                freeclimb.percl.redirect(`${host}/transfer`)
            )
        )
    } else {
        errCount++
        res.status(200).json(
            freeclimb.percl.build(
                freeclimb.percl.say(
                    `Sorry im looking for the ${caller.CVVType} digit security code, please enter it now`
                ),
                freeclimb.percl.redirect(`${host}/ccCVVPrompt`)
            )
        )
    }
})

module.exports = router

Collect credit card ZIP code

The last piece of credit card authentication information we need to collect from the user is the ZIP code associated with the card. To create the routes that will prompt for and validate this input, create a new file called ccZip.js and add the following:

const express = require('express')
const customers = require('./customers')
const caller = require('./caller')
const freeclimb = require('./freeclimb')

const host = process.env.HOST

const router = express.Router()

let errCount = 0

router.post('/ccZipPrompt', (req, res) => {
    const incoming = req.body.from
    let script
    if (customers.has(incoming)) {
        script = 'Okay, whats the Zip code?' // this prompt is for customers interacting with a prompt for the second time
    } else {
        script = 'Last thing, Whats the zip code for this card?'
    }

    res.status(200).json(
        freeclimb.percl.build(
            freeclimb.percl.getDigits(`${host}/ccZip`, {
                prompts: [freeclimb.percl.say(script)],
                maxDigits: 5,
                minDigits: 1,
                flushBuffer: true,
                privacyMode: true
            })
        )
    )
})

router.post('/ccZip', (req, res) => {
    const getDigitsResponse = req.body
    const digits = getDigitsResponse.digits

    if (digits.length == 5) {
        caller.Zip = digits
        res.status(200).json(
            freeclimb.percl.build(freeclimb.percl.redirect(`${host}/ccRecapPrompt`))
        )
    } else if (digits == '0') {
        res.status(200).json(freeclimb.percl.build(freeclimb.percl.redirect(`${host}/transfer`)))
    } else if (errCount > 3) {
        res.status(200).json(
            freeclimb.percl.build(
                freeclimb.percl.say(
                    'You have exceeded the maximum number of retries allowed, please wait while we connect you to an operator'
                ),
                freeclimb.percl.redirect(`${host}/transfer`)
            )
        )
    } else if (errCount >= 3) {
        res.status(200).json(
            freeclimb.percl.build(
                freeclimb.percl.say(
                    'You have exceeded the maximum number of retries allowed, please wait while we connect you to an operator'
                ),
                freeclimb.percl.redirect(`${host}/transfer`)
            )
        )
    } else {
        errCount++
        res.status(200).json(
            freeclimb.percl.build(
                freeclimb.percl.say(
                    `Please enter the 5 digit zip code of the billing address for the card you've entered.`
                ),
                freeclimb.percl.redirect(`${host}/ccZipPrompt`)
            )
        )
    }
})

module.exports = router

Recap information received back to caller for verification

Next, we'll create routes that play back information about the payment and collected credit card information—such as the payment amount and last four of the entered credit card number—for the caller to verify.

If the caller finds any issue with the information played back to them, they are able to restart and move through the call flow again starting from when they are prompted to enter the amount they wish to pay.

When they run through the call flow again, the call flow will recognize that they have already tried before thanks to the customer.js and caller.js files we created in Step 3: Handle customer data. Therefore, they will hear shorter, more succinct prompts to move them through the call flow faster.

To add the verification routes, create a new file called ccRecap.js and add the following:

const express = require('express')
const caller = require('./caller')
const customers = require('./customers')
const freeclimb = require('./freeclimb')

const host = process.env.HOST

const router = express.Router()

let errCount = 0
let retries = 0

router.post('/ccRecapPrompt', (req, res) => {
    customers.add(req.from)
    res.status(200).json(
        freeclimb.percl.build(
            freeclimb.percl.getDigits(`${host}/ccRecap`, {
                prompts: [
                    freeclimb.percl.say(
                        `Your payment will be ${
                            caller.paymentAmt
                        } dollars on the card ending in ${caller.ccNum.substring(
                            caller.ccNum.length - 4
                        )}, if thats correct press 1 to confirm if not press 2 to try again`
                    )
                ],
                maxDigits: 1,
                minDigits: 1,
                flushBuffer: true
            })
        )
    )
})

router.post('/ccRecap', (req, res) => {
    const getDigitsResponse = req.body
    const digits = getDigitsResponse.digits
    const menuOpts = new Map([
        [
            '1',
            {
                script: 'Great',
                redirect: `${host}/ccProcess`
            }
        ],
        [
            '2',
            {
                script: 'Sorry. To make sure your information is correct, lets start again',
                redirect: `${host}/ccAmountPrompt` // will return caller back to ccAmount.js to retry the call flow once more
            }
        ],
        ['0', { script: 'Redirecting you to an operator', redirect: `${host}/transfer` }]
    ])
    if ((!digits || !menuOpts.get(digits)) && errCount < 1) {
        errCount++
        res.status(200).json(
            freeclimb.percl.build(
                freeclimb.percl.say('Error'),
                freeclimb.percl.redirect(`${host}/ccRecapPrompt`)
            )
        )
    } else if (errCount >= 1 || retries >= 1) {
        errCount = 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 {
        errCount = 0
        if (digits === '2') {
            retries++ // retries tracked separately from input errors
        } else if (digits === '1') {
            retries = 0
        }
        res.status(200).json(
            freeclimb.percl.build(
                freeclimb.percl.say(menuOpts.get(digits).script),
                freeclimb.percl.redirect(menuOpts.get(digits).redirect)
            )
        )
    }
})

module.exports = router

Step 7: Process the credit card payment

To complete the payment, we'll need to process the credit card. If you were to actually process cards, you would need to integrate your app with the payment processor of your choosing. For testing purposes, we've created a file of test credit card data and used it to simulate payment processing in our ccProcess.js file below.

Add sample credit card data for testing

In order to simulate a payment being sent to a real processor for authentication, we'll create a new file with test credit card numbers. Name this new file testCards.js and add the following:

const testCards = new Map([
    ['4539512795366158', { result: 'success', message: 'Transaction processed successfully' }],
    ['341814945428581', { result: 'success', message: 'Transaction processed successfully' }],
    ['4007000000027', { result: 'failure', message: 'Invalid card number' }],
    ['373543870670161', { result: 'failure', message: 'Invalid card number' }],
    ['5478670693104129', { result: 'failure', message: 'Invalid card number' }],
    ['6011592457501696709', { result: 'failure', message: 'Invalid card Number' }],
    ['4929677130838456', { result: 'failure', message: 'Invalid Expiration Date' }],
    ['6011844071763663', { result: 'failure', message: 'Invalid CVV' }],
    ['375804818469449', { result: 'failure', message: 'Invalid CVV' }],
    ['5113791227910848', { result: 'failure', message: 'Invalid Zip code' }]
])

module.exports = testCards

Process the credit card payment

The processing file we've included in this tutorial below uses sample data from testCards.js to process credit card data and return a message to the user. To process the caller's credit card information, create a file called ccProcess.js and add the the following content:

const express = require('express')
const caller = require('./caller')
const testCards = require('./testCards')
const freeclimb = require('./freeclimb')

const host = process.env.HOST

const router = express.Router()

let retries = 0

router.post('/ccProcess', (req, res) => {
    const testResponse = testCards.get(caller.ccNum)
    if (testResponse.result === 'success') { // from testCards.js
        retries = 0
        res.status(200).json(
            freeclimb.percl.build(freeclimb.percl.redirect(`${host}/ccConfirmationMessage`))
        )
    } else if (retries < 1) {
        retries++
        // console.error(testResponse.message)
        res.status(200).json(
            freeclimb.percl.build(
                freeclimb.percl.say( // although the test credit card data we've supplied provides reasons a card may fail, we don't supply the explicit reason to callers in order to combat fraud
                    "We're having trouble processing your payment, please try again. Remember we Accept Visa, Discover, Mastercard and American Express cards"
                ),
                freeclimb.percl.redirect(`${host}/ccNumberPrompt`)
            )
        )
    } else {
        retries = 0
        // console.error(testResponse.message)
        res.status(200).json(
            freeclimb.percl.build(
                freeclimb.percl.say(
                    'The payment could not be processed at this time please wait while we transfer you to an operator'
                ),
                freeclimb.percl.redirect(`${host}/transfer`)
            )
        )
    }
})
module.exports = router

If you were to actually process cards, you would replace instances of testCards.get with the payment processor of your choosing.


Step 8: Send payment confirmation to caller via SMS

Next, we'll define routes for the final step: sending a payment confirmation message via SMS to the caller. To do this, we'll first generate a random confirmation code for the SMS response we define later. Create a new file called confirmationCode.js and add the following:

exports.generate = () => {
    return Math.floor(100000 + Math.random() * 900000).toString() // create verification code as random 6 digit number represented with a string
}

Now that we're able to generate a random confirmation code from the helper function above, we can define the routes that send the confirmation code to the caller. To do so, create a new file called ccConfirmationMessage.js and add the following:

const express = require('express')
const freeclimb = require('./freeclimb')
const codeGenerator = require('./confirmationCode')
const host = process.env.HOST
const fcNumber = process.env.FC_NUMBER

const router = express.Router()

router.post('/ccConfirmationMessage', (req, res) => {
    const confirmationNumber = codeGenerator.generate()
    res.status(200).json(
        freeclimb.percl.build(
            freeclimb.percl.say(
                `Thank you for your payment, your confirmation number is ${confirmationNumber}, you will receive an sms shortly`
            ),
            freeclimb.percl.sms(
                fcNumber,
                req.body.from,
                `Your confirmation number is ${confirmationNumber}, thank you for using the Node pay-by-phone tutorial`
            ),
            freeclimb.percl.redirect(`${host}/endcall`)
        )
    )
})
module.exports = router

Step 9: Finish and run your app

Lastly, we need to make all of the routes we've just defined accessible from index.js. To do so, replace the current head of your index.js file with the following:

require('dotenv-safe').config()

const bodyParser = require('body-parser')
const express = require('express')
const mainMenuRoutes = require('./mainMenu')
const ccAmtRoutes = require('./ccAmount')
const ccAmountConfirmationRoutes = require('./ccAmountConfirmation')
const ccNumberRoutes = require('./ccNumber')
const ccExpiryRoutes = require('./ccExpiry')
const ccCVVRoutes = require('./ccCVV')
const ccZipRoutes = require('./ccZIP')
const ccRecapRoutes = require('./ccRecap')
const ccProcessRoutes = require('./ccProcess')
const ccConfirmationMessageRoutes = require('./ccConfirmationMessage')
const freeclimb = require('./freeclimb')

const app = express()
const port = process.env.PORT || 3000
const host = process.env.HOST

app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: true }))
app.use('/', mainMenuRoutes)
app.use('/', ccAmtRoutes)
app.use('/', ccAmountConfirmationRoutes)
app.use('/', ccNumberRoutes)
app.use('/', ccExpiryRoutes)
app.use('/', ccCVVRoutes)
app.use('/', ccZipRoutes)
app.use('/', ccRecapRoutes)
app.use('/', ccProcessRoutes)
app.use('/', ccConfirmationMessageRoutes)

Finally, to hear your pay by phone 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 pay by phone application.

🚧

If you are trying to run this sample app while using a trial account, make sure that the number you use to call and test your app is a verified number.

Congrats! You can now build your own PCI compliant pay by phone application. 🥳🥳