How to Create a Voice-Based Reservation System in Laravel with Twilio <Pay>
Time to read: 6 minutes
“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:
Laravel should copy the data from .env.example
into .env
. If it doesn’t, you can run:
Set the credentials for you your preferred database. After you’ve updated your credentials, run the following command in terminal:
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:
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:
Giving the IVR a Home
Now, let's create a single-action controller called IncomingCallController:
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:
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:
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.
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
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:
And then register that in our routes/web.php
file.
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:
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
:
All together, our IncomingCallController
looks like this:
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:
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.
We can register this route in our routes/web.php
file:
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:
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:
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:
- Accept Incoming: Voice Calls
- Configure With: Webhooks, TwiML Bins, Functions, Studio, or Proxy
- A Call Comes In: https://yourlink.ngrok.io (HTTP POST)
- 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
Related Posts
Related Resources
Twilio Docs
From APIs to SDKs to sample apps
API reference documentation, SDKs, helper libraries, quickstarts, and tutorials for your language and platform.
Resource Center
The latest ebooks, industry reports, and webinars
Learn from customer engagement experts to improve your own communication.
Ahoy
Twilio's developer community hub
Best practices, code samples, and inspiration to build communications and digital engagement experiences.