Laravel notifications are a single API for sending one event to email, Slack, SMS, an in app inbox, and broadcast. You write one notification class, list the channels you want in a via() method, and Laravel delivers each channel independently.
Most codebases arrive at that answer late. A Mailable sends the customer receipt, a Guzzle call posts to a Slack webhook from a controller, and an SMS client hides in a service nobody wants to open. Three code paths, three versions of the wording, three places to change when someone rewrites the copy.
You already know the duplication is the real cost. By the end of this guide you will have one class that fans out to email, Slack, and SMS, queued so it never slows a request, and covered by a test that proves it fired. We start with how the channel API works, generate the class with Artisan, wire each channel in turn, then cover queueing, the database inbox, and the mistakes that only surface in production.
Key TakeawaysOne notification class can target mail, database, broadcast, Slack, and SMS. The
via()method decides which channels run for each recipient.Mail, database, and broadcast ship with the framework. Slack needslaravel/slack-notification-channeland SMS needslaravel/vonage-notification-channelfrom Laravel 9 onward.Queued notifications dispatch a separate job per channel per recipient, so a Slack outage never holds up the invoice email.CallafterCommit()when you notify inside a database transaction, otherwise the worker can pick up the job before the row exists.Notification::fake()withassertSentTo()asserts channels and payloads without touching a single external API.
What Laravel notifications are and how the channel API works
A Laravel notification is a small class that describes one event and knows how to render itself for every delivery channel you name. The Notifiable trait on your model gives you notify(). The via() method returns an array of channels. A matching toMail(), toSlack(), or toVonage() method builds the payload for each one.
That separation is the whole design. Your application code says what happened. The notification decides how it looks per audience. Adding a fourth channel later means adding one method, not rewriting the calling code.
The three moving parts
The notifiable. Any model using the
Notifiabletrait, usuallyUser. It also holds the routing, such as which phone number or Slack channel to use.The notification. A class in
app/Notificationswith avia()method and one builder method per channel.The channel. The driver that actually sends. Mail uses your existing mailer config, Slack posts through a bot token, Vonage calls an SMS API.
Channels that ship with Laravel
Channel | Builder method | Package | Best for |
|---|---|---|---|
toMail() | Built in | Receipts, password resets, digests | |
database | toArray() or toDatabase() | Built in | In app inbox and bell icons |
broadcast | toBroadcast() | Built in with Echo | Live UI updates |
slack | toSlack() | laravel/slack-notification-channel | Internal ops and revenue alerts |
vonage | toVonage() | laravel/vonage-notification-channel | Time critical SMS |
Beyond those, the community channels registry at laravel-notification-channels.com adds Telegram, WhatsApp, Discord, web push, and dozens more, all with the same builder method shape. The official Laravel notifications documentation is the reference to keep open while you build.
Wiring five channels by hand is exactly the sort of Laravel plumbing an assistant should write for you. StackMivo turns a plain English description into production-ready Laravel apps, with notification classes, migrations, and Pest tests included. Get started free.
Generate your first notification class with Artisan
Start with the generator. One Artisan command creates the class, and a second creates the migration for the database channel if you plan to keep an in app copy.
php artisan make:notification InvoicePaid
php artisan make:notifications-table
php artisan migrateOpen the generated file and give it a constructor, a via() method, and the ShouldQueue contract. Constructor property promotion keeps the class short, and returning channels dynamically lets each recipient choose how they hear from you.
namespace App\Notifications;
use App\Models\Invoice;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Notification;
class InvoicePaid extends Notification implements ShouldQueue
{
use Queueable;
public function __construct(public Invoice $invoice)
{
}
public function via(object $notifiable): array
{
return $notifiable->wants_sms? ['mail', 'database', 'vonage']: ['mail', 'database', 'slack'];
}
}Notice that via() receives the notifiable. Read a preference column, a plan tier, or a feature flag there and the same class serves everyone. A trial user gets mail and an in app record. An account owner on a paid plan also gets the text message.
Route each channel on the notifiable
Mail routing works out of the box because Laravel looks for an email attribute. Every other channel needs a routing method on the notifiable, which is where you decide the destination.
use Illuminate\Notifications\Notification;
public function routeNotificationForVonage(Notification $notification): string
{
return $this->phone_e164;
}
public function routeNotificationForSlack(Notification $notification): string
{
return '#billing-alerts';
}Store phone numbers in E.164 format with the country code, because providers reject local formats. If you want the full list of Artisan generator flags for the models these notifications reference, the php artisan make:model cheatsheet covers every one.
Send a Laravel email notification with MailMessage
Any Laravel notification tutorial starts with mail, because it is the channel most teams already have configured. A Laravel email notification returns a MailMessage, and the framework renders it with a built in template.
use Illuminate\Notifications\Messages\MailMessage;
public function toMail(object $notifiable): MailMessage
{
return (new MailMessage)
->subject('Invoice '.$this->invoice->number.' is paid')
->greeting('Thanks, '.$notifiable->first_name)
->line('We received '.$this->invoice->formatted_total.'.')
->action('View receipt', route('invoices.show', $this->invoice))
->line('No further action is needed.');
}The builder gives you subject(), greeting(), line(), action(), and salutation(). Call error() for a red action button on failure notices. Pass markdown() a Blade view when the default layout is not enough, or view() when you want full control of the HTML.
Brand the default template
Publish the notification views once and every mail notification in the application picks up your logo, colours, and footer.
php artisan vendor:publish --tag=laravel-notificationsA three person team running a subscription billing app kept receipt wording in a Mailable, a Slack helper, and a nightly script that texted overdue accounts. Finance changed how refunds were described, and only two of the three were updated. Support fielded the mismatch for weeks before anyone traced it back to the duplicated copy. Folding all three paths into one InvoicePaid notification made the wording a single edit, and the drift stopped.
Add Slack and SMS to the same notification class
Two more methods on the class you already have is the entire job. Neither channel touches your controllers, your events, or the code that fired the notification in the first place.
Post Slack alerts with Block Kit
Install the first party channel package, add a bot token and default channel under services.slack.notifications in config/services.php, then invite the bot to the channel it will post in. A missing invite is the most common reason a message silently never appears.
composer require laravel/slack-notification-channeluse Illuminate\Notifications\Slack\BlockKit\Blocks\ContextBlock;
use Illuminate\Notifications\Slack\SlackMessage;
public function toSlack(object $notifiable): SlackMessage
{
return (new SlackMessage)
->text('Invoice '.$this->invoice->number.' was paid.')
->headerBlock('Payment received')
->contextBlock(function (ContextBlock $block) {
$block->text($this->invoice->formatted_total.' from '.$this->invoice->account->name);
});
}The builder maps onto Slack's Block Kit format, so header blocks, section blocks, context blocks, and dividers are all available. Keep text() populated anyway, because that string is what appears in the notification preview and on mobile lock screens. Multi tenant apps can return a Slack route carrying the tenant's own token and channel, which is how multi tenant SaaS platforms deliver alerts into each customer's workspace instead of one shared one.
Send SMS through the Vonage channel
Laravel SMS runs through Vonage, the first party channel the framework maintains. Install the package alongside Guzzle, then set VONAGE_KEY, VONAGE_SECRET, and VONAGE_SMS_FROM in your environment.
composer require laravel/vonage-notification-channel guzzlehttp/guzzleuse Illuminate\Notifications\Messages\VonageMessage;
public function toVonage(object $notifiable): VonageMessage
{
return (new VonageMessage)
->content('Invoice '.$this->invoice->number.' is paid. Receipt is in your inbox.')
->from('AcmeBilling');
}The from value is either a purchased number or an alphanumeric sender ID, and sender ID support varies by country. Check the Vonage SMS documentation for your target markets before launch, because a sender ID that works in one region silently fails in another.
Keep SMS short and cheap
Stay under 160 characters. Standard GSM-7 encoding fits 160 characters in one segment. Longer messages split and bill per segment.
Avoid stray emoji. A single emoji switches the message to unicode and drops the limit to 70 characters. Call
unicode()deliberately when you need it.Send only what is urgent. Every message costs money and interrupts someone. Reserve SMS for events that need action within the hour.
Record consent. Store when and how each number opted in, and honour opt out replies.
Each channel brings its own package, config keys, and payload shape, which is a lot of documentation to hold in your head at once. StackMivo's Laravel-native intelligence is trained on the ecosystem, so it writes the channel methods, the routing, and the tests the way Laravel expects them.
Queue Laravel notifications and store them in the database
An agency shipping a booking system sent confirmation texts straight from the checkout controller. Staging looked fine. On the first busy weekend the SMS provider slowed down, checkout requests began timing out, and customers who had already paid saw an error page. Adding ShouldQueue to the notification moved delivery off the request cycle, and the timeouts stopped that afternoon.
Queueing is one interface and one trait, both already present in the class above. Once the notification implements ShouldQueue, Laravel pushes delivery to your queue connection instead of blocking the response. Four behaviours are worth knowing.
One job per channel. Laravel dispatches a separate job for each channel and each recipient, which is why
viaQueues()can route Slack and mail to different queues. A Slack outage retries on its own without delaying the email.Transactions need
afterCommit(). Chain it onto the notification so the worker never reads a row the transaction has not written yet.Delays are per channel.
withDelay()returns an array keyed by channel, so the email can go now while the text waits 10 minutes.Late cancellation is free. A
shouldSend()method runs on the worker and skips delivery if the invoice was voided between dispatch and processing.
Give users an in app inbox
The database channel writes a row into the notifications table with a JSON payload built by toArray(). That is all a bell icon needs.
public function toArray(object $notifiable): array
{
return [
'invoice_id' => $this->invoice->id,
'amount_cents' => $this->invoice->total_cents,
'message' => 'Invoice '.$this->invoice->number.' is paid.',
];
}Read them back from any notifiable through the notifications and unreadNotifications relationships, then mark them read.
foreach ($user->unreadNotifications as $notification) {
echo $notification->data['message'];
}
$user->unreadNotifications->markAsRead();One deployment detail catches people out. Queue workers hold the old code in memory, so a notification you just shipped will not run until those workers restart. Platforms with one-click deployment run the migrations and restart the queue together, which removes the most common cause of notifications that work locally and go quiet in production.
Test Laravel notifications and avoid the common mistakes
No mail server and no SMS credits are needed to test any of this. Notification::fake() swaps the real sender for a recorder, and the assertions check who was notified, on which channels, and with which payload.
use App\Models\Invoice;
use App\Notifications\InvoicePaid;
use Illuminate\Support\Facades\Notification;
it('notifies the account owner when an invoice is paid', function () {
Notification::fake();
$invoice = Invoice::factory()->create();
$invoice->markAsPaid();
Notification::assertSentTo(
$invoice->account->owner,
InvoicePaid::class,
fn ($notification, $channels) => in_array('vonage', $channels)
);
});Swap markAsPaid() for whatever action your domain uses. Useful assertions include assertSentTo(), assertNotSentTo(), assertNothingSent(), and assertCount(). The closure receives both the notification and the channel array, so a single test can prove the customer got mail while the ops team got Slack.
Five mistakes that show up in production
Notifying inside a transaction without
afterCommit(). The job runs before the commit lands and the worker cannot find the model.Skipping the queue. Every synchronous channel adds its provider's latency to your response time.
Hardcoding channels. Read preferences inside
via()instead, or you will ship an unsubscribe bug.Forgetting worker environment variables. The web container has the Slack token, the worker container does not, and the failure is silent.
Writing no test. Notifications fail quietly by design, so a broken channel can go unnoticed for months.
Building all of this by hand takes a day the first time and an hour every time after. Describing the feature and reviewing the generated Laravel is faster. Try StackMivo Now on your next feature and keep the app you get.
Start with one Laravel notification class
Laravel notifications collapse three delivery paths into one class you can read in a minute. The via() method decides the channels per recipient. The builder methods shape the message for each audience without duplicating the wording. ShouldQueue keeps provider calls off the request cycle, and the database channel hands you an in app inbox for free.
Pick one event you already send by email and rebuild it this week. Generate the class with Artisan, move the existing Mailable body into toMail(), add the database channel so a record always exists, then add Slack once the mail path is green. Write the fake based test before you add SMS, because that is the channel where mistakes cost real money.
This pattern scales further than most teams expect. Once the class exists, adding push, Telegram, or a customer webhook is one method and one line in via(), and your controllers never change again.