Tutorial

How to Send Push Notifications in Laravel

July 5, 20269 min readThe Trigger Engage team

Push notifications pull people back into your app — but only if you send the right one at the right time. Done well, they are the highest-signal channel you have: a tap away from the exact screen a user needs. Done badly, they are the fastest way to earn an uninstall. This guide shows you how to send push notifications in Laravel and, more importantly, how to wire them into automated journeys so each one is timely and relevant instead of noise.

A phone lock screen showing a push notification bringing a user back to the app
A well-timed push pulls people back — as long as it stays relevant.

Web push vs mobile push

"Push" covers two different delivery paths, and it helps to keep them straight before you write any code.

Web push runs in the browser. A visitor grants permission, the browser registers a service worker, and you receive a subscription object (an endpoint URL plus encryption keys) that you store server-side. You then POST an encrypted payload to that endpoint, and the browser's push service wakes the service worker to show the notification — even when your tab is closed. It works in Chrome, Firefox, Edge, and (since recent versions) Safari on macOS and iOS for installed web apps.

Mobile push targets native iOS and Android apps. Here the operating system owns delivery: Apple Push Notification service (APNs) for iOS, Firebase Cloud Messaging (FCM) for Android. Each installed app instance hands you a device token, and you send messages to Apple or Google, who deliver to the device.

In both cases your Laravel app never talks to the device directly — it talks to a push service, keyed by a token or subscription you stored against a user. Managing APNs certificates and FCM keys by hand is fiddly, so most teams put a provider in between that abstracts both.

Choose a provider

A push provider manages the parts you do not want to own: device-token storage, APNs/FCM credentials, retries, and delivery reporting. OneSignal is a common choice because it handles iOS, Android, and web push behind one API; you can also talk to FCM and APNs directly if you prefer no middleman.

Whatever you pick, your side of the contract is the same: keep a mapping between your users and their devices. When someone logs in on a phone, you associate that device (or the provider's "player"/subscription id) with the user record. That mapping is what lets you say "notify user 4821" instead of juggling raw tokens.

PathDelivery serviceWhat you store
Web pushBrowser push service (via VAPID)Subscription endpoint + keys
iOSAPNsDevice token
AndroidFCMRegistration token
Any (abstracted)Provider e.g. OneSignalPlayer / external user id

Option A: Laravel notifications with a push channel

Laravel's notification system is the cleanest home for push. You define a Notification class, list a push channel in via(), and build the payload in a to...() method. Queue it so a slow provider call never blocks the request.

use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;

class SessionReminder extends Notification implements ShouldQueue
{
    use Queueable;

    public function via($notifiable): array
    {
        return ['onesignal']; // or your custom push channel
    }

    public function toOneSignal($notifiable)
    {
        return [
            'title' => 'Your session starts soon',
            'body'  => 'Tap to join — your therapist is ready.',
            'url'   => 'myapp://session/' . $this->sessionId,
        ];
    }
}

Trigger it with $user->notify(new SessionReminder($id)). The channel package reads the user's stored device/player id from a routeNotificationFor...() method on your User model. Keep the payload provider-agnostic — a title, a body, and a deep-link URL are all you truly need — so swapping providers later is a config change, not a rewrite. Store the provider key and app id in config/services.php, never inline.

Tip: Always queue push notifications. Provider APIs occasionally take a second or two, and you never want a user's request — or an event handler — waiting on a third-party HTTP call. Queueing also gives you free retries when the provider hiccups.

Option B: call the provider API directly

If you do not want a channel package, or you are targeting a whole segment rather than one user, call the provider's HTTP endpoint yourself with Laravel's Http client.

use Illuminate\Support\Facades\Http;

Http::withToken(config('services.push.key'))
    ->post('https://provider.example/api/v1/notifications', [
        'app_id'            => config('services.push.app_id'),
        'include_external_ids' => [(string) $user->id],
        'headings'          => ['en' => 'We saved your spot'],
        'contents'          => ['en' => 'Finish booking in two taps.'],
    ]);

This is fine for one-off blasts, but notice what it encourages: send calls scattered across controllers and jobs. That gets hard to reason about the moment you add a second channel.

Send push from events (as part of a journey)

The better pattern is to stop calling "send push" from your business logic at all. Instead, fire a single event that describes what happened — session_booked, trial_ending, cart_abandoned — and let a messaging engine decide which channels to use and when. Push then sits alongside email and SMS as one output of the same trigger, instead of a call you have to remember to make.

That is exactly what Trigger Engage does — an open-source, MIT-licensed engine you self-host or embed in Laravel (composer require trigger-engage/server), with a first-party SDK for identifying users and emitting events. Push is delivered through OneSignal, and there is no per-profile fee.

use TriggerEngage\Laravel\TriggerEngage;

TriggerEngage::identify($user->id, [
    'email' => $user->email,
    'name'  => $user->name,
]);

TriggerEngage::event($user->id, 'session_booked', [
    'session_id' => $id,
    'starts_at'  => $startsAt,
]);

The SDK is fail-open: if the engine is unreachable, your booking still succeeds — messaging never becomes a hard dependency of your core flow. A journey configured for session_booked might send a confirmation email immediately and a push reminder 30 minutes before the session, with no send logic living in your controller.

Push best practices

Push is a privilege the user grants, and they can revoke it instantly by muting or uninstalling. Protect it.

  • Ask permission at the right moment. Never prompt on first load, before the user understands your value. Ask right after a moment that makes notifications obviously useful — after booking, after a first success, after opting into reminders.
  • Cap frequency. Set a per-user ceiling and honour quiet hours. A single well-timed push beats five interruptions.
  • Deep-link to the relevant screen. A notification that dumps the user on your home screen wastes the tap. Include a URL that lands them exactly where the message points.
  • Segment so it is relevant. Send to the users a message actually applies to. Over-notifying drives uninstalls — the exact opposite of what you want when you are trying to reduce churn.

Where push fits in the lifecycle

Push is powerful but narrow: it is short, it is interruptive, and it only reaches users who installed your app and kept notifications on. That makes it a supporting channel, not a standalone strategy. Its real strength shows when it is one instrument in a broader lifecycle sequence — email carries the detail, SMS catches the time-critical moments, and push nudges an engaged user back at just the right second. Think of it as one channel inside your wider marketing automation approach, chosen per message rather than defaulted to. Fire events, let the engine pick the channel, and push earns its place instead of wearing out its welcome.

How do I send push notifications in Laravel?
Store a device token or subscription for each user, then either use a Laravel Notification class with a push channel in via(), or POST to a provider's API with the Http client. Queue the send so a slow provider call never blocks the request, and keep the payload to a title, body, and deep-link URL so you stay provider-agnostic.
Web push vs mobile push — what's the difference?
Web push runs through the browser via a service worker and a stored subscription, and works even when your tab is closed. Mobile push targets native iOS and Android apps through APNs and FCM using device tokens. In both cases Laravel talks to a push service, not the device directly, so a provider that abstracts all three keeps your code simple.
How often should I send push notifications?
As rarely as you can while still being useful. Set a per-user frequency cap, respect quiet hours, and only send to users a message genuinely applies to. Over-notifying is one of the top causes of uninstalls, so err toward fewer, better-timed, well-segmented pushes.
Can I send push, email, and SMS from one event?
Yes — that is the recommended pattern. Fire a single event describing what happened and let a messaging engine choose the channels. With Trigger Engage you call TriggerEngage::event() once, and a configured journey can send email, SMS, and push (via OneSignal) on whatever schedule you define, with no per-channel logic in your app code.