Pay by Phone with Voice Calling

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

In this how-to guide, 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 how-to guide 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 how-to guide",
  "main": "index.js",
  "license": "MIT",
  "dependencies": {
    "@freeclimb/sdk": "^3.9.0",
    "body-parser": "^1.20.3",
    "card-validator": "^10.0.0",
    "dotenv": "^16.4.5",
    "dotenv-safe": "^9.1.0",
    "express": "^4.21.1"
  },
  "devDependencies": {
    "husky": "^8.0.0",
    "jest": "^29.7.0",
    "prettier": "^3.3.3",
    "prettier-standard": "^16.4.1",
    "supertest": "^7.0.0"
  },
  "resolutions": {
    "trim": "0.0.3",
    "postcss": "8.4.31",
    "minimatch": "3.0.5",
    "minimist": "1.2.6",
    "semver": "^7.5.2"
  },
  "scripts": {
    "test": "jest",
    "test:cov": "jest --coverage",
    "start": "DEBUG=express:* node .",
    "format": "prettier-standard --format",
    "lint": "prettier-standard --format --check",
    "prepare": "husky install"
  },
  "packageManager": "[email protected]+sha1.4ba7fc5c6e704fce2066ecbfb0b0d8976fe62447"
}

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 configuration = freeclimbSDK.createConfiguration({ accountId, apiKey })
const freeclimb = new freeclimbSDK.DefaultApi(configuration)

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 { PerclScript, Say, Pause, Redirect, Hangup } = require('@freeclimb/sdk')
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(
        new PerclScript({
            commands: [
                new Say({ text: 'Welcome to the Node Pay By Phone Sample App.' }),
                new Pause({ length: 100 }),
                new Redirect({ actionUrl: `${host}/mainMenuPrompt` })
            ]
        }).build()
    )
})

app.post('/transfer', (req, res) => {
    res.status(200).json(
        new PerclScript({
            commands: [
                new Say({ text: 'there are no operators available at this time' }),
                new Redirect({ actionUrl: `${host}/endCall` })
            ]
        }).build()
    )
})

app.post('/endCall', (req, res) => {
    res.status(200).json(
        new PerclScript({
            commands: [
                new Say({
                    text:
                        'Thank you for calling the Node Pay By Phone sample app , have a nice day!'
                }),
                new Hangup({})
            ]
        }).build()
    )
})

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 { PerclScript, GetDigits, Say, Redirect, Pause } = require('@freeclimb/sdk')

const host = process.env.HOST

const router = express.Router()

let mainMenuErrCount = 0

router.post('/mainMenuPrompt', (req, res) => {
    res.status(200).json(
        new PerclScript({
            commands: [
                new GetDigits({
                    prompts: [
                        new Say({
                            text:
                                ' to make a one time payment please press 1, or press 0 to speak with an operator'
                        })
                    ],
                    actionUrl: `${host}/mainMenu`,
                    maxDigits: 1,
                    minDigits: 1,
                    flushBuffer: true
                })
            ]
        }).build()
    )
})

router.post('/mainMenu', (req, res) => {
    const getDigitsResponse = req.body
    const digits = getDigitsResponse.digits
    const menuOpts = new Map([
        [
            '1',
            {
                redirect: `${host}/ccAmountPrompt`
            }
        ],
        [
            '0',
            {
                redirect: `${host}/transfer`
            }
        ]
    ])
    if ((!digits || !menuOpts.get(digits)) && mainMenuErrCount < 3) {
        mainMenuErrCount++
        res.status(200).json(
            new PerclScript({
                commands: [
                    new Say({ text: 'Error, please try again' }),
                    new Redirect({ actionUrl: `${host}/mainMenuPrompt` })
                ]
            }).build()
        )
    } else if (mainMenuErrCount >= 3) {
        mainMenuErrCount = 0
        res.status(200).json(
            new PerclScript({
                commands: [
                    new Say({ text: 'Max retry limit reached' }),
                    new Pause({ length: 100 }),
                    new Redirect({ actionUrl: `${host}/endCall` })
                ]
            }).build()
        )
    } else {
        mainMenuErrCount = 0
        res.status(200).json(
            new PerclScript({
                commands: [new Redirect({ actionUrl: menuOpts.get(digits).redirect })]
            }).build()
        )
    }
})

module.exports = router

📘

Interested in routing callers via speech input?

Check out our Call Routing Using Voice Input how-to guide 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 how-to guides, this guide also tracks whether or not the caller has already heard certain prompts and will play shorter, more succinct 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 { PerclScript, GetDigits, Say, Redirect } = require('@freeclimb/sdk')

const host = process.env.HOST

const router = express.Router()

let errCount = 0

router.post('/ccAmountPrompt', (req, res) => {
    res.status(200).json(
        new PerclScript({
            commands: [
                new GetDigits({
                    prompts: [
                        new Say({
                            text: customers.has(req.body.from)
                                ? 'so how much would you like to pay'
                                : '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'
                        }),
                        new Say({ text: 'The maximum amount is 100 dollars' })
                    ],
                    actionUrl: `${host}/ccAmount`,
                    maxDigits: 3,
                    minDigits: 1,
                    flushBuffer: true
                })
            ]
        }).build()
    )
})

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

    if (digits == '0') {
        res.status(200).json(
            new PerclScript({
                commands: [new Redirect({ actionUrl: `${host}/transfer` })]
            }).build()
        )
    } else if (price < 100) {
        res.status(200).json(
            new PerclScript({
                commands: [
                    new Redirect({ actionUrl: `${host}/ccAmountConfirmationPrompt?amt=${price}` })
                ]
            }).build()
        )
    } else if (errCount >= 3) {
        res.status(200).json(
            new PerclScript({
                commands: [
                    new Say({
                        text:
                            'You have exceeded the maximum number of retries allowed, please wait while we connect you to an operator'
                    }),
                    new Redirect({ actionUrl: `${host}/transfer` })
                ]
            }).build()
        )
    } else {
        errCount++
        res.status(200).json(
            new PerclScript({
                commands: [
                    new Say({ text: 'Sorry the number you entered was invalid please try again' }),
                    new Redirect({ actionUrl: `${host}/ccAmountPrompt` })
                ]
            }).build()
        )
    }
})

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 { PerclScript, GetDigits, Say, Redirect, Pause } = require('@freeclimb/sdk')

const host = process.env.HOST

const router = express.Router()

let errCount = 0
let retries = 0

router.post('/ccAmountConfirmationPrompt', (req, res) => {
    res.status(200).json(
        new PerclScript({
            commands: [
                new GetDigits({
                    prompts: [
                        new Say({
                            text: `Just to be sure thats ${req.query.amt} dollars is that correct? Press 1 for yes and 2 for no`
                        })
                    ],
                    actionUrl: `${host}/ccAmountConfirmation?amt=${req.query.amt}`,
                    maxDigits: 1,
                    minDigits: 1,
                    flushBuffer: true
                })
            ]
        }).build()
    )
})

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(
            new PerclScript({
                commands: [
                    new Say({ text: 'Error' }),
                    new Redirect({
                        actionUrl: `${host}/ccAmountConfirmationPrompt?amt=${req.query.amt}`
                    })
                ]
            }).build()
        )
    } else if (errCount >= 3 || retries >= 2) {
        errCount = 0
        res.status(200).json(
            new PerclScript({
                commands: [
                    new Say({ text: 'Please wait while we connect you to an operator' }),
                    new Pause({ length: 100 }),
                    new Redirect({ actionUrl: `${host}/transfer` })
                ]
            }).build()
        )
    } else {
        errCount = 0
        if (digits === '2') {
            retries++ // retries tracked separately from input errors
        } else if (digits === '1') {
            retries = 0
            caller.paymentAmt = req.query.amt
        }
        res.status(200).json(
            new PerclScript({
                commands: [
                    new Say({ text: menuOpts.get(digits).script }),
                    new Redirect({ actionUrl: menuOpts.get(digits).redirect })
                ]
            }).build()
        )
    }
})

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 { PerclScript, GetDigits, Say, Redirect } = require('@freeclimb/sdk')

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'
    } else {
        script = 'To make a payment with a credit card please enter the card number'
    }

    res.status(200).json(
        new PerclScript({
            commands: [
                new GetDigits({
                    prompts: [new Say({ text: script })],
                    actionUrl: `${host}/ccNumber`,
                    maxDigits: 19,
                    minDigits: 1,
                    flushBuffer: true,
                    privacyMode: true // privacyMode hides important information to maintain pci compliance, avoid logging sensitive info
                })
            ]
        }).build()
    )
})

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(
            new PerclScript({
                commands: [new Redirect({ actionUrl: `${host}/ccExpiryPrompt` })]
            }).build()
        )
    } else if (digits == '0') {
        res.status(200).json(
            new PerclScript({
                commands: [new Redirect({ actionUrl: `${host}/transfer` })]
            }).build()
        )
    } else if (errCount > 3) {
        res.status(200).json(
            new PerclScript({
                commands: [
                    new Say({
                        text:
                            'You have exceeded the maximum number of retries allowed, please wait while we connect you to an operator'
                    }),
                    new Redirect({ actionUrl: `${host}/transfer` })
                ]
            }).build()
        )
    } else if (errCount >= 3) {
        res.status(200).json(
            new PerclScript({
                commands: [
                    new Say({
                        text:
                            'You have exceeded the maximum number of retries allowed, please wait while we connect you to an operator'
                    }),
                    new Redirect({ actionUrl: `${host}/transfer` })
                ]
            }).build()
        )
    } else {
        errCount++
        res.status(200).json(
            new PerclScript({
                commands: [
                    new Say({ text: 'Sorry the number you entered was invalid please try again' }),
                    new Redirect({ actionUrl: `${host}/ccNumberPrompt` })
                ]
            }).build()
        )
    }
})

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 { PerclScript, GetDigits, Say, Redirect } = require('@freeclimb/sdk')

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'
    } else {
        script =
            'Enter the expiration date using two digits for the month and two digits for the year'
    }

    res.status(200).json(
        new PerclScript({
            commands: [
                new GetDigits({
                    prompts: [new Say({ text: script })],
                    actionUrl: `${host}/ccExpiry`,
                    maxDigits: 4,
                    minDigits: 1,
                    flushBuffer: true,
                    privacyMode: true
                })
            ]
        }).build()
    )
})

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(
            new PerclScript({
                commands: [new Redirect({ actionUrl: `${host}/ccCVVPrompt` })]
            }).build()
        )
    } else if (digits == '0') {
        res.status(200).json(
            new PerclScript({
                commands: [new Redirect({ actionUrl: `${host}/transfer` })]
            }).build()
        )
    } else if (errCount > 3) {
        res.status(200).json(
            new PerclScript({
                commands: [
                    new Say({
                        text:
                            'You have exceeded the maximum number of retries allowed, please wait while we connect you to an operator'
                    }),
                    new Redirect({ actionUrl: `${host}/transfer` })
                ]
            }).build()
        )
    } else if (errCount >= 3) {
        res.status(200).json(
            new PerclScript({
                commands: [
                    new Say({
                        text:
                            'You have exceeded the maximum number of retries allowed, please wait while we connect you to an operator'
                    }),
                    new Redirect({ actionUrl: `${host}/transfer` })
                ]
            }).build()
        )
    } else {
        errCount++
        res.status(200).json(
            new PerclScript({
                commands: [
                    new Say({ text: 'Sorry the date you entered was invalid please try again' }),
                    new Redirect({ actionUrl: `${host}/ccExpiryPrompt` })
                ]
            }).build()
        )
    }
})

const dateCheck = digits => {
    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 { PerclScript, GetDigits, Say, Redirect } = require('@freeclimb/sdk')

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'
    } else {
        script = 'Almost done, Whats the security code'
    }

    res.status(200).json(
        new PerclScript({
            commands: [
                new GetDigits({
                    prompts: [new Say({ text: script })],
                    actionUrl: `${host}/ccCVV`,
                    maxDigits: 4,
                    minDigits: 1,
                    flushBuffer: true,
                    privacyMode: true
                })
            ]
        }).build()
    )
})

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(
            new PerclScript({
                commands: [new Redirect({ actionUrl: `${host}/ccZipPrompt` })]
            }).build()
        )
    } else if (digits == '0') {
        res.status(200).json(
            new PerclScript({
                commands: [new Redirect({ actionUrl: `${host}/transfer` })]
            }).build()
        )
    } else if (errCount > 3) {
        res.status(200).json(
            new PerclScript({
                commands: [
                    new Say({
                        text:
                            'You have exceeded the maximum number of retries allowed, please wait while we connect you to an operator'
                    }),
                    new Redirect({ actionUrl: `${host}/transfer` })
                ]
            }).build()
        )
    } else if (errCount >= 3) {
        res.status(200).json(
            new PerclScript({
                commands: [
                    new Say({
                        text:
                            'You have exceeded the maximum number of retries allowed, please wait while we connect you to an operator'
                    }),
                    new Redirect({ actionUrl: `${host}/transfer` })
                ]
            }).build()
        )
    } else {
        errCount++
        res.status(200).json(
            new PerclScript({
                commands: [
                    new Say({
                        text: `Sorry im looking for the ${caller.CVVType} digit security code, please enter it now`
                    }),
                    new Redirect({ actionUrl: `${host}/ccCVVPrompt` })
                ]
            }).build()
        )
    }
})

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 { PerclScript, GetDigits, Say, Redirect } = require('@freeclimb/sdk')

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'
    } else {
        script = 'Last thing, Whats the zip code for this card?'
    }

    res.status(200).json(
        new PerclScript({
            commands: [
                new GetDigits({
                    prompts: [new Say({ text: script })],
                    actionUrl: `${host}/ccZip`,
                    maxDigits: 5,
                    minDigits: 1,
                    flushBuffer: true,
                    privacyMode: true
                })
            ]
        }).build()
    )
})

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

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

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 { PerclScript, GetDigits, Say, Redirect, Pause } = require('@freeclimb/sdk')

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(
        new PerclScript({
            commands: [
                new GetDigits({
                    prompts: [
                        new Say({
                            text: `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`
                        })
                    ],
                    actionUrl: `${host}/ccRecap`,
                    maxDigits: 1,
                    minDigits: 1,
                    flushBuffer: true
                })
            ]
        }).build()
    )
})

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`
            }
        ],
        ['0', { script: 'Redirecting you to an operator', redirect: `${host}/transfer` }]
    ])
    if ((!digits || !menuOpts.get(digits)) && errCount < 1) {
        errCount++
        res.status(200).json(
            new PerclScript({
                commands: [
                    new Say({ text: 'Error' }),
                    new Redirect({ actionUrl: `${host}/ccRecapPrompt` })
                ]
            }).build()
        )
    } else if (errCount >= 1 || retries >= 1) {
        errCount = 0
        res.status(200).json(
            new PerclScript({
                commands: [
                    new Say({ text: 'Please wait while we connect you to an operator' }),
                    new Pause({ length: 100 }),
                    new Redirect({ actionUrl: `${host}/transfer` })
                ]
            }).build()
        )
    } else {
        errCount = 0
        if (digits === '2') {
            retries++ // retries tracked separately from input errors
        } else if (digits === '1') {
            retries = 0
        }
        res.status(200).json(
            new PerclScript({
                commands: [
                    new Say({ text: menuOpts.get(digits).script }),
                    new Redirect({ actionUrl: menuOpts.get(digits).redirect })
                ]
            }).build()
        )
    }
})

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 guide 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 { PerclScript, Redirect, Say } = require('@freeclimb/sdk')

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') {
        retries = 0
        res.status(200).json(
            new PerclScript({
                commands: [new Redirect({ actionUrl: `${host}/ccConfirmationMessage` })]
            }).build()
        )
    } else if (retries < 1) {
        retries++
        res.status(200).json(
            new PerclScript({
                commands: [
                    new Say({
                        text:
                            "We're having trouble processing your payment, please try again. Remember we Accept Visa, Discover, Mastercard and American Express cards"
                    }),
                    new Redirect({ actionUrl: `${host}/ccNumberPrompt` })
                ]
            }).build()
        )
    } else {
        retries = 0
        res.status(200).json(
            new PerclScript({
                commands: [
                    new Say({
                        text:
                            'The payment could not be processed at this time please wait while we transfer you to an operator'
                    }),
                    new Redirect({ actionUrl: `${host}/transfer` })
                ]
            }).build()
        )
    }
})
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 codeGenerator = require('./confirmationCode')
const { PerclScript, Say, Sms, Redirect } = require('@freeclimb/sdk')
const host = process.env.HOST

const router = express.Router()

router.post('/ccConfirmationMessage', (req, res) => {
    const confirmationNumber = codeGenerator.generate()
    res.status(200).json(
        new PerclScript({
            commands: [
                new Say({
                    text: `Thank you for your payment, your confirmation number is ${confirmationNumber}, you will receive an sms shortly`
                }),
                new Sms({
                    from: process.env.FC_NUMBER,
                    to: req.body.from,
                    text: `your confirmation number is ${confirmationNumber}, thank you for using the Node pay-by-phone tutorial`
                }),
                new Redirect({ actionUrl: `${host}/endcall` })
            ]
        }).build()
    )
})
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. 🥳🥳