Building an Interactive Voice Response System with Laravel and Nexmo

Published on by

Building an Interactive Voice Response System with Laravel and Nexmo image
Want to dial your Laravel app, and have it talk back to you? Let’s take a quick look at how to build a really simple IVR (interactive voice response) – a ‘phone menu’ – with Laravel and Nexmo. When it’s done, you’ll be able to pick up your cell phone, dial a phone number, and have your Laravel app control what you hear.

It’s probably easier than you think, let’s get started!

The Setup

First, we’ll get setup with the standard Laravel installation using Composer. I’m putting this together using Laradock, so first I have to start a bash process to work inside one of the containers:

$docker-compose exec --user=laradock workspace bash

That puts me in my project directory, so I’ll create a new Laravel project right there. The workspace container has composer already installed. If you’re not using Laradock, you’ll need to have composer installed on your development system:

$composer create-project laravel/laravel laravel-hotline

It’s pretty vanilla, but you can take a peek at the code at this point. Not much to see there, so let’s add the Nexmo packages:

$composer require nexmo/laravel

And so our hotline can do something interesting, we’ll add two more dependencies. SimplePie so we can parse RSS feeds, and a Twitter client so we can pull in a Tweet.

$composer require simplepie/simplepie
$composer require thujohn/twitter

Once all the libraries are installed, we can add the service providers to our configuration. Put these two lines in the providers section of config/app.php:

'providers' => [
//...
Nexmo\Laravel\NexmoServiceProvider::class,
Thujohn\Twitter\TwitterServiceProvider::class,
],

And we can also add the two Facade aliases in the same file:

'aliases' => [
//...
'Nexmo' => Nexmo\Laravel\Facade\Nexmo::class,
'Twitter' => Thujohn\Twitter\Facades\Twitter::class,
]

The final step is generating and editing the configuration files. To create configuration files for both Nexmo and Twitter, from the root of the project run the command:

$php artisan vendor:publish

Then edit config/nexmo.php and add your Nexmo key and secret. Don’t have one? Signup for free at nexmo.com. You’ll have to add credentials to config/ttwitter.php as well. If you don’t have those, head over to Twitter’s app management page and create a new application.

Instead of editing the configuration files, you can also just add the credentials to your .env file. Take a look at the Nexmo and Twitter client readmes for specifics.

If you want, you can skip the setup, and grab the code at this point.

Creating a Nexmo Application

Just like routing web requests to your application requires a domain name pointing to our server’s IP address, routing phone calls requires some configuration external to our code. Nexmo manages that configuration with an API for ‘Applications’.

An Application is a container for configuration, allowing us to define a set of webhooks, attach a phone number, and store credentials. Webhooks are specific URLs Nexmo can request when something happens – like an incoming phone call. The phone numbers come from Nexmo, and once we have one in our account, we can attach it to the application.

Since creating an application, and attaching a phone number are not tasks that happen frequently, or that should be triggered by a web request (at least for our application), they’re perfect targets for an Artisan command.

The AppCreate Command

We’ll use an existing Artisan command to generate the structure of our new command:

php artisan make:command AppCreate

Once the basic structure is created (find it in /app/Console/Commands/AppCreate.php), we can update the constructor to give it the Nexmo PHP Client we included with composer earlier.

<?php
namespace App\Console\Commands;
use Nexmo\Client;
 
//...
 
protected $client;
 
public function __construct(Client $client)
{
$this->client = $client;
parent::__construct();
}

We also need to update the commands description, and it’s signature. That will make --help useful, and define the arguments we expect the command to be given. The $description is pretty straightforward:

protected $description = 'Create a new Nexmo Application';
protected $signature = 'nexmo:app:create {name} {answer_url} {event_url} {--type=voice} {--answer_method} {--event_method} {--keyfile=nexmo.key}';

The $signature is a little more complex. We start out by defining that nexmo:app:create will be how we call the command. Then we’ll expect an application {name}. After that, we need two webhooks. The webhook to use when there’s a new inbound call is aptly named the answer_url, and the webhook to use for any call related event (like a call hanging up) is the event_url.

Optionally we’ll allow an application --type to be set (but at the moment, the API only supports voice applications), and allow the HTTP methods of the webhooks to be set. By default, the method used for the answer_url is a GET, and the method used by the event_url is a POST.

Finally, when an application is created a private key is sent in the API response. This is only provided on the creation of an application, so it needs to be saved somewhere. --keyfile will let us specify where; however, by default, it will be stored in nexmo.key.

This argument set and order follows that of the same command in the Nexmo CLI tool, which does far more than just create an application. Obviously, that could be used instead of an Artisan command, but this gives us a chance to see how easy it is to create a new Application using the PHP Client.

With our command defined, let’s write the code to handle() the command. The PHP Client allows us to build up an Application fluently, but for the sake of clarity, we’ll create it with a few lines of code. We’ll just create a new Application and pass it a VoiceConfig with the webhooks:

<?php
namespace App\Console\Commands;
use Nexmo\Client;
use Nexmo\Application\Application;
use Nexmo\Application\VoiceConfig;
 
//...
 
public function handle()
{
$application = new Application();
$application->setName($this->argument('name'));
 
$config = new VoiceConfig();
$config->setWebhook(VoiceConfig::ANSWER, $this->argument('answer_url'), $this->option('answer_method'));
$config->setWebhook(VoiceConfig::EVENT, $this->argument('event_url'), $this->option('event_method'));
 
$application->setVoiceConfig($config);
//...

Once the Application is configured, we can finish up the handle() method and make the API. Since the API call could fail (invalid credentials, a bad network connection, etc. ), we’ll wrap the call in a try block. We’ll also inform the user of our progress, and on success provide the Application ID that can be used later to reference the Application.

//...
try {
$this->info('Making API Request');
$application = $this->client->applications()->post($application);
file_put_contents($this->option('keyfile'), $application->getPrivateKey());
$this->info('Key Saved To: ' . realpath($this->option('keyfile')));
$this->info('Created Application: ' . $application->getId());
} catch (\Exception $e) {
$this->error('Request Failed: ' . $e->getMessage());
$this->error($e->getTraceAsString(), \Symfony\Component\Console\Output\OutputInterface::VERBOSITY_DEBUG);
}
} //end of handle()

Should the API request fail, we notify the user and – should they request it – include the stack trace.

With the command done, we need to add it to the $commands array in app/Console/Kernel.php.

protected $commands = [
//...
\App\Console\Commands\AppCreate::class
];

Now all we need to do is run the command and create our application:

$php artisan nexmo:app:create Laravel-Hotline \
http://tjlytle.ngrok.io/nexmo/answer \
http://tjlytle.ngrok.io/nexmo/event
 
Making API Request
Key Saved To: /var/www/nexmo.key
Created Application: e2de8f00-0400-4475-89c9-a9c4fde5f40d

At this point you’ll notice we’re not referencing localhost in the webhooks, even though that’s likely where the application is running. For Nexmo to send the inbound call webhook to the application, the URL needs to be externally accessible. If you’ve not used it before, ngrok is the perfect solution for making a local development environment accessible on the public internet.

The Link App Command

We now have a Nexmo Application with webhooks pointing at our Laravel Hotline, but that Application can not be called without a phone number. Numbers can be acquired using the Nexmo dashboard, through the Nexmo CLI tool, or with an API call.

If you just created a new Nexmo account, there may be a test number in your dashboard. If not, login and grab one.

You can link one of your numbers to your new Application using the Application ID; however, you can also link it with the API, so let’s create another Artisan command to do that:

php artisan make:command LinkApp

Just like the CreateApp command, we’ll update the command’s constructor to inject the Nexmo PHP Client. The $signature is a lot simpler this time, and like CreateApp it follows the same format as its sibling command in the Nexmo CLI tool:

protected $description = 'Link a Number to an Application';
protected $signature = 'nexmo:link:app {number} {app}';

The command expects only two arguments, the phone number to link, and the application it should be linked to. We’ll add a few lines to the handle() method to set up the relationship:

<?php
namespace App\Console\Commands;
use Nexmo\Client;
use Nexmo\Application\Application;
use Nexmo\Numbers\Number;
 
//...
 
public function handle()
{
$application = new Application($this->argument('app'));
$number = new Number($this->argument('number'));
 
$number->setVoiceDestination($application);
//...

As you can see, an Application can be created with an application ID, and it’ll reference the matching Application in the API. The same is true for a Number, if it is instantiated with the phone number as an ID, it will reference the same number in your Nexmo account.

Once we have the two resources, we can configure the number to use the application for any voice calls.

Like CreateApp we’ll wrap the API call in a try block so we can catch any errors and inform the user:

//...
try{
$this->info('Making API Request');
$this->client->numbers()->update($number);
$this->info('Linked Number to Application');
} catch (\Exception $e) {
$this->error('Request Failed: ' . $e->getMessage());
$this->error($e->getTraceAsString(), \Symfony\Component\Console\Output\OutputInterface::VERBOSITY_DEBUG);
}
} //end of handle()

Register the command by adding it to the $commands array alongside AppCreate in app/Console/Kernel.php.

protected $commands = [
//...
\App\Console\Commands\AppCreate::class,
\App\Console\Commands\LinkApp::class
];

Then give it a spin (with your Nexmo number of course):

$php artisan -vvv nexmo:link:app 14045559404 e2de8f00-0400-4475-89c9-a9c4fde5f40d
 
Making API Request
Linked Number to Application

Certianly all of this could have been done with a few commands in the Nexmo CLI tool; however, if you’re building a SaaS-like product automatically provisioning applications and linking numbers is something you’ll want to do inside your application. And it’s easy to do with a few lines of code.

Just reading along? You can see the code at this point as well.

Picking Up The Phone

Before you can unlock your phone and call your application, we need to put some code behind the webhooks we defined.

A Bit About Webhooks

When a call is made to our application’s number, Nexmo will make an HTTP request to /nexmo/answer and expect the HTTP response to be the JSON structure of a valid NCCO (Nexmo call control object) stack. That stack defines what should happen in the call. For example, saying “Welcome to the Laravel Hotline” or capturing the user pressing a number on their phone.

An NCCO can also define what URL should get the next webhook. When we capture a user’s key presses using the input NCCO, we also define the URL where that data is sent – just like an HTML <form>.

The Nexmo Documentation has all you need to know about NCCOs.

The other webhook we defined was /nexmo/event. Nexmo will make an HTTP request to that URL every time a call changes state. For example, an inbound call to our application’s number will result in a webhook when the call is ringing, and another when the call is answered by our application. These webhooks allow us to track call status.

Configuring the Routes

By default, Laravel attaches a CSRF middleware to the normal route (a good thing). By defaul, Nexmo sends event webhooks, as well as any user input to an NCCO, as a POST (also a good thing).

We can remove the CSFR middleware by editing app/Http/Kernel.php, but if we’re building an app that has both a web interface and a voice interface, we shouldn’t do that.

We could also add a URI exception to middleware itself, and since we prefixed all the urls with /nexmo that would only take a line:

protected $except = [
'nexmo/*',
];

But instead, lets create a new middleware group for all Nexmo routes. In app/Providers/RouteServiceProvider.php we’ll add method that looks similar to the existing API mapping method:

protected function mapNexmoRoutes()
{
Route::prefix('nexmo')
->middleware('bindings')
->namespace($this->namespace)
->group(base_path('routes/nexmo.php'));
}

And call that method from map():

public function map()
{
$this->mapApiRoutes();
$this->mapWebRoutes();
$this->mapNexmoRoutes();
}

Now any routes found in routes/nexmo.php won’t have the CSRF middleware (as well as the other web related middleware). And any routes defined in routes/nexmo.php are prefixed by /nexmo, so to handle the /nexmo/answer webhook, we just define a route for /answer.

Let’s create routes/nexmo.php and define a simple route:

<?php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
 
//simple hello world
Route::get('/answer', function (Request $request) {
return [
[
'action' => 'talk',
'text' => 'Welcome to the Laravel Hotline'
]
 
];
});
 
//just log the events so we can inspect the data
Route::post('/event', function (Request $request) {
error_log($request->getContent());
return;
});

As simple as that! Laravel will see we’re returning an array, json_encode the data, and set the correct content/type. Go ahead, give it a call and see for yourself.

As mentioned when we created the application, for Nexmo to make the webhooks to /nexmo/answer and /nexmo/event the application needs to be accessible on the public internet at the domain used when you created the application. If you need to update those web hooks, it’s as easy as an API call or a Nexmo CLI command.

Take this for a spin by checking out the code at this point.

Making A Controller

Putting the NCCO right in routes/nexmo.php is a quick way to see everything in action, but we should build a proper IVR controller to handle our voice requests. Let’s update our route to point to a controller:

Route::get ('/answer', 'IvrController@answer')->name('ivr.answer');
Route::post('/menu', 'IvrController@menu' )->name('ivr.menu');

We’ve added a new /menu route to handle user input after our greeting. We’ve also named the routes so that we can generate URLs for them later. Now let’s use Artisan to create a new controller:

$php artisan make:controller IvrController

We’ll open up /app/Http/Controllers/IvrController.php and add the answer method that will answer inbound calls. This will take the simple NCCO we used before as a greeting, and add two more NCCOs to the stack:

public function answer(Request $request)
{
return [
[
'action' => 'talk',
'text' => 'Welcome to the Laravel Hotline'
],
[
'action' => 'talk',
'text' => "Press 1 to hear Taylor's latest tweet. Press 2 to listen to the latest Laravel Podcast",
'bargeIn' => true
],
[
'action' => 'input',
'eventUrl' => [route('ivr.menu')],
'maxDigits' => 1
]
 
];
}

The first NCCO is exactly what we used before. The second is also a talk, but we’ve added the bargeIn flag which allows the called to press a digit at any time. Since our first talk doesn’t have bargeIn, the text will continue playing even if the user pressed a key.

The last NCCO captures the user input and sends the results to the ivr.menu route (/nexmo/menu) as a POST. Let’s build that simple method (menu() now:

public function menu(Request $request)
{
switch ($request->json('dtmf')){
case '1';
return $this->tweet($request);
case '2':
return $this->podcast($request);
default:
return $this->answer($request);
}
}

This simple switch looks for valid options, and if none are found replays the menu. When the user dials a 1 we return whatever NCCO is created by tweet(), and for 2 we use the NCCO generated by podcast().

Our tweet() method fetches the most recent tweet from @taylorotwell (note the array dereferencing used to get the individual tweet) :

public function tweet()
{
$tweet = \Twitter::getUserTimeline(['screen_name' => 'taylorotwell', 'count' => 1, 'format' => 'array'])[0];
$text = $tweet['text'];
//...

Since there’s a reasonable chance he’s tweeted a link, and having the Nexmo TTS read a t.co shortened URL isn’t all that informative, we can replace the URL with a reference to the domain name (at least that’ll add some context for those that call in):

//...
foreach($tweet['entities']['urls'] as $link){
$domain = parse_url($link['expanded_url'], PHP_URL_HOST);
 
$text = substr($text, 0, $link['indices'][0]) .
'and a link to ' . $domain .
substr($text, $link['indices'][1]);
}
//....

With the tweet made a little more readable, we’ll return a simple talk NCCO:

//...
return [
[
'action' => 'talk',
'text' => \Twitter::ago($tweet['created_at']) . ' he tweeted ' . $text
]
];
} // end of tweet()

Go ahead, try it out – dial your hotline number and press 1.

Now let’s finish up the second option. For that, we’ll grab the RSS feed for the Laravel Podcast and find the latest audio file. To keep configuration simple, we’ll disable the cache:

public function podcast()
{
$rss = new \SimplePie();
$rss->enable_cache(false);
$rss->set_feed_url('https://rss.simplecast.com/podcasts/351/rss');
$rss->init();
 
$item = $rss->get_item(0);
//...

With the RSS feed parsed, and the first item found, we’ll return an NCCO stack with a talk that reads the episode description, and a stream that plays the audio file:

//...
return [
[
'action' => 'talk',
'text' => $item->get_description()
],
[
'action' => 'stream',
'streamUrl' => [$item->get_enclosure(0)->get_link()]
]
];
} //end of podcast()

Now if you dial the hotline, you can listen to the latest Laravel Podcast with just a press of a button.

What else can you do with Nexmo and Laravel? Next time we’ll add a soundboard to our Laravel Hotline app. You’ll be able to call your friends, and during the conversation inject your favorite Laravel audio bits into the conversation.

And of course, you can grab the source of the Laravel Hotline app in its completed form.

Eric L. Barnes photo

Eric is the creator of Laravel News and has been covering Laravel since 2012.

Cube

Laravel Newsletter

Join 40k+ other developers and never miss out on new tips, tutorials, and more.

image
Laravel Forge

Easily create and manage your servers and deploy your Laravel applications in seconds.

Visit Laravel Forge
Laravel Forge logo

Laravel Forge

Easily create and manage your servers and deploy your Laravel applications in seconds.

Laravel Forge
Tinkerwell logo

Tinkerwell

The must-have code runner for Laravel developers. Tinker with AI, autocompletion and instant feedback on local and production environments.

Tinkerwell
No Compromises logo

No Compromises

Joel and Aaron, the two seasoned devs from the No Compromises podcast, are now available to hire for your Laravel project. ⬧ Flat rate of $7500/mo. ⬧ No lengthy sales process. ⬧ No contracts. ⬧ 100% money back guarantee.

No Compromises
Kirschbaum logo

Kirschbaum

Providing innovation and stability to ensure your web application succeeds.

Kirschbaum
Shift logo

Shift

Running an old Laravel version? Instant, automated Laravel upgrades and code modernization to keep your applications fresh.

Shift
Bacancy logo

Bacancy

Supercharge your project with a seasoned Laravel developer with 4-6 years of experience for just $2500/month. Get 160 hours of dedicated expertise & a risk-free 15-day trial. Schedule a call now!

Bacancy
Lucky Media logo

Lucky Media

Bespoke software solutions built for your business. We ♥ Laravel

Lucky Media
Lunar: Laravel E-Commerce logo

Lunar: Laravel E-Commerce

E-Commerce for Laravel. An open-source package that brings the power of modern headless e-commerce functionality to Laravel.

Lunar: Laravel E-Commerce
LaraJobs logo

LaraJobs

The official Laravel job board

LaraJobs
Larafast: Laravel SaaS Starter Kit logo

Larafast: Laravel SaaS Starter Kit

Larafast is a Laravel SaaS Starter Kit with ready-to-go features for Payments, Auth, Admin, Blog, SEO, and beautiful themes. Available with VILT and TALL stacks.

Larafast: Laravel SaaS Starter Kit
SaaSykit: Laravel SaaS Starter Kit logo

SaaSykit: Laravel SaaS Starter Kit

SaaSykit is a Laravel SaaS Starter Kit that comes with all features required to run a modern SaaS. Payments, Beautiful Checkout, Admin Panel, User dashboard, Auth, Ready Components, Stats, Blog, Docs and more.

SaaSykit: Laravel SaaS Starter Kit
Rector logo

Rector

Your partner for seamless Laravel upgrades, cutting costs, and accelerating innovation for successful companies

Rector

The latest

View all →
Sort Elements with the Alpine.js Sort Plugin image

Sort Elements with the Alpine.js Sort Plugin

Read article
Anonymous Event Broadcasting in Laravel 11.5 image

Anonymous Event Broadcasting in Laravel 11.5

Read article
Microsoft Clarity Integration for Laravel image

Microsoft Clarity Integration for Laravel

Read article
Apply Dynamic Filters to Eloquent Models with the Filterable Package image

Apply Dynamic Filters to Eloquent Models with the Filterable Package

Read article
Property Hooks Get Closer to Becoming a Reality in PHP 8.4 image

Property Hooks Get Closer to Becoming a Reality in PHP 8.4

Read article
Asserting Exceptions in Laravel Tests image

Asserting Exceptions in Laravel Tests

Read article