Build a Phone Tree Menu With the Tall Stack and Twilio's Programmable Voice API

Developer working on a Programmable Voice API
September 04, 2023
Written by
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by
Twilion

Phone trees are a common feature in customer service and can help streamline call routing and improve the overall customer experience. The TALL stack (Tailwind CSS, Alpine.js, Laravel, and Laravel Livewire) is a popular combination of web technologies that can be used to create powerful and responsive web applications.

In this tutorial, we will combine it with Twilio's Programmable Voice API to build a phone tree application.

Prerequisites

  • Prior experience with the TALL stack
  • A free or paid Twilio account. If you are new to Twilio, click here to create a free account
  • Ngrok
  • Composer installed globally
  • PHP 8.2
  • A Twilio phone number
  • A local phone number for the call center agent configuration

Create a fresh Laravel application

Our phone tree application will feature a user-friendly dashboard for customer agents to communicate with callers effectively. Let's begin by creating a new Laravel project and changing into the new project directory using the commands below:

composer create-project laravel/laravel phone_tree_application
cd phone_tree_application

Add Laravel Breeze authentication

Next, we'll implement authentication using Laravel Breeze, by running the following command:

composer require laravel/breeze --dev

You need to configure your application's database by adding the database credentials to your .env file before completing the following steps.

Once the installation is complete, scaffold the authentication with Blade templates using these commands:

php artisan breeze:install blade
php artisan migrate
npm install
npm run dev

Start the application

To access the application, in a new terminal session/tab, start it up with the following command:

php artisan serve

The application will be listening on http://127.0.0.1 on port 8000. Open the application in your browser of choice, where it should resemble the image below.

The default Laravel route displayed in Google Chrome

To verify the authentication functionality, navigate to the respective /login or /register routes and test that the authentication flow works.

Install Laravel Livewire

Next, we'll install Laravel Livewire using the following command, in a new terminal session/tab:

composer require livewire/livewire

To integrate Livewire into your application's layout, you need to add the provided Blade directives to the head tag and just before the closing body tag in resources/views/layouts/app.blade.php. These will include scripts, styles, and font links. Update layouts/app.blade.php to match the code below.

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="csrf-token" content="{{ csrf_token() }}">

    <title>{{ config('app.name', 'Laravel') }}</title>

    <!-- Fonts -->
    <link rel="preconnect" href="https://fonts.bunny.net">
    <link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />

    <!-- Scripts -->
    @vite(['resources/css/app.css', 'resources/js/app.js'])
    <livewire:styles />
</head>

<body class="font-sans antialiased">
    <div class="min-h-screen bg-gray-100">
        @include('layouts.navigation')

        <!-- Page Heading -->
        @if (isset($header))
        <header class="bg-white shadow">
            <div class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
                {{ $header }}
            </div>
        </header>
        @endif

        <!-- Page Content -->
        <main>
            {{ $slot }}
        </main>
    </div>
    <livewire:scripts />
</body>
</html>

Add Twilio's PHP SDK

To interact with Twilio, we need to install the Twilio PHP Helper Library in our application. Execute the following command to do so:

composer require twilio/sdk

Set the required environment variables

To simplify managing the required environment variables, we need to add our Twilio credentials to our .env file. Open the .env and add the following lines:

TWILIO_ACCOUNT_SID=<<your_twilio_account_sid>>
TWILIO_AUTH_TOKEN=<<your_twilio_account_auth_token>>

Make sure to replace the <<your_twilio_account_sid>> and <<your_twilio_account_auth_token>> placeholders with your Twilio Account SID and Auth Token, respectively. You can find these in the dashboard of the Twilio Console.

Update the routing table

Once the installation is complete, add the following routes to the end of routes/web.php (before require __DIR__.'/auth.php';):

Route::any(
    '/call', 
    [PhoneTreeLivewireController::class, 'call']
)->name('phone-tree.call');

Route::any(
    '/process-call', 
    [PhoneTreeLivewireController::class, 'processCall']
)->name('phone-tree.process-call');

Route::any(
    '/speak-to-agent', 
    [PhoneTreeLivewireController::class, 'speakToAgent']
)->name('phone-tree.speak-to-agent');

Route::any(
    '/agent-accept-call', 
    [PhoneTreeLivewireController::class, 'agentAcceptCall']
)->name('phone-tree.agent-accept-call');

Then, add the following to the use statements at the top of the file to import the PhoneTreeLivewireController.

use App\Http\Livewire\PhoneTreeLivewireController;

Create the phone tree controller

To handle the logic of our phone tree, create a Livewire controller using the following command:

php artisan make:livewire PhoneTreeLivewireController

The command will generate two files: app/Http/Livewire/PhoneTreeLivewireController.php and resources/views/livewire/phone-tree-livewire-controller.blade.php.

Now, let's modify the contents of PhoneTreeLivewireController.php as follows:

<?php

namespace App\Http\Livewire;

use Twilio\Exceptions\TwimlException;
use Twilio\TwiML\VoiceResponse;
use Twilio\Rest\Client;
use Illuminate\Http\Request;
use Livewire\Component;

class PhoneTreeLivewireController extends Component
{
    public function call()
    {
        $response = new VoiceResponse();
        $gather = $response->gather([
            'numDigits' => 1,
            'action' => route('phone-tree.process-call'),
            'method' => 'GET'
        ]);
        $gather->say('Thanks for calling. This is a phone tree application for educational purposes. Press 1 for marketing, press 2 for customer care');
        $response->say('We didn\'t receive any input. Goodbye!');
        return response($response)->header('Content-Type', 'text/xml');
    }

    public function processCall(Request $request)
    {
        $response = new VoiceResponse();
        $phoneInput = $request->input('Digits');
        switch ($phoneInput) {
            case 1: return $this->marketing();
            case 2: return $this->customerCare();
            case 0: return back();
        }
        return response($response)->header('Content-Type', 'text/xml');
    }

    public function customerCare()
    {
        $response = new VoiceResponse();
        $gather = $response->gather([
            'numDigits' => 1,
            'action' => route('phone-tree.speak-to-agent'),
            'method' => 'GET'
        ]);
        $gather->say('This is the customer care department. Press 1 to listen to a helpful voice note or press 2 to speak with an agent');
        return response($response)->header('Content-Type', 'text/xml');
    }

    public function marketing()
    {
        $response = new VoiceResponse();
        $response->say("This is the marketing department. Instructions on how to solve your marketing issues will go here");
        return response($response)->header('Content-Type', 'text/xml');
    }

    public function speakToAgent(Request $request)
    {
        $response = new VoiceResponse();
        $phoneInput = $request->input('Digits');
        switch ($phoneInput) {
            case 1:
                return $response->say('Customer is king. This is the most helpful instruction you can get here. You can speak to an agent if you need more help');
            case 2:
                $response->say('Thanks for reaching out. You have been added to a queue. An agent will get to you shortly');
                $response->enqueue('support', ['url' => 'about_to_connect.xml']);
                return response($response)->header('Content-Type', 'text/xml');

            case 0:
                return back();
        }
        return response($response)->header('Content-Type', 'text/xml');
    }

    public function makeCall()
    {
        // make the call to your agent
        $client = new Client(getenv("TWILIO_ACCOUNT_SID"), getenv("TWILIO_AUTH_TOKEN"));
        //agent phone rings
        $call = $client->calls->create(
            '<<call center agent phone number>>',
            '<<Your Twilio phone number>>',
            ['url' => 'https://your-ngrok-url/agent-accept-call']
        );
    }

    public function agentAcceptCall()
    {
        try {
            csrf_token();
            $response = new VoiceResponse();
            $dial = $response->dial();
            $dial->queue('support');
            return $response;
        } catch (TwimlException $e) {
            return $e->getCode();
        }
    }

    public function render()
    {
        return view('livewire.phone-tree-livewire-controller');
    }
}

The modified file contains methods for different call scenarios in the phone tree. These methods handle call processing, menu options, agent interactions, and making calls to agents.

Now, let's dive deeper into the code above to gain a better understanding of each method's functionality:

  • call(): This method is triggered when a call is made to a Twilio phone number. It utilizes the Twilio TwiML Gather verb to greet the caller with a welcome message and provide instructions for navigating through the phone tree. It also captures the caller's input and sends a GET request to the phone-tree.process-call route to process the input.
  • processCall(Request $request): This method corresponds to the phone-tree.process-call route. It receives the call input as a request parameter using $request->input('Digits') and stores it in the $phoneInput variable. The method utilizes a switch case to check the user's input and calls the appropriate function, such as $this->marketing() if the input is 1 or $this->customerCare() if the input is 2.
  • customerCare(): This method is similar to the call() method, mentioned earlier. It uses the TwiML Gather verb to process input and communicate with the caller. It captures the caller's input from phone calls and sends requests to the phone-tree.speak-to-agent route for further processing.
  • speakToAgent(Request $request): This method corresponds to the phone-tree.speak-to-agent route. It accepts input through the $request parameter. Depending on the caller's input, it performs different actions. If the input is 1, it uses TwiML to speak a helpful message to the caller. If the input is 2, the caller requests to speak with a customer care agent and is added to a queue to wait for an agent to call back.
  • makeCall(): This method configures our application to make phone calls using Twilio's Programmable Voice API. We need to provide a Twilio Account SID, Twilio Auth Token, and phone number for the voice call configuration. In this method, we also expose our application to the internet using ngrok. This URL points to the /agent-accept-call route, enabling the agent to call back from the queue.
  • agentAcceptCall(): This method connects the next caller in the support queue with the agent.
  • render(): This Livewire method is responsible for rendering the view associated with the controller.

Add an agent dashboard

Now, let's proceed to build a simple dashboard that allows agents to make calls from the queue. Modify the content of resources/views/livewire/phone-tree-livewire-controller.blade.php as follows:

<div>
    <h1 class="text-4xl font-bold text-gray-800 mb-4">Call Center</h1>
    <hr class="py-4">
    <div class="flex justify-between">
        <button class="p-4 bg-green-500 text-white rounded-md" wire:click="makeCall">Accept call</button>
    </div>
</div>

Here, the button, when clicked, calls the makeCall() method from the Livewire controller which calls from the queue.

Start Ngrok

To expose our application to the internet, we'll use Ngrok. If you're unfamiliar with Ngrok, refer to the Ngrok documentation for more information. To expose our application, run the following command:

ngrok http 8000

This command will make  our application available on the public internet. You should see output in the terminal similar to the example below:

Ngrok running in the terminal, showing the default output

The generated ngrok URL will be used to configure our Twilio phone number and direct incoming calls to our application. 

Now, replace the url value of  ['url' => 'https://your-ngrok-url/agent-accept-call'] in the makeCall() method with the Ngrok generated URL, and add the path /agent-accept-call on the end.

Using the screenshot above, ['url' => 'https://your-ngrok-url/agent-accept-call'] would be replaced with ['url' => 'https://5b5e-169-159-80-211.ngrok-free.app/agent-accept-call'].

Configure your Twilio phone number

The next step is to configure our Twilio phone number to direct incoming calls to our Ngrok URL.

Editing the voice configuration settings of a Twilio phone number in the Twilio Console.

To configure your Twilio phone number, follow these steps:

  1. Open to the Twilio Console and, in the left-hand side navigation menu, navigate to Explore Products > Phone Numbers > Manage > Active Numbers.
  2. Choose the phone number you want to use for your application.
  3. In the Voice Configuration section, change the A Call Comes In dropdown value to Webhook.
  4. Set your ngrok Forwarding URL, followed by /call as the value for the URL field.
  5. Ensure that the HTTP dropdown is set to GET.
  6. Click Save configuration at the bottom of the page.

By configuring your Twilio phone number with the ngrok URL, Twilio will send a GET request to your application's designated route (e.g., /call) whenever an incoming call is received.

Add a CSRF token exception

Before proceeding with testing that the application works, it's important to add an exception to the Laravel CSRF token verification to prevent an HTTP 401 error, which indicates a CSRF token error. To do this, open app/Http/Middleware/VerifyCsrfToken.php and update it to match the following:

<?php

namespace App\Http\Middleware;

use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;

class VerifyCsrfToken extends Middleware
{
    /**
     * The URIs that should be excluded from CSRF verification.
     *
     * @var array
     */
    protected $except = [
        'agent-accept-call/',
    ];
}

By adding 'agent-accept-call/' to the $except array, we are excluding the agent-accept-call route from CSRF token verification. This allows the route to be accessed without requiring a CSRF token. This step is crucial to ensure that the agent-accept-call route functions properly without encountering CSRF token errors during testing.

Test the application

To test the application, we need to authenticate a user and access the application dashboard, and also make a call to our Twilio phone line.

To authenticate a user, navigate to the /register route and create an account, or navigate to the /login route to login, if an account already exists. Once logged in, the user should be redirected to the dashboard where agents can accept incoming calls from the Twilio phone number.

Next, we need to call the Twilio phone number, following the instructions in the phone tree to ensure it works.

Conclusion

In this tutorial, we have successfully developed a phone tree application using Twilio TwiML and Laravel Livewire. To make the application accessible on the internet, we utilized ngrok, which generated a public URL for our Laravel application. This allowed us to receive incoming calls when we called our Twilio phone number.

Using the TwiML Gather verb, our application was able to accept input from callers during the phone calls. Based on the callers' requests, the application processed the inputs accordingly. This functionality enables effective call handling and routing within the phone tree application.

By combining the power of Twilio TwiML, Laravel Livewire, and Ngrok, we have created a robust phone tree application that enhances customer communication and streamlines call management processes. The code is available on GitHub.

Moses Anumadu is a software developer and online educator who loves to write clean, maintainable code. I create technical contents for technical audiences. You can find me here.