How to Create a Voice-Based Reservation System in Laravel with Twilio <Pay>

March 13, 2019
Written by
Nick Parker
Contributor
Opinions expressed by Twilio contributors are their own

ivr-twilio-pay-laravel-cover-photo.png

“Please listen carefully, as our menu options have changed.”

Yikes!

It is rare that I find myself memorizing a company’s IVR options. It is much more common for me to repeat "Representative" into the phone and press the 0 key until something works.

As a small business owner, I find most IVRs to be out of touch with reality. While I want to engage my customers 24/7/365, I don’t want to make their lives difficult. I recently had an experience with an airline call center that allowed me to take control of my reservation without ever speaking to a representative.

As a developer and Twilio fan, I began writing pseudo-code in my head of how I could build a simple call center that would not only give my customers a personalized self-service portal, but would also allow for my employees to focus their time on more involved interactions.

Objectives

Today, we are going to leverage Laravel and the Twilio Platform to:

  • Engage customers with a custom greeting
  • Interact with customers by gathering keypad inputs
  • Learn and implement the <Pay> verb
  • Sprinkle in a few PHP & Laravel tricks to make life simpler

Starting Blocks

Reservation App

For this tutorial, I’ve drafted a basic Laravel application that can be used to manage reservations. This is a sample application that provides a reservation system for us to use. The code you will see today is portable. I implore you to think of how you can integrate this with some of your current solutions, such as an API for your current reservation system.

Dev Environment

For this tutorial, we will be using Laravel Valet on macOS. This tutorial will work with other PHP dev environments provided they meet the Laravel minimum requirements. You will also need a way to connect your app to the Internet. Valet provides the share command as a wrapper for ngrok. If you don't have Valet, ngrok on its own will work.

Clone the Sample Laravel Reservations App

Clone the repository, then run Composer by running the following commands:

$ git clone https://github.com/nickparker39/reservations-app
$ cd reservations-app
~/reservations-app$ composer install

Laravel should copy the data from .env.example into .env. If it doesn’t, you can run:

~/reservations-app$ cp .env.example .env

Set the credentials for you your preferred database. After you’ve updated your credentials, run the following command in terminal:

~/reservations-app$ php artisan key:generate

Now open the database/seeds/DatabaseSeeder.php seed file and replace the default user information with your name and cell phone number.

Run the database migrations to complete the setup:

~/reservations-app$ php artisan migrate --seed

Getting Ready to Accept Calls

We need to be able to interact with the Twilio API in this tutorial. While there is a Laravel-specific Twilio library, we are going to use the Twilio PHP SDK. The laravel-twilio package makes it super easy to scaffold SMS and voice, but the official Twilio PHP SDK gives us easier access to the underlying API.

Install Twilio PHP SDK

Let's bring the SDK into our project by switching to the reservations-app directory and using Composer to install it:

~/reservations-app$ composer require twilio/sdk

Giving the IVR a Home

Now, let's create a single-action controller called IncomingCallController:

~/reservations-app$ php artisan make:controller IncomingCallController -i

The -i flag tells Laravel that we would like this to be an invokable controller.

NOTE: The __invoke() method is a native PHP magic method that allows you to call a class as a function. In the context of Laravel, this is implemented in invokable or single-action controllers. This allows us to create more descriptive controller names, utilize this shorthand, and not have to worry about trying to force a CRUD action that may not fit what we are doing.

Create a Route for Our New Controller

Similar to any other requests coming into our application, we need to tell them where to go. This is accomplished by adding a route to our routes/web.php file. Go ahead and add a line for our new controller:

Route::post('twilio/incoming/call', 'IncomingCallController');

This is where we see our magic method shine. We are able to call a class (IncomingCallController) as a function thanks to the aforementioned __invoke() method.

Disable CSRF for Incoming Calls

By default, Laravel runs CSRF checks on all non-GET routes in our web.php file. Therefore, without a CSRF token, Laravel will block a POST request from Twilio. Thankfully, we can head over to app\Http\Middleware\VerifyCsrfToken.php and add this route to the $except array:

protected $except = [
    'twilio/incoming/call'
];

NOTE: The $except array also allows wildcards. If our app handled multiple Twilio routes, we could simply add twilio/incoming/* to the array to disable CSRF protection on all of these routes.

Handling Incoming Twilio Calls

This is where my wheels really started turning. Once we unlock the power of recognizing a user by their phone number, we can provide an amazing automated experience. But first, we need to figure out who our caller is!

Finding Our Caller

For this tutorial, we are using the E.164 format for phone numbers in our demo app.

In our IncomingCallController, let's search for our user by phone number. Just a reminder: this is going to look up the user that you created in your database seeder earlier in the tutorial. We will need to import our User class to do this. We are also going to import the TwiML VoiceResponse class during this step.

<?php

namespace App\Http\Controllers;

use App\User;
use Illuminate\Http\Request;
use Twilio\TwiML\VoiceResponse;

class IncomingCallController extends Controller
{
    public function __invoke(Request $request)
    {
        $response = new VoiceResponse();
        
        $user = User::whereCellPhone($request['From'])->first();
    }
}

Once we have the user, we can now greet them by name and look up their latest reservation. Add the following code to the __invoke() method in app\Http\Controllers\IncomingCallController.php 

$response->say("Hello {$user->name}. Thank you for calling again.");

$reservation = $user->getLatestReservation();

Once we have located the reservation, we want to give the customer the option to cancel or pay. To do this, we will nest another <Say/> verb under the <Gather/> verb.

We need to give the <Gather/> verb a place to send the digits. Let's create another invokable controller to handle this:

~/reservations-app$ php artisan make:controller ProcessIVRDigitsController

And then register that in our routes/web.php file.

Route::post('twilio/incoming/digits', 'ProcessIVRDigitsController');

We need to exclude this from our CSRF middleware, so let's head back over to app\Http\Middleware\VerifyCsrfToken.php and change the $except array to a wildcard like we discussed earlier:

$except = [
    'twilio/incoming/*'
];

This will allow us to build a few more controllers and register these routes without having to repeat ourselves.

Now, we can collect a response and send it to our new controller. We only have two options, so let's limit this to collect one digit. Add the following code to the end of the __invoke() method in App\Http\Controllers\IncomingCallController.php:

$gather = $response->gather([
    'action' => secure_url('/twilio/incoming/digits')
,
    'numDigits' => 1
]);
$gather->say("It looks like you have an upcoming reservation on {$reservation->starts->format('l')}. To cancel this reservation, press 1. To pay for it, press 2.");

echo $response;

All together, our IncomingCallController looks like this:

<?php

namespace App\Http\Controllers;

use App\User;
use Illuminate\Http\Request;
use Twilio\TwiML\VoiceResponse;

class IncomingCallController extends Controller
{
    public function __invoke(Request $request)
    {
        $response = new VoiceResponse();
        
        $user = User::whereCellPhone($request['From'])->first();

        $reservation = $user->getLatestReservation();

        $response->say("Hello {$user->name}. Thank you for calling again.");

        $gather = $response->gather([
            'action' => secure_url('/twilio/incoming/digits')
,
            'numDigits' => 1
        ]);

        $gather->say("It looks like you have an upcoming reservation on {$reservation->starts->format('l')}. To cancel this reservation, press 1. To pay for it, press 2.");

        echo $response;
    }
}

Inside of our ProcessIVRDigitsController located at app/Http/Controllers/ProcessIVRDigitsController.php, we can check for these digits and perform the correct action based on the digit that was pressed. Add the following code:

<?php

namespace App\Http\Controllers;

use App\User;
use Illuminate\Http\Request;
use Twilio\TwiML\VoiceResponse;

class ProcessIVRDigitsController extends Controller
{
    public function __invoke(Request $request)
    {
        $user = User::whereCellPhone($request['From'])->first();

        $reservation = $user->getLatestReservation();

        $response = new VoiceResponse();

        switch ($request['Digits']) {
            case 1:
                $reservation->delete();
                $response->say('Your reservation has been canceled. Have a great day!');
                break;
            case 2:
                $response->pay([
                    'charge_amount' => $reservation->dollar_amount,
                    'action' => secure_url('/twilio/incoming/payment', $reservation )

                ]);
                break;
            default:
                $response->say("Sorry, {$user->first_name}. You can only press 1 to cancel your reservation, or 2 to pay for your reservation.");
                break;
        }

        echo $response;
    }
}

Twilio allows us to charge an amount using the pay verb. In order to save some time writing extra code, we will let Twilio do this for us.

We have referenced a new controller called ReservationPaymentController. Let's create that now so we can handle the result of the payment.

~/reservations-app$ php artisan make:controller ReservationPaymentController

We can register this route in our routes/web.php file:

Route::post('twilio/incoming/payment/{reservation}', 'ReservationPaymentController@store');

Then, we can write some logic to save the confirmation number and thank the user for their payment. Add the following code to the ReservationPaymentController:

<?php

namespace App\Http\Controllers;

use App\Reservation;
use Illuminate\Http\Request;
use Twilio\TwiML\VoiceResponse;

class ReservationPaymentController extends Controller
{
    public function store(Request $request, Reservation $reservation)
    {
        $response = new VoiceResponse();

        $reservation->update([
            'payment_confirmation' => $request['PaymentConfirmationCode']
        ]);

        $response->say('Thank you for your payment!');

        echo $response;
    }
}

Getting Twilio Ready

There are a few more steps that we need to take behind the scenes before we can turn the lights on in our application.

Head over to your Twilio console and find Programmable Voice > Settings > General. Enable PCI mode by clicking on the button.

Now, go back over to Programmable Voice > Pay Connectors and click on Stripe. Click Install. Give your Connector a name and then connect it to your Stripe account (or create a new one).

Connecting to the Web

The big moment is finally here!

Head back to your console. Because we used Valet for our dev environment, we can easily connect to ngrok, if necessary.

Sharing Using Valet

Simply execute to expose your Laravel app to the Internet:

~/reservations-app$ valet share

Enter your sudo password and wait to be connected! After your connection is active, copy the secure forwarding address (the one with https://).

Direct Twilio to Your App

Now, go back to your Twilio dashboard and purchase a number or choose one of your existing ones. Under Voice & Fax set the following settings:

  1. Accept Incoming: Voice Calls
  2. Configure With: Webhooks, TwiML Bins, Functions, Studio, or Proxy
  3. A Call Comes In: https://yourlink.ngrok.io (HTTP POST)
  4. Save your number.

Showtime

Our database seeder has provided two reservations for us. Let go ahead and cancel one, then pay for another.

Call your number from the number you put in the seeder at the beginning of the tutorial.

You should be greeted by name and asked about your upcoming reservation.

Choose to cancel this one. You should now hear a cancel confirmation.

Call your number back, and choose to pay for this reservation. Use a Stripe test card such as 4242 4242 4242 4242 to test out the payment. You should now hear the system say “thank you for your payment!”

Conclusion

While we covered a lot in this tutorial, there are still a multitude of ways you can engage your customers when it comes to reservations. Think of this tutorial as a starting point. Imagine swapping out the custom Laravel reservation engine for another API. Think about the possibilities of incorporating Laravel notifications for near-instant feedback using e-mail and SMS. Picture using Laravel Cashier to save a payment card from Twilio.

On the code side of things, there's plenty of room to abstract a lot of our code when it comes to dealing with incoming calls.

I hope this sparks some creativity on new ways to engage your customers. I'm always open to feedback, so please feel free to email me or reach me on Twitter!

Nick Parker
Email: nick@anchortech.co
Twitter: @nickpfire
Github: nickparker39