Build a Interactive Voice Response (IVR) application with Africa's Talking, HapiJS, Docker and Heroku
Introduction
Editorial Note: This is a JavaScript version of an earlier article. You can check it out if you're interested in getting started with Africa's Talking's Voice platform with Python. It was really fun writing this, enjoy!
Welcome back! This is a brand new series on creating amazing experiences with just phone calls. No fancy UI. No React. No Angular. Just a voice call. It may seem super simplistic but in reality, everyone makes calls. In light of this, is it possible to create apps that run on phone calls? Yes! That's what we'll be doing today.
We'll be using Flask, a python micro-framework, and deploying our app on Heroku. We don't have a database as of now but in a future series we'll be saving the data from user responses so stay tuned in for that.
What the app will do
We'll be building a simple guessing game that allows peeps to guess how old Africa's Talking is! This is the current user flow:
- A user will call our number and listen to the instructions
- They will dial in their response on the in-call dial pad
- The app will direct them on whether they have chosen the right answer or not
There are some challenges with the app, we aren't able to keep a user in a loop until they get the right answer but we'll cover this in the next series as we look at slightly mode advanced techniques.
Getting Started
To get your phone call set up, we are going to be using the Africa's Talking platform. Africa's Talking provides APIs that tap into telco infrastructure and makes it easier for developers across Africa to integrate services such as SMS, Voice, USSD, Payments and Airtime. There's also a brand new IoT platform that you could checkout too.
To get started, what you need is a programmable number. Africa's Talking provides these numbers at a cost however, for this tutorial, you can get a number for free. All you have to do is tell us what idea you would like to build on the Voice platform and we'll get you a number free of charge! Fill out this form to share your ideas.
How the Africa's Talking Voice API works
The platform provides a pretty simple approach to building our Interactive Voice Application through a REST API. Here's a simple diagram showing how it works:
Project Setup
Let's get started building our app. We'll get back to Africa's Talking in a few moments. In the mean time, we'll be setting up our project with HapiJS. HapiJS is built off the idea that code should be more of configuration than anything else and it will shortly become apparent as we get our project set up. To get started, run the following commands on your terminal:
mkdir hapi-voice
cd hapi-voice
Once done, its time to initialize our NodeJS project with NPM:
npm init -y
npm install @hapi/hapi xmlbuilder
Here, we install two key packages:
@hapi/hapi
- This installs the core HapiJS library which comes with quite a few features. We'll use just the basic ones but you can learn more here.xmlbuilder
- this library allows us to build out our XML without having to write the actual XML which can be a pain. (Check the Python tutorial to see how it works).
Next up, fire up your favorite code editor. I use VS Code for JavaScript. TBH, it's hard to find a more complete editor right now in the market.
Create a new file server.js
that will be our main file. Add in the following code to set up a basic server:
'use strict'
// Import our modules
const Hapi = require('@hapi/hapi')
const xmlbuilder = require('xmlbuilder')
// Initialize our server
const init = async () => {
const server = Hapi.Server({
port: 3000,
host: 'localhost'
})
// Basic route set up
server.route({
method: 'GET',
path: '/',
handler: async (req, h) => {
return h.response('Everything is okay').code(200)
}
})
await server.start()
console.log(`Server is running on: ${server.info.uri}`)
}
// Start our server
init()
With this code, we've set up a server that when loaded up should show you Server running on port: http://localhost:3000
on your terminal.
Building your XML
We now need to define what our user should here once they dial in their answer to our question. How do we do this? This is where the xmlbuilder
library comes in. All it really does is help us write less XML. You can read more about it here.
Lets get to building our XML responses. Right under our plugin imports, write the following code:
// When to say what
const entrySayActionXMl = xmlbuilder
.create('Response')
.ele('GetDigits', { timeout: '30', finishOnKey: '#', callbackURL: '<YOUR_APP_URL>/voice/say' })
.ele(
'Say',
{ voice: 'woman' },
'Hi, welcome to the Africas Talking Freelance Developer Program demo app. We have a little question for you. How old is Africas Talking? Dial in your guess and press hash'
)
.end({ pretty: true })
const successSayActionXMl = xmlbuilder
.create('Response')
.ele(
'Say',
{ voice: 'woman' },
'Awesome! You got it right! Africas Talking has been around for almost a decade!'
)
.end({ pretty: true })
const errorHighSayActionXML = xmlbuilder
.create('Response')
.ele(
'Say',
{ voice: 'woman' },
'Hi, sorry, thats not quite it. Guess a little lower. Call back to try again. Goodbye.'
)
.end({ pretty: true })
const errorLowSayActionXML = xmlbuilder
.create('Response')
.ele(
'Say',
{ voice: 'woman' },
'Hi, sorry, thats not quite it. Guess a little higher. Call back to try again. Goodbye.'
)
.end({ pretty: true })
const errorSayActionXML = xmlbuilder
.create('Response')
.ele(
'Say',
{ voice: 'woman' },
'Hi, sorry, thats not quite it. Something is wrong with the input you provided. Call back to try again. Goodbye.'
)
.end({ pretty: true })
Lets break down how the XML builder works:
- We create a root XML tag:
xmlbuilder.create('Response')
which will look something like this<Response></Response
- We need to create nested elements and we use the
ele
method to do so:.ele('GetDigits', { timeout: '30', finishOnKey: '#', callbackURL: '' })
this outputs something like this:<GetDigits timeout="30" finishOnKey="#" callbackURL="<YOUR_APP_URL>/voice/say">
- We then close a tag using
.end
and we add in something to the method,{ pretty: true }
which makes sure our XML is formatted properly.
You can nest as many elements as you want depending on your XML data structure
Routes and Logic
Since we have our responses already laid out, we'll get to defining some routes and logic that will serve as a flow for your app. There are two key routes we'll be setting up:
"/" POST
- this route will be hit the first time our app is loaded up"/voice/say" POST
- this route is loaded once the user hits the#
key. ThecallbackURL
will be executed at this time since this is the route we'll define as our callback URL
Let's actually write these routes! Just below the first route that we set up, add this code in:
server.route({
method: 'POST',
path: '/',
handler: async (req, h) => {
callAction = entrySayActionXMl
return h.response(callAction)
}
})
server.route({
method: 'POST',
path: '/voice/say',
handler: async (req, h) => {
try {
digits = parseInt(req.payload.dtmfDigits)
// Adding checks and tasks
if (digits == 9) {
return h.response(successSayActionXMl)
} else if (digits < 9) {
return h.response(errorLowSayActionXML)
} else if (digits > 9) {
return h.response(errorHighSayActionXML)
} else {
return h.response(errorSayActionXML)
}
} catch (e) {
h.response(e).code(500)
}
}
})
Let's break down a couple of things around what we've just done:
- The first
index
route runs when the user first makes the call. We then read out our text. Once the user dials in their answer and hits the#
key, we redirect the user using thecallbackURL
link we provide which follows this pattern:<YOUR_APP_URL>/voice/say
. - Once we are redirected to the
/voice/say
route, we check if the user entered the right input and provide feedback based on the answer. We getdtmfDigits
from the Africa's Talking API to make the comparisons.
Once done, we basically have our voice application ready. Next up, we need to prep it for deployment. You can test your endpoints with Postman or, as I found out today courtesy of Hashnode, Postwoman.
Deploying to Heroku with a Docker container
We'll be using Heroku in this case to deploy our app and best of all we'll be using Docker to containerize our application. To get this done, you'll need a Heroku account and Docker installed on your machine. Once you have your accounts created, install the Heroku CLI and run the following commands:
heroku login
heroku container:login
This logs us into the Heroku CLI and the second command gets our Heroku container registry setup. To confirm that you have Docker properly installed, run:
docker --version
You should see something like Docker version 19.03.4, build 9013bf5
. That's what I'm running currently.
Let's get to deploying this awesome app of ours!
Docker and containers
Docker basically ends the "Works on my machine but not in production" problem that so many of us have faced. To make our app a containerized application, we need to create a Dockerfile
. To do this, create a new file on your terminal with:
touch Dockerfile
Open the file and add the following lines:
FROM node:12
#CREATE APP DIRECTORY
WORKDIR /usr/src/app
#Install dependencies
COPY package*.json ./
RUN npm install
#Bundle app source
COPY . .
EXPOSE 8080
CMD ["node","server.js"]
We have essentially finished setting up our container. We just need to run a few commands to get up and running to talk to the Africa's Talking service.
Firstly, we will build our container into an image. We ask docker to build this container, tag it voiceapp
and use the Dockerfile
in this folder.
docker build -t hapivoiceapp .
Secondly, we will create a heroku app.:
heroku create
We now have an project on Heroku that can actually host our application. The Heroku CLI displays it on your terminal. We then set up heroku to take our container for deployment:
heroku container:push web --app <YOUR_HEROKU_APP_NAME>
The container will be published but it's still not live. Next, we make it live with this command:
heroku container:release web --app <YOUR_HEROKU_APP_NAME>
We are now live!!!!
You can view your application on your Heroku dashboard or view logs from the container using:
heroku logs --app <YOUR_HEROKU_APP_NAME> --tail
Remember to add in the link to your
/voice/say
route as soon as you're live
Connecting to the Africa's Talking Voice Dashboard
Now that our app is live, we head to the Africa's Talking dashboard . If you don't have an account, signing up is super easy. Once its all set up, create an team and an app that you would use. We currently are still working on getting the Voice sandbox out hence we'll be heading to a live account immediately. Head over to the Voice tab and you should be able to see the number assigned to you. You won't need an API key for this product.
Using the link that we have from Heroku, we set it as the callback URL. And that's it. Give your number a call and enjoy a nice interactive voice response system right there!
Summary
Today, we built an IVR application with some basic functionality. This is a taste of the power of Africa's Talking APIs. I would love to know what awesome apps you can build using this tech and we'll be giving away numbers to developers and start-ups with great ideas on how to use this tech. You can fill out this form and we'll get back to you ASAP! Can't wait to see what awesome software comes out of this.
Next Steps
Next up, we'll be covering a little more advanced techniques around Voice and USSD. If you have cool apps you've built on Africa's Talking, share it in an article and we'll give you a shout-out! (and some swag too!!! ๐)
Important links
To learn more about Africa's Talking products:
- AT Build Documentation: https://build.at-labs.io/discover
- More on our culture on our blog: https://blog.africastalking.com
- Join our Freelance Developer community: WhatsApp , Facebook
Subscribe to my newsletter
Read articles from Anthony Kiplimo directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Anthony Kiplimo
Anthony Kiplimo
I am a developer, developer relations expert and a user experience designer at Africa's Talking and Elarian. I believe that the next wave of wealth for Africa will be unlocked through the digital economy.