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'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.
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:
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 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 { 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 how-to 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. 🥳🥳
Updated about 2 months ago