Warm Transfer with PHP and Laravel

Warm Transfer

Have you ever been disconnected from a support call while being transferred to someone else?  That couldn't have left a great impression on you...

Warm transfer eliminates this problem - it allows your agents to have the ability to dial in another agent in real time.

Today we'll add warm transfer to a PHP and Laravel application so we can engender warm feelings among customers talking to support.

Here's how it works at a high level:

  1. The first agent becomes available by connecting through the web client.
  2. The second agent becomes available by connecting through the web client.
  3. A customer calls our support line.
  4. The client stays on hold while the first agent joins the call.
  5. While the first agent is on the phone with the client, he or she dials the second agent into the call.
  6. Once the second agent is on the call, the first one can disconnect. The client and the second agent stay on the call.

Let's get started!  Clone the sample application from Github, and click the button below to begin.

Set Up The Voice Web Hook

First, let's configure the voice web-hook for the Twilio number that customers will dial when they want to talk to a support agent. 

Twilio Console for Warm Transfer

This should be the public-facing URL for your app in production.

One option to expose a development URL from your local machine is to use ngrok.  Your URL would then be something like:

 https://<your-ngrok-id>.ngrok.io/conference/connect/client

Loading Code Samples...
Language
<?php

/*
|--------------------------------------------------------------------------
| Application Routes
|--------------------------------------------------------------------------
|
| Here is where you can register all of the routes for an application.
| It's a breeze. Simply tell Laravel the URIs it should respond to
| and give it the controller to call when that URI is requested.
|
*/

Route::get('/', function () {
    return view('welcome');
});

Route::post('/{agent_id}/token',
    ['uses' => 'TokenController@token', 'as' => 'agent-token']
);
Route::post('conference/connect/client',
    ['uses' => 'ConferenceController@connectClient', 'as' => 'conference-connect-client']
);
Route::post('conference/wait',
    ['uses' => 'ConferenceController@wait', 'as' => 'conference-wait']
);
Route::post('conference/connect/{conference_id}/agent1',
    ['uses' => 'ConferenceController@connectAgent1', 'as' => 'conference-connect-agent1']
);
Route::post('conference/connect/{conference_id}/agent2',
    ['uses' => 'ConferenceController@connectAgent2', 'as' => 'conference-connect-agent2']
);
Route::post('conference/{agent_id}/call',
    ['uses' => 'ConferenceController@callAgent2', 'as' => 'conference-call']
);
app/Http/routes.php
Route for our webhook

app/Http/routes.php

Great work!  Webhook in place, we're ready to start looking at the code.

Connect an Agent to a Call

Here you can see all front-end code necessary to connect an agent using Twilio's Voice Web Client.

We need three things to have a live web client:

  • A capability token (provided by our Laravel app)
  • A unique identifier (string) for each agent
  • Event listeners to handle different Twilio-triggered events
Loading Code Samples...
Language
$(function() {
  var currentAgentId;
  var currentConnection;
  var $callStatus = $('#call-status');
  var $connectAgent1Button = $("#connect-agent1-button");
  var $connectAgent2Button = $("#connect-agent2-button");

  var $answerCallButton = $("#answer-call-button");
  var $hangupCallButton = $("#hangup-call-button");
  var $dialAgent2Button = $("#dial-agent2-button");

  $connectAgent1Button.on('click', { agentId: 'agent1' }, agentClickHandler);
  $connectAgent2Button.on('click', { agentId: 'agent2' }, agentClickHandler);
  $hangupCallButton.on('click', hangUp);
  $dialAgent2Button.on('click', dialAgent2);

  function fetchToken(agentId) {
    $.post('/' + agentId + '/token', {}, function(data) {
      currentAgentId = data.agentId;
      connectClient(data.token)
    }, 'json');
  }

  function connectClient(token) {
    Twilio.Device.setup(token);
  }

  Twilio.Device.ready(function (device) {
    updateCallStatus("Ready");
    agentConnectedHandler(device._clientName);
  });

  // Callback for when Twilio Client receives a new incoming call
  Twilio.Device.incoming(function(connection) {
    currentConnection = connection;
    updateCallStatus("Incoming support call");

    // Set a callback to be executed when the connection is accepted
    connection.accept(function() {
      updateCallStatus("In call with customer");
      $answerCallButton.prop('disabled', true);
      $hangupCallButton.prop('disabled', false);
      $dialAgent2Button.prop('disabled', false);
    });

    // Set a callback on the answer button and enable it
    $answerCallButton.click(function() {
      connection.accept();
    });
    $answerCallButton.prop('disabled', false);
  });

  /* Report any errors to the call status display */
  Twilio.Device.error(function (error) {
    updateCallStatus("ERROR: " + error.message);
    disableConnectButtons(false);
  });

  // Callback for when the call finalizes
  Twilio.Device.disconnect(function(connection) {
    callEndedHandler(connection.device._clientName);
  });

  function dialAgent2() {
    $.post('/conference/' + currentAgentId + '/call')
  }

  /* End a call */
  function hangUp() {
    Twilio.Device.disconnectAll();
  }

  function agentClickHandler(e) {
    var agentId = e.data.agentId;
    disableConnectButtons(true);
    fetchToken(agentId);
  }

  function agentConnectedHandler(agentId) {
    $('#connect-agent-row').addClass('hidden');
    $('#connected-agent-row').removeClass('hidden');
    updateCallStatus("Connected as: " + agentId);

    if (agentId === 'agent1') {
      $dialAgent2Button.removeClass('hidden').prop('disabled', true);
    }
    else {
      $dialAgent2Button.addClass('hidden')
    }
  }

  function callEndedHandler(agentId) {
    $dialAgent2Button.prop('disabled', true);
    $hangupCallButton.prop('disabled', true);
    $answerCallButton.prop('disabled', true)
    updateCallStatus("Connected as: " + agentId);
  }

  function disableConnectButtons(disable) {
    $connectAgent1Button.prop('disabled', disable);
    $connectAgent2Button.prop('disabled', disable);
  }

  function updateCallStatus(status) {
    $callStatus.text(status);
  }
});
public/js/main.js
Create a new call from the browser

public/js/main.js

In the next step we'll take a closer look at capability token generation.

Generate a Capability Token

In order to connect the Twilio Voice Web Client we need a capability token.

To allow incoming connections through the web client an identifier must be provided when generating the token.

Loading Code Samples...
Language
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Http\Requests;
use App\Http\Controllers\Controller;
use Twilio\Jwt\ClientToken;

class TokenController extends Controller
{
    public function token($agentId, ClientToken $capability)
    {
        $capability->allowClientIncoming($agentId);

        $token = $capability->generateToken();
        return response()->json(['token' => $token, 'agentId' => $agentId]);
    }
}
app/Http/Controllers/TokenController.php
Generate a new capability token for a given agent identifier

app/Http/Controllers/TokenController.php

Next we'll look at handling incoming calls.

Handle Incoming Calls

For this tutorial we used fixed identifier strings like agent1 and agent2 but you can use any unique string for your call center clients. These identifiers will be used to create outbound calls to the specified agent through the Twilio REST API.

When a client makes a call to our Twilio number ,the application receives a POST request asking for instructions. We use TwiML to instruct the client to join a conference room and use the Twilio REST API client to invite (and start a call with) the first agent.

When providing instructions to the client, we also provide a waitUrl. This URL is another end point of our application and will return more TwiML to SAY welcome to the user and also PLAY some music while on hold.

We use the client's CallSid as the conference identifier. Since all participants need this identifier to join the conference, we'll need to store it in a database so that we can grab it when we dial the second agent into the conference.

Loading Code Samples...
Language
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Http\Requests;
use App\ActiveCall;
use App\Http\Controllers\Controller;
use Twilio\Twiml;
use Twilio\Rest\Client;

class ConferenceController extends Controller
{
    public function wait()
    {
        return $this->generateWaitTwiml();
    }

    public function connectClient(Request $request, Client $client)
    {
        $conferenceId = $request->input('CallSid');
        $twilioNumber = config('services.twilio')['number'];

        $this->createCall('agent1', $conferenceId, $client, $request);

        $activeCall = ActiveCall::firstOrNew(['agent_id' => 'agent1']);
        $activeCall->conference_id = $conferenceId;
        $activeCall->save();

        return $this->generateConferenceTwiml($conferenceId, false, true, '/conference/wait');
    }

    public function connectAgent1($conferenceId)
    {
        return $this->generateConferenceTwiml($conferenceId, true, false);
    }

    public function connectAgent2($conferenceId)
    {
        return $this->generateConferenceTwiml($conferenceId, true, true);
    }

    public function callAgent2($agentId, Request $request, Client $client)
    {
        $twilioNumber = config('services.twilio')['number'];
        $conferenceId = ActiveCall::where('agent_id', $agentId)->first()->conference_id;

        return $this->createCall('agent2', $conferenceId, $client, $request);
    }

    private function createCall($agentId, $conferenceId, $client, $request)
    {
        $destinationNumber = 'client:' . $agentId;
        $twilioNumber = config('services.twilio')['number'];
        $path = str_replace($request->path(), '', $request->url()) . 'conference/connect/' . $conferenceId . '/' . $agentId;
        try {
            $client->calls->create(
                'client:' . $agentId, // The agent_id that will receive the call
                $twilioNumber, // The number of the phone initiating the call
                [
                    'url' => $path // The URL Twilio will request when the call is answered
                ]
            );
        } catch (Exception $e) {
            return 'Error: ' . $e->getMessage();
        }
        return 'ok';
    }

    private function generateConferenceTwiml($conferenceId, $startOnEnter, $endOnExit, $waitUrl = null)
    {
        if ($waitUrl === null){
            $waitUrl = 'http://twimlets.com/holdmusic?Bucket=com.twilio.music.classical';
        }
        $response = new Twiml();
        $dial = $response->dial();
        $dial->conference(
            $conferenceId,
            ['startConferenceOnEnter' => $startOnEnter,
            'endConferenceOnExit' => $endOnExit,
            'waitUrl' => $waitUrl]
        );
        return response($response)->header('Content-Type', 'application/xml');
    }

    private function generateWaitTwiml(){
        $response = new Twiml();
        $response->say(
            'Thank you for calling. Please wait in line for a few seconds. An agent will be with you shortly.',
            ['voice' => 'alice', 'language' => 'en-GB']
        );
        $response->play('http://com.twilio.music.classical.s3.amazonaws.com/BusyStrings.mp3');
        return response($response)->header('Content-Type', 'application/xml');
    }
}
app/Http/Controllers/ConferenceController.php
Create a conference call, put the client on hold, and dial agent #1

app/Http/Controllers/ConferenceController.php

Next let's look at the TwiML response of our application.

Provide TwiML Instruction To The Client

Here we create a TwiMLResponse that will contain a DIAL verb with a CONFERENCE noun that will instruct the JavaScript client to join a specific conference room.

Loading Code Samples...
Language
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Http\Requests;
use App\ActiveCall;
use App\Http\Controllers\Controller;
use Twilio\Twiml;
use Twilio\Rest\Client;

class ConferenceController extends Controller
{
    public function wait()
    {
        return $this->generateWaitTwiml();
    }

    public function connectClient(Request $request, Client $client)
    {
        $conferenceId = $request->input('CallSid');
        $twilioNumber = config('services.twilio')['number'];

        $this->createCall('agent1', $conferenceId, $client, $request);

        $activeCall = ActiveCall::firstOrNew(['agent_id' => 'agent1']);
        $activeCall->conference_id = $conferenceId;
        $activeCall->save();

        return $this->generateConferenceTwiml($conferenceId, false, true, '/conference/wait');
    }

    public function connectAgent1($conferenceId)
    {
        return $this->generateConferenceTwiml($conferenceId, true, false);
    }

    public function connectAgent2($conferenceId)
    {
        return $this->generateConferenceTwiml($conferenceId, true, true);
    }

    public function callAgent2($agentId, Request $request, Client $client)
    {
        $twilioNumber = config('services.twilio')['number'];
        $conferenceId = ActiveCall::where('agent_id', $agentId)->first()->conference_id;

        return $this->createCall('agent2', $conferenceId, $client, $request);
    }

    private function createCall($agentId, $conferenceId, $client, $request)
    {
        $destinationNumber = 'client:' . $agentId;
        $twilioNumber = config('services.twilio')['number'];
        $path = str_replace($request->path(), '', $request->url()) . 'conference/connect/' . $conferenceId . '/' . $agentId;
        try {
            $client->calls->create(
                'client:' . $agentId, // The agent_id that will receive the call
                $twilioNumber, // The number of the phone initiating the call
                [
                    'url' => $path // The URL Twilio will request when the call is answered
                ]
            );
        } catch (Exception $e) {
            return 'Error: ' . $e->getMessage();
        }
        return 'ok';
    }

    private function generateConferenceTwiml($conferenceId, $startOnEnter, $endOnExit, $waitUrl = null)
    {
        if ($waitUrl === null){
            $waitUrl = 'http://twimlets.com/holdmusic?Bucket=com.twilio.music.classical';
        }
        $response = new Twiml();
        $dial = $response->dial();
        $dial->conference(
            $conferenceId,
            ['startConferenceOnEnter' => $startOnEnter,
            'endConferenceOnExit' => $endOnExit,
            'waitUrl' => $waitUrl]
        );
        return response($response)->header('Content-Type', 'application/xml');
    }

    private function generateWaitTwiml(){
        $response = new Twiml();
        $response->say(
            'Thank you for calling. Please wait in line for a few seconds. An agent will be with you shortly.',
            ['voice' => 'alice', 'language' => 'en-GB']
        );
        $response->play('http://com.twilio.music.classical.s3.amazonaws.com/BusyStrings.mp3');
        return response($response)->header('Content-Type', 'application/xml');
    }
}
app/Http/Controllers/ConferenceController.php
Generate TwiML for the client to join a conference call

app/Http/Controllers/ConferenceController.php

Next let's see how to dial the first agent into the call.

Dial The First Agent Into the Call

For our app we created a createCall method to handle dialing our agents. This method uses Twilio's REST API to create a new call. The create method receives the following parameters:

  1. from: Your Twilio number
  2. to : The agent web client's identifier (agent1 or agent2)
  3. url : A URL to ask for TwiML instructions when the call connects

Once the agent answers the call in the web client, a request is made to the callback URL instructing this call to join the conference where the client is already waiting.

Loading Code Samples...
Language
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Http\Requests;
use App\ActiveCall;
use App\Http\Controllers\Controller;
use Twilio\Twiml;
use Twilio\Rest\Client;

class ConferenceController extends Controller
{
    public function wait()
    {
        return $this->generateWaitTwiml();
    }

    public function connectClient(Request $request, Client $client)
    {
        $conferenceId = $request->input('CallSid');
        $twilioNumber = config('services.twilio')['number'];

        $this->createCall('agent1', $conferenceId, $client, $request);

        $activeCall = ActiveCall::firstOrNew(['agent_id' => 'agent1']);
        $activeCall->conference_id = $conferenceId;
        $activeCall->save();

        return $this->generateConferenceTwiml($conferenceId, false, true, '/conference/wait');
    }

    public function connectAgent1($conferenceId)
    {
        return $this->generateConferenceTwiml($conferenceId, true, false);
    }

    public function connectAgent2($conferenceId)
    {
        return $this->generateConferenceTwiml($conferenceId, true, true);
    }

    public function callAgent2($agentId, Request $request, Client $client)
    {
        $twilioNumber = config('services.twilio')['number'];
        $conferenceId = ActiveCall::where('agent_id', $agentId)->first()->conference_id;

        return $this->createCall('agent2', $conferenceId, $client, $request);
    }

    private function createCall($agentId, $conferenceId, $client, $request)
    {
        $destinationNumber = 'client:' . $agentId;
        $twilioNumber = config('services.twilio')['number'];
        $path = str_replace($request->path(), '', $request->url()) . 'conference/connect/' . $conferenceId . '/' . $agentId;
        try {
            $client->calls->create(
                'client:' . $agentId, // The agent_id that will receive the call
                $twilioNumber, // The number of the phone initiating the call
                [
                    'url' => $path // The URL Twilio will request when the call is answered
                ]
            );
        } catch (Exception $e) {
            return 'Error: ' . $e->getMessage();
        }
        return 'ok';
    }

    private function generateConferenceTwiml($conferenceId, $startOnEnter, $endOnExit, $waitUrl = null)
    {
        if ($waitUrl === null){
            $waitUrl = 'http://twimlets.com/holdmusic?Bucket=com.twilio.music.classical';
        }
        $response = new Twiml();
        $dial = $response->dial();
        $dial->conference(
            $conferenceId,
            ['startConferenceOnEnter' => $startOnEnter,
            'endConferenceOnExit' => $endOnExit,
            'waitUrl' => $waitUrl]
        );
        return response($response)->header('Content-Type', 'application/xml');
    }

    private function generateWaitTwiml(){
        $response = new Twiml();
        $response->say(
            'Thank you for calling. Please wait in line for a few seconds. An agent will be with you shortly.',
            ['voice' => 'alice', 'language' => 'en-GB']
        );
        $response->play('http://com.twilio.music.classical.s3.amazonaws.com/BusyStrings.mp3');
        return response($response)->header('Content-Type', 'application/xml');
    }
}
app/Http/Controllers/ConferenceController.php
Call an agent and redirect him or her to the conference call

app/Http/Controllers/ConferenceController.php

Let's look at bringing agent #2 into the call next.

Dial the Second Agent Into the Call

When the client and the first agent are both in the call we are ready to perform a warm transfer to a second agent.

The first agent makes a request passing its agentId.  We look for the conferenceId needed so we can connect the second agent. Since we already have a createCall method, we can simply use that to invite the second agent.

Loading Code Samples...
Language
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Http\Requests;
use App\ActiveCall;
use App\Http\Controllers\Controller;
use Twilio\Twiml;
use Twilio\Rest\Client;

class ConferenceController extends Controller
{
    public function wait()
    {
        return $this->generateWaitTwiml();
    }

    public function connectClient(Request $request, Client $client)
    {
        $conferenceId = $request->input('CallSid');
        $twilioNumber = config('services.twilio')['number'];

        $this->createCall('agent1', $conferenceId, $client, $request);

        $activeCall = ActiveCall::firstOrNew(['agent_id' => 'agent1']);
        $activeCall->conference_id = $conferenceId;
        $activeCall->save();

        return $this->generateConferenceTwiml($conferenceId, false, true, '/conference/wait');
    }

    public function connectAgent1($conferenceId)
    {
        return $this->generateConferenceTwiml($conferenceId, true, false);
    }

    public function connectAgent2($conferenceId)
    {
        return $this->generateConferenceTwiml($conferenceId, true, true);
    }

    public function callAgent2($agentId, Request $request, Client $client)
    {
        $twilioNumber = config('services.twilio')['number'];
        $conferenceId = ActiveCall::where('agent_id', $agentId)->first()->conference_id;

        return $this->createCall('agent2', $conferenceId, $client, $request);
    }

    private function createCall($agentId, $conferenceId, $client, $request)
    {
        $destinationNumber = 'client:' . $agentId;
        $twilioNumber = config('services.twilio')['number'];
        $path = str_replace($request->path(), '', $request->url()) . 'conference/connect/' . $conferenceId . '/' . $agentId;
        try {
            $client->calls->create(
                'client:' . $agentId, // The agent_id that will receive the call
                $twilioNumber, // The number of the phone initiating the call
                [
                    'url' => $path // The URL Twilio will request when the call is answered
                ]
            );
        } catch (Exception $e) {
            return 'Error: ' . $e->getMessage();
        }
        return 'ok';
    }

    private function generateConferenceTwiml($conferenceId, $startOnEnter, $endOnExit, $waitUrl = null)
    {
        if ($waitUrl === null){
            $waitUrl = 'http://twimlets.com/holdmusic?Bucket=com.twilio.music.classical';
        }
        $response = new Twiml();
        $dial = $response->dial();
        $dial->conference(
            $conferenceId,
            ['startConferenceOnEnter' => $startOnEnter,
            'endConferenceOnExit' => $endOnExit,
            'waitUrl' => $waitUrl]
        );
        return response($response)->header('Content-Type', 'application/xml');
    }

    private function generateWaitTwiml(){
        $response = new Twiml();
        $response->say(
            'Thank you for calling. Please wait in line for a few seconds. An agent will be with you shortly.',
            ['voice' => 'alice', 'language' => 'en-GB']
        );
        $response->play('http://com.twilio.music.classical.s3.amazonaws.com/BusyStrings.mp3');
        return response($response)->header('Content-Type', 'application/xml');
    }
}
app/Http/Controllers/ConferenceController.php
Invite a second agent to the conference

app/Http/Controllers/ConferenceController.php

Coming up next: letting the first agent leave the support call so the second can take over.

The First Agent Leaves the Call

When the three participants have joined the same call, the first agent has served his or her purpose. Now agent #1 can drop the call, leaving agent #2 and the client to discuss the weather and important matters of support.

It is important to notice the differences between the TwiML each one of the participants received when joining the call:

  • Both agent one and two have startConferenceOnEnter set to true.
  • For the client calling and for agent two, endConferenceOnExit is set to true.

Translated, this means a conference will start when either agent joins the call.  It also means the client or agent #2 disconnecting will hang up the call.

Loading Code Samples...
Language
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Http\Requests;
use App\ActiveCall;
use App\Http\Controllers\Controller;
use Twilio\Twiml;
use Twilio\Rest\Client;

class ConferenceController extends Controller
{
    public function wait()
    {
        return $this->generateWaitTwiml();
    }

    public function connectClient(Request $request, Client $client)
    {
        $conferenceId = $request->input('CallSid');
        $twilioNumber = config('services.twilio')['number'];

        $this->createCall('agent1', $conferenceId, $client, $request);

        $activeCall = ActiveCall::firstOrNew(['agent_id' => 'agent1']);
        $activeCall->conference_id = $conferenceId;
        $activeCall->save();

        return $this->generateConferenceTwiml($conferenceId, false, true, '/conference/wait');
    }

    public function connectAgent1($conferenceId)
    {
        return $this->generateConferenceTwiml($conferenceId, true, false);
    }

    public function connectAgent2($conferenceId)
    {
        return $this->generateConferenceTwiml($conferenceId, true, true);
    }

    public function callAgent2($agentId, Request $request, Client $client)
    {
        $twilioNumber = config('services.twilio')['number'];
        $conferenceId = ActiveCall::where('agent_id', $agentId)->first()->conference_id;

        return $this->createCall('agent2', $conferenceId, $client, $request);
    }

    private function createCall($agentId, $conferenceId, $client, $request)
    {
        $destinationNumber = 'client:' . $agentId;
        $twilioNumber = config('services.twilio')['number'];
        $path = str_replace($request->path(), '', $request->url()) . 'conference/connect/' . $conferenceId . '/' . $agentId;
        try {
            $client->calls->create(
                'client:' . $agentId, // The agent_id that will receive the call
                $twilioNumber, // The number of the phone initiating the call
                [
                    'url' => $path // The URL Twilio will request when the call is answered
                ]
            );
        } catch (Exception $e) {
            return 'Error: ' . $e->getMessage();
        }
        return 'ok';
    }

    private function generateConferenceTwiml($conferenceId, $startOnEnter, $endOnExit, $waitUrl = null)
    {
        if ($waitUrl === null){
            $waitUrl = 'http://twimlets.com/holdmusic?Bucket=com.twilio.music.classical';
        }
        $response = new Twiml();
        $dial = $response->dial();
        $dial->conference(
            $conferenceId,
            ['startConferenceOnEnter' => $startOnEnter,
            'endConferenceOnExit' => $endOnExit,
            'waitUrl' => $waitUrl]
        );
        return response($response)->header('Content-Type', 'application/xml');
    }

    private function generateWaitTwiml(){
        $response = new Twiml();
        $response->say(
            'Thank you for calling. Please wait in line for a few seconds. An agent will be with you shortly.',
            ['voice' => 'alice', 'language' => 'en-GB']
        );
        $response->play('http://com.twilio.music.classical.s3.amazonaws.com/BusyStrings.mp3');
        return response($response)->header('Content-Type', 'application/xml');
    }
}
app/Http/Controllers/ConferenceController.php
Instruction to dial into a conference call

app/Http/Controllers/ConferenceController.php

And that's a wrap!  Thank you for helping us add warm transfers so our support staff can tag in other agents for help.

Next we'll look at some other awesome features Twilio makes easy to add.

Where to Next?

We love PHP here at Twilio and have a lot of great content on the site.  Alas, we're going to suggest just two other pieces at this point:

Server Notifications

Never miss another server outage. Learn how to build a server notification system that will alert all administrators via SMS when a server outage occurs.

Appointment Reminders

Cut down on the no-shows by implementing automatic appointment reminders for your clients.

Did this help?

Thanks for checking this tutorial out! Let us know on Twitter what you've built... or what you're building.

Mario Celi
Samuel Mendes
Paul Kamp

Need some help?

We all do sometimes; code is hard. Get help now from our support team, or lean on the wisdom of the crowd browsing the Twilio tag on Stack Overflow.

1 / 1
Loading Code Samples...
<?php

/*
|--------------------------------------------------------------------------
| Application Routes
|--------------------------------------------------------------------------
|
| Here is where you can register all of the routes for an application.
| It's a breeze. Simply tell Laravel the URIs it should respond to
| and give it the controller to call when that URI is requested.
|
*/

Route::get('/', function () {
    return view('welcome');
});

Route::post('/{agent_id}/token',
    ['uses' => 'TokenController@token', 'as' => 'agent-token']
);
Route::post('conference/connect/client',
    ['uses' => 'ConferenceController@connectClient', 'as' => 'conference-connect-client']
);
Route::post('conference/wait',
    ['uses' => 'ConferenceController@wait', 'as' => 'conference-wait']
);
Route::post('conference/connect/{conference_id}/agent1',
    ['uses' => 'ConferenceController@connectAgent1', 'as' => 'conference-connect-agent1']
);
Route::post('conference/connect/{conference_id}/agent2',
    ['uses' => 'ConferenceController@connectAgent2', 'as' => 'conference-connect-agent2']
);
Route::post('conference/{agent_id}/call',
    ['uses' => 'ConferenceController@callAgent2', 'as' => 'conference-call']
);
$(function() {
  var currentAgentId;
  var currentConnection;
  var $callStatus = $('#call-status');
  var $connectAgent1Button = $("#connect-agent1-button");
  var $connectAgent2Button = $("#connect-agent2-button");

  var $answerCallButton = $("#answer-call-button");
  var $hangupCallButton = $("#hangup-call-button");
  var $dialAgent2Button = $("#dial-agent2-button");

  $connectAgent1Button.on('click', { agentId: 'agent1' }, agentClickHandler);
  $connectAgent2Button.on('click', { agentId: 'agent2' }, agentClickHandler);
  $hangupCallButton.on('click', hangUp);
  $dialAgent2Button.on('click', dialAgent2);

  function fetchToken(agentId) {
    $.post('/' + agentId + '/token', {}, function(data) {
      currentAgentId = data.agentId;
      connectClient(data.token)
    }, 'json');
  }

  function connectClient(token) {
    Twilio.Device.setup(token);
  }

  Twilio.Device.ready(function (device) {
    updateCallStatus("Ready");
    agentConnectedHandler(device._clientName);
  });

  // Callback for when Twilio Client receives a new incoming call
  Twilio.Device.incoming(function(connection) {
    currentConnection = connection;
    updateCallStatus("Incoming support call");

    // Set a callback to be executed when the connection is accepted
    connection.accept(function() {
      updateCallStatus("In call with customer");
      $answerCallButton.prop('disabled', true);
      $hangupCallButton.prop('disabled', false);
      $dialAgent2Button.prop('disabled', false);
    });

    // Set a callback on the answer button and enable it
    $answerCallButton.click(function() {
      connection.accept();
    });
    $answerCallButton.prop('disabled', false);
  });

  /* Report any errors to the call status display */
  Twilio.Device.error(function (error) {
    updateCallStatus("ERROR: " + error.message);
    disableConnectButtons(false);
  });

  // Callback for when the call finalizes
  Twilio.Device.disconnect(function(connection) {
    callEndedHandler(connection.device._clientName);
  });

  function dialAgent2() {
    $.post('/conference/' + currentAgentId + '/call')
  }

  /* End a call */
  function hangUp() {
    Twilio.Device.disconnectAll();
  }

  function agentClickHandler(e) {
    var agentId = e.data.agentId;
    disableConnectButtons(true);
    fetchToken(agentId);
  }

  function agentConnectedHandler(agentId) {
    $('#connect-agent-row').addClass('hidden');
    $('#connected-agent-row').removeClass('hidden');
    updateCallStatus("Connected as: " + agentId);

    if (agentId === 'agent1') {
      $dialAgent2Button.removeClass('hidden').prop('disabled', true);
    }
    else {
      $dialAgent2Button.addClass('hidden')
    }
  }

  function callEndedHandler(agentId) {
    $dialAgent2Button.prop('disabled', true);
    $hangupCallButton.prop('disabled', true);
    $answerCallButton.prop('disabled', true)
    updateCallStatus("Connected as: " + agentId);
  }

  function disableConnectButtons(disable) {
    $connectAgent1Button.prop('disabled', disable);
    $connectAgent2Button.prop('disabled', disable);
  }

  function updateCallStatus(status) {
    $callStatus.text(status);
  }
});
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Http\Requests;
use App\Http\Controllers\Controller;
use Twilio\Jwt\ClientToken;

class TokenController extends Controller
{
    public function token($agentId, ClientToken $capability)
    {
        $capability->allowClientIncoming($agentId);

        $token = $capability->generateToken();
        return response()->json(['token' => $token, 'agentId' => $agentId]);
    }
}
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Http\Requests;
use App\ActiveCall;
use App\Http\Controllers\Controller;
use Twilio\Twiml;
use Twilio\Rest\Client;

class ConferenceController extends Controller
{
    public function wait()
    {
        return $this->generateWaitTwiml();
    }

    public function connectClient(Request $request, Client $client)
    {
        $conferenceId = $request->input('CallSid');
        $twilioNumber = config('services.twilio')['number'];

        $this->createCall('agent1', $conferenceId, $client, $request);

        $activeCall = ActiveCall::firstOrNew(['agent_id' => 'agent1']);
        $activeCall->conference_id = $conferenceId;
        $activeCall->save();

        return $this->generateConferenceTwiml($conferenceId, false, true, '/conference/wait');
    }

    public function connectAgent1($conferenceId)
    {
        return $this->generateConferenceTwiml($conferenceId, true, false);
    }

    public function connectAgent2($conferenceId)
    {
        return $this->generateConferenceTwiml($conferenceId, true, true);
    }

    public function callAgent2($agentId, Request $request, Client $client)
    {
        $twilioNumber = config('services.twilio')['number'];
        $conferenceId = ActiveCall::where('agent_id', $agentId)->first()->conference_id;

        return $this->createCall('agent2', $conferenceId, $client, $request);
    }

    private function createCall($agentId, $conferenceId, $client, $request)
    {
        $destinationNumber = 'client:' . $agentId;
        $twilioNumber = config('services.twilio')['number'];
        $path = str_replace($request->path(), '', $request->url()) . 'conference/connect/' . $conferenceId . '/' . $agentId;
        try {
            $client->calls->create(
                'client:' . $agentId, // The agent_id that will receive the call
                $twilioNumber, // The number of the phone initiating the call
                [
                    'url' => $path // The URL Twilio will request when the call is answered
                ]
            );
        } catch (Exception $e) {
            return 'Error: ' . $e->getMessage();
        }
        return 'ok';
    }

    private function generateConferenceTwiml($conferenceId, $startOnEnter, $endOnExit, $waitUrl = null)
    {
        if ($waitUrl === null){
            $waitUrl = 'http://twimlets.com/holdmusic?Bucket=com.twilio.music.classical';
        }
        $response = new Twiml();
        $dial = $response->dial();
        $dial->conference(
            $conferenceId,
            ['startConferenceOnEnter' => $startOnEnter,
            'endConferenceOnExit' => $endOnExit,
            'waitUrl' => $waitUrl]
        );
        return response($response)->header('Content-Type', 'application/xml');
    }

    private function generateWaitTwiml(){
        $response = new Twiml();
        $response->say(
            'Thank you for calling. Please wait in line for a few seconds. An agent will be with you shortly.',
            ['voice' => 'alice', 'language' => 'en-GB']
        );
        $response->play('http://com.twilio.music.classical.s3.amazonaws.com/BusyStrings.mp3');
        return response($response)->header('Content-Type', 'application/xml');
    }
}
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Http\Requests;
use App\ActiveCall;
use App\Http\Controllers\Controller;
use Twilio\Twiml;
use Twilio\Rest\Client;

class ConferenceController extends Controller
{
    public function wait()
    {
        return $this->generateWaitTwiml();
    }

    public function connectClient(Request $request, Client $client)
    {
        $conferenceId = $request->input('CallSid');
        $twilioNumber = config('services.twilio')['number'];

        $this->createCall('agent1', $conferenceId, $client, $request);

        $activeCall = ActiveCall::firstOrNew(['agent_id' => 'agent1']);
        $activeCall->conference_id = $conferenceId;
        $activeCall->save();

        return $this->generateConferenceTwiml($conferenceId, false, true, '/conference/wait');
    }

    public function connectAgent1($conferenceId)
    {
        return $this->generateConferenceTwiml($conferenceId, true, false);
    }

    public function connectAgent2($conferenceId)
    {
        return $this->generateConferenceTwiml($conferenceId, true, true);
    }

    public function callAgent2($agentId, Request $request, Client $client)
    {
        $twilioNumber = config('services.twilio')['number'];
        $conferenceId = ActiveCall::where('agent_id', $agentId)->first()->conference_id;

        return $this->createCall('agent2', $conferenceId, $client, $request);
    }

    private function createCall($agentId, $conferenceId, $client, $request)
    {
        $destinationNumber = 'client:' . $agentId;
        $twilioNumber = config('services.twilio')['number'];
        $path = str_replace($request->path(), '', $request->url()) . 'conference/connect/' . $conferenceId . '/' . $agentId;
        try {
            $client->calls->create(
                'client:' . $agentId, // The agent_id that will receive the call
                $twilioNumber, // The number of the phone initiating the call
                [
                    'url' => $path // The URL Twilio will request when the call is answered
                ]
            );
        } catch (Exception $e) {
            return 'Error: ' . $e->getMessage();
        }
        return 'ok';
    }

    private function generateConferenceTwiml($conferenceId, $startOnEnter, $endOnExit, $waitUrl = null)
    {
        if ($waitUrl === null){
            $waitUrl = 'http://twimlets.com/holdmusic?Bucket=com.twilio.music.classical';
        }
        $response = new Twiml();
        $dial = $response->dial();
        $dial->conference(
            $conferenceId,
            ['startConferenceOnEnter' => $startOnEnter,
            'endConferenceOnExit' => $endOnExit,
            'waitUrl' => $waitUrl]
        );
        return response($response)->header('Content-Type', 'application/xml');
    }

    private function generateWaitTwiml(){
        $response = new Twiml();
        $response->say(
            'Thank you for calling. Please wait in line for a few seconds. An agent will be with you shortly.',
            ['voice' => 'alice', 'language' => 'en-GB']
        );
        $response->play('http://com.twilio.music.classical.s3.amazonaws.com/BusyStrings.mp3');
        return response($response)->header('Content-Type', 'application/xml');
    }
}
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Http\Requests;
use App\ActiveCall;
use App\Http\Controllers\Controller;
use Twilio\Twiml;
use Twilio\Rest\Client;

class ConferenceController extends Controller
{
    public function wait()
    {
        return $this->generateWaitTwiml();
    }

    public function connectClient(Request $request, Client $client)
    {
        $conferenceId = $request->input('CallSid');
        $twilioNumber = config('services.twilio')['number'];

        $this->createCall('agent1', $conferenceId, $client, $request);

        $activeCall = ActiveCall::firstOrNew(['agent_id' => 'agent1']);
        $activeCall->conference_id = $conferenceId;
        $activeCall->save();

        return $this->generateConferenceTwiml($conferenceId, false, true, '/conference/wait');
    }

    public function connectAgent1($conferenceId)
    {
        return $this->generateConferenceTwiml($conferenceId, true, false);
    }

    public function connectAgent2($conferenceId)
    {
        return $this->generateConferenceTwiml($conferenceId, true, true);
    }

    public function callAgent2($agentId, Request $request, Client $client)
    {
        $twilioNumber = config('services.twilio')['number'];
        $conferenceId = ActiveCall::where('agent_id', $agentId)->first()->conference_id;

        return $this->createCall('agent2', $conferenceId, $client, $request);
    }

    private function createCall($agentId, $conferenceId, $client, $request)
    {
        $destinationNumber = 'client:' . $agentId;
        $twilioNumber = config('services.twilio')['number'];
        $path = str_replace($request->path(), '', $request->url()) . 'conference/connect/' . $conferenceId . '/' . $agentId;
        try {
            $client->calls->create(
                'client:' . $agentId, // The agent_id that will receive the call
                $twilioNumber, // The number of the phone initiating the call
                [
                    'url' => $path // The URL Twilio will request when the call is answered
                ]
            );
        } catch (Exception $e) {
            return 'Error: ' . $e->getMessage();
        }
        return 'ok';
    }

    private function generateConferenceTwiml($conferenceId, $startOnEnter, $endOnExit, $waitUrl = null)
    {
        if ($waitUrl === null){
            $waitUrl = 'http://twimlets.com/holdmusic?Bucket=com.twilio.music.classical';
        }
        $response = new Twiml();
        $dial = $response->dial();
        $dial->conference(
            $conferenceId,
            ['startConferenceOnEnter' => $startOnEnter,
            'endConferenceOnExit' => $endOnExit,
            'waitUrl' => $waitUrl]
        );
        return response($response)->header('Content-Type', 'application/xml');
    }

    private function generateWaitTwiml(){
        $response = new Twiml();
        $response->say(
            'Thank you for calling. Please wait in line for a few seconds. An agent will be with you shortly.',
            ['voice' => 'alice', 'language' => 'en-GB']
        );
        $response->play('http://com.twilio.music.classical.s3.amazonaws.com/BusyStrings.mp3');
        return response($response)->header('Content-Type', 'application/xml');
    }
}
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Http\Requests;
use App\ActiveCall;
use App\Http\Controllers\Controller;
use Twilio\Twiml;
use Twilio\Rest\Client;

class ConferenceController extends Controller
{
    public function wait()
    {
        return $this->generateWaitTwiml();
    }

    public function connectClient(Request $request, Client $client)
    {
        $conferenceId = $request->input('CallSid');
        $twilioNumber = config('services.twilio')['number'];

        $this->createCall('agent1', $conferenceId, $client, $request);

        $activeCall = ActiveCall::firstOrNew(['agent_id' => 'agent1']);
        $activeCall->conference_id = $conferenceId;
        $activeCall->save();

        return $this->generateConferenceTwiml($conferenceId, false, true, '/conference/wait');
    }

    public function connectAgent1($conferenceId)
    {
        return $this->generateConferenceTwiml($conferenceId, true, false);
    }

    public function connectAgent2($conferenceId)
    {
        return $this->generateConferenceTwiml($conferenceId, true, true);
    }

    public function callAgent2($agentId, Request $request, Client $client)
    {
        $twilioNumber = config('services.twilio')['number'];
        $conferenceId = ActiveCall::where('agent_id', $agentId)->first()->conference_id;

        return $this->createCall('agent2', $conferenceId, $client, $request);
    }

    private function createCall($agentId, $conferenceId, $client, $request)
    {
        $destinationNumber = 'client:' . $agentId;
        $twilioNumber = config('services.twilio')['number'];
        $path = str_replace($request->path(), '', $request->url()) . 'conference/connect/' . $conferenceId . '/' . $agentId;
        try {
            $client->calls->create(
                'client:' . $agentId, // The agent_id that will receive the call
                $twilioNumber, // The number of the phone initiating the call
                [
                    'url' => $path // The URL Twilio will request when the call is answered
                ]
            );
        } catch (Exception $e) {
            return 'Error: ' . $e->getMessage();
        }
        return 'ok';
    }

    private function generateConferenceTwiml($conferenceId, $startOnEnter, $endOnExit, $waitUrl = null)
    {
        if ($waitUrl === null){
            $waitUrl = 'http://twimlets.com/holdmusic?Bucket=com.twilio.music.classical';
        }
        $response = new Twiml();
        $dial = $response->dial();
        $dial->conference(
            $conferenceId,
            ['startConferenceOnEnter' => $startOnEnter,
            'endConferenceOnExit' => $endOnExit,
            'waitUrl' => $waitUrl]
        );
        return response($response)->header('Content-Type', 'application/xml');
    }

    private function generateWaitTwiml(){
        $response = new Twiml();
        $response->say(
            'Thank you for calling. Please wait in line for a few seconds. An agent will be with you shortly.',
            ['voice' => 'alice', 'language' => 'en-GB']
        );
        $response->play('http://com.twilio.music.classical.s3.amazonaws.com/BusyStrings.mp3');
        return response($response)->header('Content-Type', 'application/xml');
    }
}
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Http\Requests;
use App\ActiveCall;
use App\Http\Controllers\Controller;
use Twilio\Twiml;
use Twilio\Rest\Client;

class ConferenceController extends Controller
{
    public function wait()
    {
        return $this->generateWaitTwiml();
    }

    public function connectClient(Request $request, Client $client)
    {
        $conferenceId = $request->input('CallSid');
        $twilioNumber = config('services.twilio')['number'];

        $this->createCall('agent1', $conferenceId, $client, $request);

        $activeCall = ActiveCall::firstOrNew(['agent_id' => 'agent1']);
        $activeCall->conference_id = $conferenceId;
        $activeCall->save();

        return $this->generateConferenceTwiml($conferenceId, false, true, '/conference/wait');
    }

    public function connectAgent1($conferenceId)
    {
        return $this->generateConferenceTwiml($conferenceId, true, false);
    }

    public function connectAgent2($conferenceId)
    {
        return $this->generateConferenceTwiml($conferenceId, true, true);
    }

    public function callAgent2($agentId, Request $request, Client $client)
    {
        $twilioNumber = config('services.twilio')['number'];
        $conferenceId = ActiveCall::where('agent_id', $agentId)->first()->conference_id;

        return $this->createCall('agent2', $conferenceId, $client, $request);
    }

    private function createCall($agentId, $conferenceId, $client, $request)
    {
        $destinationNumber = 'client:' . $agentId;
        $twilioNumber = config('services.twilio')['number'];
        $path = str_replace($request->path(), '', $request->url()) . 'conference/connect/' . $conferenceId . '/' . $agentId;
        try {
            $client->calls->create(
                'client:' . $agentId, // The agent_id that will receive the call
                $twilioNumber, // The number of the phone initiating the call
                [
                    'url' => $path // The URL Twilio will request when the call is answered
                ]
            );
        } catch (Exception $e) {
            return 'Error: ' . $e->getMessage();
        }
        return 'ok';
    }

    private function generateConferenceTwiml($conferenceId, $startOnEnter, $endOnExit, $waitUrl = null)
    {
        if ($waitUrl === null){
            $waitUrl = 'http://twimlets.com/holdmusic?Bucket=com.twilio.music.classical';
        }
        $response = new Twiml();
        $dial = $response->dial();
        $dial->conference(
            $conferenceId,
            ['startConferenceOnEnter' => $startOnEnter,
            'endConferenceOnExit' => $endOnExit,
            'waitUrl' => $waitUrl]
        );
        return response($response)->header('Content-Type', 'application/xml');
    }

    private function generateWaitTwiml(){
        $response = new Twiml();
        $response->say(
            'Thank you for calling. Please wait in line for a few seconds. An agent will be with you shortly.',
            ['voice' => 'alice', 'language' => 'en-GB']
        );
        $response->play('http://com.twilio.music.classical.s3.amazonaws.com/BusyStrings.mp3');
        return response($response)->header('Content-Type', 'application/xml');
    }
}