Powering Customer Engagement With Twilio's WhatsApp Integration

December 06, 2024
Written by
Stephen Popoola
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by

Meet Stephen Popoola, a proud Twilio Champion passionate about building innovative solutions using Twilio's powerful communication tools. As part of the Twilio Champions program, he actively shares insights and experiences to inspire and empower fellow developers in the community. Discover how his projects leverage Twilio's capabilities to create impactful applications. Interested in joining the program? Learn more about becoming a Twilio Champion here.

Apart from my main job as an engineering manager for a financial services company based in London, I also volunteer as head of technology for a charity in Kent, in the south east of England. In this post, I share how Twilio’s Messaging API with the Twilio WhatsApp Business API can be used as a powerful tool by businesses to engage with their customers.

Project requirement

I handle all the IT and software solutions for The Chatham Evangelical Church, a charity that I volunteer for. A few years ago, there was a requirement for a solution to broadcast messages to members of the charity to update them on upcoming events and other relevant information. To meet this requirement, I offered to implement an SMS sending feature using Twilio’s Messaging API

This worked well for a while, until it became a struggle to fit the required text into 160 characters, which is the limit for a single SMS. Although most mobile networks now support the ability to concatenate messages enabling longer messages to be sent, this would potentially incur more cost per message. It was also not possible to determine ahead of time what the length of each message would be. I was therefore tasked with looking for an alternative solution.

After researching several options, I chose WhatsApp and Twilio's WhatsApp Business API based on the provided functionality and several other criteria.

Why I chose WhatsApp

With WhatsApp's global reach and Twilio’s robust API, businesses can automate customer interactions, send real-time notifications, and enhance support services. This integration not only streamlines communication but also improves customer satisfaction, making it an invaluable asset for businesses and organisations seeking more efficient outreach.

My recommendation of WhatsApp was based on the following features:

  1. Content Approval: WhatsApp enforces compliance by reviewing and approving business-initiated messages sent through its API. Businesses cannot send messages or notifications to customers unless the content has been approved by WhatsApp, or the message is sent within 24 hours of a customer-initiated conversation. This process ensures high-quality messaging while protecting customers from spam and irrelevant content.
  2. Rich Media Support: Unlike SMS, WhatsApp supports sending images, videos, documents, and location data, enabling more diverse and engaging interactions
  3. End-to-End Encryption: WhatsApp offers end-to-end encryption, between devices. For business communications, platforms such as Twilio’s WhatsApp API ensure that requests from users are secured using secure REST API.
  4. Group Messaging: Businesses can create group chats for team collaboration or customer groups, a feature not available with traditional SMS
  5. Read Receipts: WhatsApp provides read receipts, allowing businesses to know when their messages have been seen

Why I chose Twilio’s WhatsApp API

One of the charity's administrators started sending messages via WhatsApp through his phone, but I advised against this, citing security concerns as well as potential loss of data should anything happen to his device. So, I was now tasked with finding a solution to replace both the direct sending of messages from individual devices and via SMS. This led me to Twilio’s WhatsApp Business API.

Some of the API's key advantages are

  • Scalability: Twilio's infrastructure allows businesses to handle high volumes of WhatsApp messages efficiently
  • API-Driven Communication: The API enables easy integration of WhatsApp messaging into existing business systems and workflows. This was a winner for me, because when I compared my previous code sending SMS and the changes I needed to make to make it compatible with WhatsApp, they were quite negligible
  • Multi-Channel Support: This meant that you could still use Twilio for SMS as well as for WhatsApp
  • Analytics and Insights: Twilio provides detailed analytics on message delivery, engagement rates, and other key metrics

How to set up a WhatsApp Business account

Based on the above, I decided to give Twilio’s WhatsApp Business API a go. Thankfully, Twilio’s documentation is top notch, so it did not take me too long to integrate it. What took longer was the verification needed by WhatsApp which proved a bit challenging.

But, Twilio’s friendly and efficient support team helped with resolving the onboarding issue I had where my WhatsApp Business Account (WABA) was not linked to Twilio during the onboarding process.

The process I had to follow was:

  • If you do not already have a Twilio account, you can go to the Twilio website and sign up for a free account.
  • Verify your phone number. Twilio will send a verification code to your phone number. Enter the code to verify your account. In my case, I wanted a custom number so I bought a Twilio number.
  • Once your number is verified, you need to request access to WhatsApp Business API. This step could take between 24 to 48 hours for Twilio to review your application.
  • While you are waiting for approval, Twilio allows you to set up a Twilio WhatsApp Sandbox. Setting up the Sandbox is easy and straightforward, taking you step-by-step through the process, until you are able to communicate between Twilio and your WhatsApp number.
  • Once Twilio approves your number, the next step is to verify your number with Meta, which is the parent company for WhatsApp. This is another step that could take a while, but Twilio takes you through each step. If all goes well, you should get approved within 24 to 48 hours. If your application is rejected or fails during the Meta verification process, Twilio will notify you of the reason for the rejection. Common causes for failure include incomplete or incorrect business information, or content that doesn’t comply with WhatsApp’s Business Messaging Guidelines. In case of a rejection, you can review the feedback provided and make the necessary corrections. This can add some time to the process, but in most cases, addressing the issues ensures successful approval.

The main requirement was for an announcement broadcast tool that will send messages with the ability to add placeholders to the pre-approved WhatsApp content template.

Application overview

I developed the application using Laravel, a tool that helps simplify web development with PHP. For the frontend, I used VueJs, which is a JavaScript framework for creating interactive user interfaces. To connect both the frontend and backend smoothly, I used InertiaJs, which allows the entire application to feel unified and work seamlessly without needing separate backend and frontend setups.

Prerequisites

In order to follow along with this guide, make sure you have the following installed on your system:

  1. PHP: You will need PHP installed to run the code examples in this tutorial. You can download the latest version from PHP's official website. It contains installation instructions for different operating systems.
  2. Composer: This is a dependency manager for PHP. You can install it by following the instructions at getcomposer.org.
  3. MySQL: MySQL is a database management system that will be used to store data. In this case, you only need it to store user data. It is recommended that you download the 5.7 version from the official MySQL website. Alternatively you can install Docker or Podman to create a MySQL database without the need to install it yourself.
  4. Laravel Herd (optional): For an easier and quicker setup of your PHP environment, I recommend installing Laravel Herd. Herd provides a lightweight, all-in-one PHP development environment, allowing developers to bypass manual configurations and get started right away with a seamless setup for PHP projects.
  5. A Twilio phone number: You will need a Twilio Number configured as a WhatsApp sender. To obtain a Twilio number configured as a WhatsApp sender, you must have access to a verified Meta Business Manager account. This requires a legal business name, a business email address, and supporting documentation to complete Meta's business verification process. You’ll also need to create and link a WhatsApp Business Account (WABA) to your Meta Business Manager. This ensures compliance with WhatsApp’s policies and is a prerequisite for registering a Twilio phone number for WhatsApp messaging. Without these, the setup process cannot proceed.
  6. A Twilio Content SID: This is only needed to create a predetermined template of the message you will be sending. You will create templates with the Content Template Builder. For this tutorial, you will be creating a template for an announcement-only content with content variables that will be added when the code executes.

In your Twilio Console, navigate to Messaging, then click on Content Template Builder. You will need to add a template name, set the template language, and select the content type. Click next to configure the content text, then add the following to the Body field.

Hi Church! Service will take place at {{1}} on Sunday. Start time is {{2}}. Teas and Coffees will be served from {{3}}. See you there!.

The placeholders are added by clicking the Add Variable link at the bottom of the text input, at the position you want the variable inserted. Once you are done, click on the Submit for WhatsApp approval. Once it is approved, you should be able to see a preview of how the message will be sent.

Approved Content for WhatsApp

Set up Laravel

To start building the application, you need to create a new Laravel project by running the commands below. I called the project “whatsapp-twilio-app”.

composer create-project --prefer-dist laravel/laravel whatsapp-twilio-app
cd whatsapp-twilio-app

Next, copy the default .env.example file as .env, so that the default environment variables will be loaded:

cp .env.example .env
php artisan key:generate

You will need a database to ensure that only logged in users can send messages. By default, Laravel provides a database migration and model for the "users" table, so all you need to do is configure the database, as exemplified in the configuration below.

DB_CONNECTION=mysql 
DB_HOST=127.0.0.1 
DB_PORT=3306 
DB_DATABASE=your_database_name 
DB_USERNAME=your_username 
DB_PASSWORD=your_password

I am using MySQL as a database. If you want to use a different database, you can configure this in the config/database.php file. You can use Docker or Podman to start your MySQL database without having to install it first. 

Here's an example of using Podman:

podman run -d \
  --name mysql_server \
  -e MYSQL_DATABASE=your_database_name \
  -e MYSQL_USER=your_username \
  -e MYSQL_PASSWORD=your_password \
  -e MYSQL_ROOT_PASSWORD=your_root_password \
  -p 3306:3306 \
  mysql:latest

Regardless of your database and how you start it, you'll then have to run the database migrations, with the following command.

php artisan migrate

Install Laravel Breeze and Inertia.js

Laravel Breeze is a package that automatically scaffolds all the logic needed for user authentication, thereby saving you the time to implement the logic yourselves. Set it up by running the following commands.

composer require laravel/breeze --dev
php artisan breeze:install vue
php artisan migrate
npm install && npm run dev
composer require twilio/sdk

You installed the Twilio PHP Helper Library because it simplifies the process of integrating Twilio's APIs into the Laravel application. Instead of manually crafting HTTP requests to Twilio's endpoints, the library provides a robust set of pre-built methods for common operations such as sending messages, handling responses, and managing errors.

In your .env file (following the example below), add Twilio API credentials. You will find these in your Twilio Console. See here for how to find your credentials.

TWILIO_SID=<your_twilio_account_sid>
TWILIO_AUTH_TOKEN=<your_twilio_auth_token>
TWILIO_WHATSAPP_NUMBER=whatsapp+<your_approved_whatsapp_number>
TWILIO_CONTENT_SID=<HXXXXXXXX> (This is the ID of the content template you created earlier)

To reference the environment variables above, you need to add them to the project configuration by adding the following in the config/services.php file. Make sure you add it at the bottom before the last closing array bracket.

'twilio' => [
    'sid' => env('TWILIO_SID'),
    'token' => env('TWILIO_AUTH_TOKEN'),
    'whatsapp_from' => env('TWILIO_WHATSAPP_NUMBER'), 
    'content_template_sid' => env('TWILIO_CONTENT_SID'), 
],
php artisan make:controller WhatsAppController

This will create the WhatsAppController.php in the app/Http/Controllers directory. Next, you need to create the logic to show the form that will be used for messaging. You will do this by defining a route in the routes/web.php file, and the method in the controller that will handle the request to the route.

First, in routes/web.php, add the following before the line: require __DIR__.'/auth.php';:

Route::get('/send-message', [WhatsAppController::class, 'showForm'])
    ->name('get.send-message');

At the top of the routes/web.php file, add the following line to ensure you import the WhatsAppController.

use App\Http\Controllers\WhatsAppController;

Now, in the app/Http/Controllers/ WhatsAppController.php file, you will implement the showform() method. Your controller should look like the code below:

<?php

use Inertia\Inertia;
use Twilio\Rest\Client;

class WhatsAppController extends Controller
{
    // Show the form for sending WhatsApp messages
    public function showForm()
    {
        $sid = config('services.twilio.sid');
        $token = config('services.twilio.token');
        $contentSid = config('services.twilio.content_template_sid');
        $twilio = new Client($sid, $token);
        $content = $twilio->content->v1
            ->contents($contentSid)
            ->fetch();
        $bodyContent = (isset($content->types['twilio/text']['body'])) 
            ? $content->types['twilio/text']['body']
            : 'No content available';
            
        return Inertia::render('Message/Send', [
            'content' => $bodyContent,
        ]);
    }
}

Create the view layer

Traditionally, Laravel renders views using PHP-based Blade templates, but in this project you are using Vuejs. Inertia provides the adapter that enables you to render the pages using Vue components. The method above calls Inertia’s render() function, passing it the Vuejs component.

To create the component, create a directory called Message in the resources/js/Pages directory, and create a Vue component named Send.vue. Then, add the following code to the component:

<script setup>
import { ref } from 'vue';
import { useForm } from '@inertiajs/vue3';
const props = defineProps({
    content: String,
})
const form = useForm({
    to: '',
    location: '',
    startTime: '',
    coffeeTime: '',
    message: props.content,
});
const loading = ref(false);
const submitted = ref(false);
function submitMessageForm() {
    loading.value = true;
    form.message = form.message
        .replace('{{1}}', form.location)
        .replace('{{2}}', form.startTime)
        .replace('{{3}}', form.coffeeTime);
    form.post('/send-message', {
        onSuccess: () => {
            loading.value = false;
            submitted.value = true;
            form.reset();
        },
    });
}
</script>
<template>
    <div class="flex justify-center items-center min-h-screen bg-gray-100">
        <div class="w-full max-w-5xl mx-auto bg-white shadow-md rounded-md p-8"> <!-- Adjust width here -->
            <h1 class="text-2xl font-semibold mb-6">Send WhatsApp Message</h1> <!-- Larger text -->
            <form @submit.prevent="submitMessageForm" class="space-y-6"> <!-- Increased space between fields -->
                <!-- Recipient Number Field -->
                <div>
                    <label for="to" class="block text-lg font-medium text-gray-700">Recipient Number</label>
                    <input
                        v-model="form.to"
                        type="text"
                        id="to"
                        placeholder="+1234567890"
                        class="mt-1 block w-full px-4 py-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 text-lg"
                    />
                </div>
                <!-- Location (Content Variable {{1}}) -->
                <div>
                    <label for="location" class="block text-lg font-medium text-gray-700">Service Location</label>
                    <input
                        v-model="form.location"
                        type="text"
                        id="location"
                        placeholder="Church Hall"
                        class="mt-1 block w-full px-4 py-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 text-lg"
                    />
                </div>
                <!-- Start Time (Content Variable {{2}}) -->
                <div>
                    <label for="startTime" class="block text-lg font-medium text-gray-700">Service Start Time</label>
                    <input
                        v-model="form.startTime"
                        type="text"
                        id="startTime"
                        placeholder="10:00 AM"
                        class="mt-1 block w-full px-4 py-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 text-lg"
                    />
                </div>
                <!-- Coffee Time (Content Variable {{3}}) -->
                <div>
                    <label for="coffeeTime" class="block text-lg font-medium text-gray-700">Teas & Coffees Start Time</label>
                    <input
                        v-model="form.coffeeTime"
                        type="text"
                        id="coffeeTime"
                        placeholder="9:30 AM"
                        class="mt-1 block w-full px-4 py-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 text-lg"
                    />
                </div>
                <!-- Message Field -->
                <div>
                    <label for="message" class="block text-lg font-medium text-gray-600">Message Template</label>
                    <textarea
                        v-model="form.message"
                        :disabled="true"
                        id="message"
                        rows="4"
                        placeholder="Type your message here"
                        class="mt-1 block w-full px-4 py-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 text-lg text-gray-400"
                    ></textarea>
                </div>
                <!-- Submit Button -->
                <div>
                    <button
                        :disabled="form.processing"
                        type="submit"
                        class="w-full inline-flex justify-center py-3 px-6 border border-transparent shadow-sm text-lg font-medium rounded-md text-white bg-blue-500 hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500"
                    >
                        Send Message
                    </button>
                </div>
            </form>
            <!-- Success Message -->
            <div v-if="submitted" class="mt-8 bg-green-100 border-l-4 border-green-500 p-4 rounded-md">
                <div class="flex">
                    <div class="flex-shrink-0">
                        <svg class="h-5 w-5 text-green-500" fill="currentColor" viewBox="0 0 20 20">
                            <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-11a1 1 0 00-2 0v5a1 1 0 102 0v-5zm0 7a1 1 0 00-2 0v1a1 1 0 102 0v-1z" clip-rule="evenodd"></path>
                        </svg>
                    </div>
                    <div class="ml-3">
                        <p class="text-sm leading-5 text-green-700">
                            Your message has been sent successfully.
                        </p>
                    </div>
                </div>
            </div>
        </div>
    </div>
</template>

Create another Vue component in a new file named resources/js/Pages/Message/Receipt.vue. This will be used to display the response to the form submission request. Add the following to the component:

<template>
<div>
    <div class="flex justify-center items-center min-h-screen bg-gray-100">
        <div class="bg-white shadow overflow-hidden sm:rounded-lg">
            <div class="px-4 py-5 sm:px-6">
                <h3 class="text-lg font-medium leading-6 text-gray-900">Message Receipt</h3>
                <p class="mt-1 max-w-2xl text-sm text-gray-500">Your message has been sent successfully.</p>
                <div class="p-3">
                    <div v-if="$page.props.flash.message">
                        <p>Message Id: </p>
                        <p>{{ $page.props.flash.message }}</p>
                    </div>
                    <div v-else>
                        <p>{{ $page.props.flash.error }}</p>
                    </div>
                </div>
            </div>
            <div class="p-6 flex justify-center">
                <a :href="route('get.send-message')" class="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-500 hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500">Send Another Message</a>
            </div>
       </div>
  </div>
</div>
</template>
<script setup></script>

Add code to send messages

Before you can attempt to send a message, you will need to create a new user to login to the application. You can do this in the local environment by using Tinker, Laravel’s built in CLI.

In the project directory, enter the following:

php artisan tinker

Once you are logged into the Tinker Console, you can create a new user by making use of the User model as follows:

App\Models\User::create([
    'name' => 'Steve P', 
    'email' => 'steve@myemail.com', 
    'password' => bcrypt('my_secure_password')
]);

Once you press Enter, you should see the user that you just created displayed in the console.

Make sure you have a database configured and have run your migrations. Do not do this in production.

One final thing before you can run the application: Open the file resources/js/Pages/Dashboard.vue and add the following code just below the DIV surrounding the text, “You're logged in!”:

<div class="overflow-hidden bg-white shadow-sm sm:rounded-lg">
    <div class="p-6 text-gray-900">
        <a href="/send-message" class="px-3 py-2 bg-blue-600 hover:bg-blue-800 text-white rounded">Send Announcement</a>
    </div>
</div>
If you are making changes to the Vue components, make sure to run npm run dev in another terminal so that your changes will be compiled.

When you are ready, run the following to start up the application:

php artisan serve

Once the application starts running, you should see the following in the console:

Terminal showing Laravel server running on HTTP

This indicates that the application is running and ready to start accepting requests. You can now open your browser and visit http://localhost:8000. Click on the login link at the top of the page and enter the details of the user you created earlier. On successful login, you should see the following page:

Dashboard interface with You're logged in! message and a Send Announcement button.
If you are not seeing the same as shown above, make sure you are running ‘yarn dev’ in another terminal instance or run yarn build to build the frontend assets.

Create the handler for the form submission

Create another route and controller method to handle the sending of the announcement message to Twilio Messaging API. Add the following to the route file routes/web.php underneath the previous one you added for displaying the message form.

Route::post('/send-message', [WhatsAppController::class, 'sendMessage'])
    ->name('post.send-message');

Add the following method under the showForm() function in the WhatsAppController.

public function sendMessage(Request $request) {
    $validated = $request->validate([
        'to' => 'required',
        'location' => 'required',
        'startTime' => 'required',
        'coffeeTime' => 'required',
    ]);
    $sid = config('services.twilio.sid');
    $token = config('services.twilio.token');
    $twilio = new Client($sid, $token);

    try {
        $message = $twilio->messages->create(
            "whatsapp:" . $validated['to'], // WhatsApp recipient
            [
                'contentSid' =>  config('services.twilio.content_template_sid’),
                'from' => config('services.twilio.whatsapp_from'),
                'contentVariables' => json_encode([
                    '1' => $validated['location'],
                    '2' => $validated['startTime'],
                    '3' => $validated['coffeeTime'],
                ]),
            ]
        );
    } catch (\Exception $e) {
        return redirect()
            ->route('get.show.receipt-message')
            ->with('message', 'There was a problem sending the message' . $e->getMessage());
    }

    return redirect()->route('get.show.receipt-message')
        ->with('message', $message->sid);
}

In the code above, validate the request to ensure that all the values required are submitted from the form. This is done by ensuring that the necessary configuration values are fetched and assigned to variables.

Twilio's PHP Helper Library is then used to make the request, and the response is stored in the $message variable. If the request fails, the user is redirected to the receipt page and shown the reason for the failure.

To ensure that the receipt message displays after the user is redirected, you need to update the share() function in app/Http/Middleware/HandleInertiaRequests.php.

 

{
    return [
        ...parent::share($request),
        'auth' => [
            'user' => $request->user(),
        ],
        'flash' => [
            'message' => fn () => $request->session()->get('message'),
            'error' => fn () => $request->session()->get('error'),
        ],
    ];
}

What you added is the flash array which will ensure that the frontend would receive the values stored when the response is returned.

Display the receipt message

We will now create the final route and method for the WhatsAppController. In the routes/web.php file, add the following after the previous /send-message route;

Route::get('/receipt-message', [WhatsAppController::class, 'showReceipt'])
    ->name('get.show.receipt-message');

We then create the showReceipt() function in app/Http/Controllers/WhatsAppController.php.

public function showReceipt() {
    return Inertia::render('Message/Receipt’);
}

Test that the application works as expected

  1. Click on the Send Announcement button to navigate to the WhatsApp Message form
  2. Fill in the recipient’s WhatsApp number and update the editable fields which are, "Service Location", "Service Start Time" and "Teas and Coffee start Time"
  3. Click on the Send Message button to submit the form

Send Message Form

Form for sending message

If the request is successful, you should be redirected to the receipt status page with the Message Id displayed.

Successful Receipt

Message receipt showing a unique ID and a confirmation that the message has been sent successfully.

If testing with your own number, you should have received a message that looks like the image below:

WhatsApp message from Chatham Evangelical Church about Sunday service details.

Known limitations

While testing the implementation of Twilio’s WhatsApp integration for the charity, it became clear to me that there were some limitations to this solution for their specific use case. The major limitation was that their communication style relied on unstructured, free-form messaging, which does not conform to WhatsApp’s business communication guidelines.

Since WhatsApp requires business-initiated messages to use pre-approved templates for messages sent outside a 24-hour window of the customer initiated message, the charity’s need for more flexible, spontaneous communication were restricted by this requirement

This is not a limitation with Twilio’s integration, but a restriction by Meta. It was however noted that this would be ideal for structured messaging like announcing events, appointments, dispatch notes, or even invoices.

That's how to power customer engagement with Twilio's WhatsApp integration

By integrating Twilio’s Messaging API with the WhatsApp Business API, I was able to help a charity improve its customer engagement strategies. This powerful combination utilises WhatsApp's widespread use and rich media capabilities alongside Twilio’s reliable infrastructure, providing a seamless way to communicate on a global scale.

While there are a few limitations, the benefits of enhanced communication, greater reach, and stronger customer relationships make it an invaluable tool for businesses.

Stephen Popoola is an Engineering Manager for a UK based financial services company. He also works part time as a CTO for a Medical Services company. With over 20 years of experience in software development across various technologies, he brings a wealth of knowledge and expertise to his work. You can learn more about Stephen on his website at https://stephenpopoola.uk/.