Sending and receiving SMS Laravel Notifications with Nexmo

Published on by

Sending and receiving SMS Laravel Notifications with Nexmo image

This is the first part of a series of tutorials by Michael Heap, covering how to build a multi-channel help desk system with Laravel. In this post, we’ll take a basic web based help desk system and extend it to send and receive SMS messages using the built-in Laravel notification system (which is powered by Nexmo).


To work through this post, you’ll need a Nexmo account and the Nexmo Command Line tool installed and configured, as well as your usual Laravel prerequisites.

In the second half of the post, we’ll be receiving webhooks from Nexmo when you receive an SMS message, so you’ll need to expose your local web server somehow. We recommend using ngrok for this.

Getting started

Scaffolding an application and generating models and migrations is tedious, and getting you to create forms and views is even worse. To save everyone a lot of pain, we’ve created a quick start repository that you can clone and run that has all of the boring work done for you.

Start by forking the Deskmo repository so that once you’ve made some changes, you can commit them back to Github. Then, clone the initial-scaffolding branch on to your local machine. This branch contains pre-generated models, migrations, and forms.

git clone -b initial-scaffolding<YOUR_USER>/deskmo
cd deskmo

Once you have the repository on your machine, copy .env.example to .env and edit any values (e.g. database credentials) that you may need to change for your application to run.

The next thing to do is run composer install to fetch all of our dependencies. This is a vanilla Laravel install, with the exception of laravelcollective/html being added for our form generation.

Once Composer has finished, it’s time to run our application. We need to generate a new application key, run our database migrations and start the built in web server

php artisan key:generate
php artisan migrate
php artisan serve

To use the application, you’ll need to register an account (don’t worry, this is just in your local database!). You might notice that the registration form has an additional field, phone_number. This is the number we’ll send an SMS to later on so make sure you enter your number carefully in international format, omitting the leading 0 (e.g. 14155550100). If you’re interested in learning how to add additional fields to your registration form, take a look at this commit.

Creating an account will automatically log you in and take you to a page that says there are no tickets. Click on New Ticket in the top right and create a new ticket.

Your “Recipient User ID” will be “1”. This could be a nice autocomplete at some point, but entering the user ID works just as well for now

Once you click Submit it should take you back to the list of tickets, but this time the ticket your just created should be shown.

We’re finally in a position to start writing code! Although we have a system that allows you to create tickets there’s still a lot more that it can do. In the rest of this post, we’ll purchase a Nexmo phone number, send an SMS notification whenever a ticket is created and allow the customer to reply to the ticket via SMS.

Purchasing a Nexmo phone number

We mentioned earlier that we’d need a Nexmo phone number to receive SMS messages, and that they’d be sent to us as a webhook. To make it easy for people to reply, we’ll use the same number as our outbound number too.

You can purchase and configure a number via the Nexmo Dashboard, but today we’ll be using the Nexmo CLI.

To purchase a number, we use the number:search command to find a number with both SMS and voice support, then the number:buy command to purchase that number. We’re buying a US number here, but you can replace US with any of the 45+ countries that Nexmo has local numbers in.

nexmo number:search US --sms --voice
nexmo number:buy <NUMBER> --confirm

Make a note of the number returned as we’ll need it very soon.

Configuring Nexmo

To send an SMS notification via Laravel’s notification system, we need to install nexmo/client with composer and configure our application with our Nexmo API key and secret, plus the number that we just bought.

Install nexmo/client by running composer require nexmo/client. Next, edit config/services.php and add the following configuration options:

'nexmo' => [
'key' => env('NEXMO_KEY'),
'secret' => env('NEXMO_SECRET'),
'sms_from' => env('NEXMO_NUMBER'),

Finally, edit .env and provide your API key, secret and Nexmo number. It should look like this:


That’s all it takes to integrate Nexmo with Laravel’s notification system – one composer install and three config values.

Sending a notification

We’re now in a position to send a notification, but we don’t have any notifications to send! Generate a new notification by running php artisan make:notification TicketCreated and open up your newly created file (app/Notifications/TicketCreated.php).

The first thing to change is on line 32, where the via function returns mail. We don’t want to send this via email, so we change the value to nexmo instead to send it as an SMS.

The notification class knows how to format the message for an email, but not for an SMS. To solve this problem, add a new method with the following contents:

public function toNexmo($notifiable)
return (new NexmoMessage)

This method uses two new objects – NexmoMessage and $this->entry. Let’s add those to the class now by adding the following to your imports:

use App\TicketEntry;
use Illuminate\Notifications\Messages\NexmoMessage;

You’ll also need to update your __construct() method to accept a TicketEntry and add $entry as a class variable:

protected $entry;
public function __construct(TicketEntry $entry)
$this->entry = $entry;

We now have a notification that can be sent to a user, but we’re still not quite there as we don’t know who we need to send it to.

A ticket can have multiple subscribed users, and a user can subscribe to multiple tickets. To expose this relationship, we need to add a new method to our Ticket class. We specify ticket_subscriptions as the second parameter as it’s a non-standard pivot table name (these users don’t own the ticket, they’re just subscribed to notifications on it)

public function subscribedUsers()
return $this->belongsToMany(User::class, 'ticket_subscriptions');

Once that’s done, open up TicketController and add the following to your list of imports so that we can send the notification we just created:

use App\Notifications\TicketCreated;
use Notification;

Finally, we need to actually send the notification. At the bottom of the store method, just before we redirect back to the index page add the following code:

Notification::send($ticket->subscribedUsers()->get(), new TicketCreated($entry));

This fetches all subscribed users for the ticket and dispatches a new TicketCreated notification for the newly created entry. Save your changes and try creating a new ticket with recipient ID 1 (which should be your account). You should receive an SMS with the content of the entry within a few seconds.

Congratulations! We just added SMS notifications to our app. Once we had everything configured it was nice and easy, just four short steps:

  • Generate a notification with php artisan make:notification
  • Change the via method to use nexmo
  • Add a toNexmo() method so that Laravel knows how to render the message
  • Add Notification::send() to our app to send the notification

The code for this section is on the send-sms-notification branch on Github

Receiving an SMS reply

So, our customer has received the ticket content as an SMS, but what next? Do they have to log on to a computer to be able to reply to the ticket?

We don’t actually have a way for them to reply at all yet, and if we have to spend effort to build something let’s make it as easy as possible for them – let’s add a way for them to reply via SMS.

This post assumes that your webhook HTTP method is set to POST in your Nexmo settings. It’s set to GET by default, so go and check!

When Nexmo receive an SMS to your phone number, they send a HTTP request to your application. Unfortunately, your application is currently running on your local machine and Nexmo can’t access it. This is where ngrok comes in. If you don’t have it installed, read this introduction then come back once it’s installed.

To expose your local server to the internet, run ngrok http 8000. This will bring up an ngrok session with lots of information in it. The only thing you need is your ngrok host name, which will be next to the word Forwarding and look something like

Session Status online
Account Michael (Plan: Free)
Version 2.2.8
Region United States (us)
Web Interface
Forwarding -> localhost:8000
Forwarding -> localhost:8000
Connections ttl opn rt1 rt5 p50 p90
6 0 0.00 0.00 0.11 0.78

Once you have an ngrok URL, you’ll need to tell Nexmo what your public URL is using the Nexmo CLI tool. Run the following command, replacing the placeholders with your Nexmo number and ngrok URL.

nexmo link:sms <NEXMO_NUMBER> http://<NGROK_URL>/ticket-entry

This configures Nexmo to send all webhooks for that number to http://<NGROK_URL>/ticket-entry. Let’s put together a quick test to make sure that everything is configured correctly. Open up TicketEntryController and replace the store method with the following:

public function store(Request $request)
error_log(print_r($request->all(), true));

This will log any incoming requests to the console in the terminal that we ran php artisan serve in, but it won’t work just yet. There’s one more change to make before we can test our SMS receiver – we need to disable CSRF protection for the ticket-entry route.

By default, Laravel protects us from cross site attacks by validating a CSRF token on every form submission (this is a good thing!), but Nexmo can’t know the token to use. To work around this, edit Http/Middleware/VerifyCsrfToken.php and add ticket-entry to the $except array like so:

protected $except = [

If you now reply to one of the earlier SMS messages you received earlier you should see an array of information on the screen (it may take up to a minute to come through):

[msisdn] => 14155550201
[to] => 14155550100
[messageId] => 0C0103008660C470
[text] => This is a test
[type] => text
[keyword] => THIS
[message-timestamp] => 2018-01-09 02:11:40
[timestamp] => 1515464263
[nonce] => e143f53f-dev2-42f5-9103-f81e56a406c3
[sig] => ef52cbcabcc2e631eff3853ec1bfe38b

This means that our integration is configured correctly, and any time that number receives an SMS, Nexmo will send a HTTP request to our app.

The final thing to do is to replace the print_r statement with some code that adds an entry to a ticket. As there’s no concept of replies with SMS messages, all we can do is look for the latest ticket with activity that a user is watching and assume that their reply is intended for that ticket.

As this logic is quite complicated, let’s add it to our User model for future reuse. Open up app/User.php and add the following method:

public function latestTicketWithActivity() {
// Get all tickets this user is watching
$watchedTickets = TicketSubscription::select('ticket_id')->where('user_id', $this->id)->get()->pluck('ticket_id');
// Grab the latest ticket with activity that's in this list
$latestTicketEntry = TicketEntry::select("ticket_id")->whereIn('ticket_id', $watchedTickets)->orderBy('created_at', 'desc')->limit(1)->first();
// Fetch the actual ticket
return $latestTicketEntry->ticket()->first();

This will return the Ticket object that has the most recent TicketEntry for a given user. This is all we need to attach our incoming message to the ticket.

Go back to TicketEntryController and replace the print_r line with the following code, which will look up a user based on their phone number, fetch the latest ticket and attach a new entry to it with the incoming message’s content:

// Make sure it's in the correct format
$data = $this->validate($request, [
'msisdn' => 'required',
'text' => 'required'
// Find the user based on their phone number
$user = User::where('phone_number', $data['msisdn'])->firstOrFail();
// And then find their latest ticket
$ticket = $user->latestTicketWithActivity();
// Create a new entry with the incoming SMS content
$entry = new TicketEntry([
'content' => $data['text'],
'channel' => 'sms',
// Attach this entry to the user and ticket, then save
return response('', 204);

Finally, we need to add two more imports for classes that we’ve used in the code above:

use App\User;
use App\TicketEntry;

Save this and send an SMS reply, then refresh the latest ticket and watch as your message appears.

Receiving an SMS is even easier than sending an SMS! We went from zero capability to SMS replies via webhooks in just five small steps:

  • Configure Nexmo webhooks for a number
  • Run ngrok to expose your local system
  • Disable CSRF protection for the webhook endpoint
  • Fetch the latest active ticket
  • Save the inbound message as a new entry

The code for this section is on the receive-sms-reply branch on Github


In this post, we covered sending notifications via the Laravel notification system and Nexmo, as well as receiving incoming SMS messages and storing them in a database.

In the next part of this series, we’ll be looking at adding voice call support to our help desk application. A ringing phone is harder to ignore than a text message, and for time-sensitive issues, it might be the correct choice.

If you’d like some Nexmo credit to work through this post and test the platform out, get in touch at quoting LaravelNews and we’ll get that sorted for you.

If you have any thoughts or questions, don’t hesitate to reach out to @mheap on Twitter or to

Many thanks to Nexmo for sponsoring this tutorial series here on Laravel News

Michael Heap photo

Michael is a PHP developer advocate at Nexmo. Working with a variety of languages and tools, he shares his technical expertise to audiences all around the world at user groups and conferences. When he finds time to code, he enjoys reducing complexity in systems and making them more predictable.


Laravel Newsletter

Join 40k+ other developers and never miss out on new tips, tutorials, and more.


Version 4 of Tinkerwell is available now. Get the most popular PHP scratchpad with all its new features and simplify your development workflow today.

Visit Tinkerwell
Laravel Forge logo

Laravel Forge

Easily create and manage your servers and deploy your Laravel applications in seconds.

Laravel Forge
Tinkerwell logo


The must-have code runner for Laravel developers. Tinker with AI, autocompletion and instant feedback on local and production environments.

No Compromises logo

No Compromises

Joel and Aaron, the two seasoned devs from the No Compromises podcast, are now available to hire for your Laravel project. ⬧ Flat rate of $7500/mo. ⬧ No lengthy sales process. ⬧ No contracts. ⬧ 100% money back guarantee.

No Compromises
Kirschbaum logo


Providing innovation and stability to ensure your web application succeeds.

Shift logo


Running an old Laravel version? Instant, automated Laravel upgrades and code modernization to keep your applications fresh.

Bacancy logo


Supercharge your project with a seasoned Laravel developer with 4-6 years of experience for just $2500/month. Get 160 hours of dedicated expertise & a risk-free 15-day trial. Schedule a call now!

Lucky Media logo

Lucky Media

Bespoke software solutions built for your business. We ♥ Laravel

Lucky Media
Lunar: Laravel E-Commerce logo

Lunar: Laravel E-Commerce

E-Commerce for Laravel. An open-source package that brings the power of modern headless e-commerce functionality to Laravel.

Lunar: Laravel E-Commerce
LaraJobs logo


The official Laravel job board

Larafast: Laravel SaaS Starter Kit logo

Larafast: Laravel SaaS Starter Kit

Larafast is a Laravel SaaS Starter Kit with ready-to-go features for Payments, Auth, Admin, Blog, SEO, and beautiful themes. Available with Vue and Livewire stacks.

Larafast: Laravel SaaS Starter Kit
SaaSykit: Laravel SaaS Starter Kit logo

SaaSykit: Laravel SaaS Starter Kit

SaaSykit is a Laravel SaaS Starter Kit that comes with all features required to run a modern SaaS. Payments, Beautiful Checkout, Admin Panel, User dashboard, Auth, Ready Components, Stats, Blog, Docs and more.

SaaSykit: Laravel SaaS Starter Kit
Rector logo


Your partner for seamless Laravel upgrades, cutting costs, and accelerating innovation for successful companies


The latest

View all →
SQLite Studio is a SQLite Database Explorer image

SQLite Studio is a SQLite Database Explorer

Read article
Running a Single Test, Skipping Tests, and Other Tips and Tricks image

Running a Single Test, Skipping Tests, and Other Tips and Tricks

Read article
View Third-party Relations in model:show - Now Available in Laravel 11.11 image

View Third-party Relations in model:show - Now Available in Laravel 11.11

Read article
Asserting a JSON Response Structure in Laravel image

Asserting a JSON Response Structure in Laravel

Read article
Backpack turns 8 years old, celebrates with 40% discount image

Backpack turns 8 years old, celebrates with 40% discount

Read article
Create a DateTime from a Timestamp With this New Method Coming to PHP 8.4 image

Create a DateTime from a Timestamp With this New Method Coming to PHP 8.4

Read article