Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Onesignal Laravel Package

laravel-notification-channels/onesignal

OneSignal notification channel for Laravel. Send push notifications through OneSignal using Laravel’s notification system, with support for segments, filters, custom data, buttons, and platform-specific delivery options.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install and wire credentials

composer require laravel-notification-channels/onesignal

Grab two values from the OneSignal dashboard (Settings → Keys & IDs): the App ID and the REST API Key. Put them in config/services.php — this package reads from services, not from its own config file:

'onesignal' => [
    'app_id'       => env('ONESIGNAL_APP_ID'),
    'rest_api_key' => env('ONESIGNAL_REST_API_KEY'),
],
ONESIGNAL_APP_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
ONESIGNAL_REST_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

No vendor:publish step is normally required; the service provider binds the client and registers the channel. Verify the exact config keys against src/OneSignalServiceProvider.php in the version you pin — this is the single most common "it silently sends nothing" cause.

2. Teach your notifiable how to be reached

class User extends Authenticatable
{
    use Notifiable;

    public function routeNotificationForOneSignal($notification = null): array|string|null
    {
        return $this->onesignal_subscription_id; // or an array for multi-device
    }
}

That column is populated by your mobile app calling OneSignal.login($externalId) / OneSignal.getDeviceState() and POSTing the subscription ID back to Laravel, or by registering the ID at app boot.

3. First notification

class OrderShipped extends Notification implements ShouldQueue
{
    use Queueable;

    public function __construct(public Order $order) {}

    public function via($notifiable): array
    {
        return [OneSignalChannel::class];
    }

    public function toOneSignal($notifiable): OneSignalMessage
    {
        return OneSignalMessage::create()
            ->setSubject('Your order shipped')
            ->setBody("Order #{$this->order->number} is on its way.")
            ->setUrl(route('orders.show', $this->order))
            ->setData(['order_id' => $this->order->id, 'type' => 'order_shipped']);
    }
}

Where to look first, in this order: the README (short, example-driven), src/OneSignalChannel.php (how routing and sending actually work — it's ~30 lines and worth reading), src/OneSignalMessage.php (the full setter surface), then src/OneSignalClient.php (the raw payload shape and error handling). Because the package is niche (294 stars, no downstream dependents), the source is the documentation for anything beyond the happy path.

First real use case: transactional, single-user push ("your driver is 5 minutes away", "invoice ready") triggered from a queued Laravel notification. This is where the channel shines: one class, one message builder, no device-side code changes.

Implementation Patterns

Audience selection drives your architecture. Decide up front which of the three targeting modes you're in, because they route differently:

Mode How Use for
Subscription / player ID routeNotificationForOneSignal() returns ID(s) Transactional push to a known user
External user ID message-level include_external_user_ids (alias-based in newer API) Same, but survives app reinstalls
Segment / filter / tag message-level segment or filter setters Broadcasts, marketing, "all users in region X"

For the segment/filter mode the notifiable route becomes irrelevant — confirm in your pinned version whether the channel omits include_player_ids when the message already carries an audience, or whether you need to return null from the route.

Wrap the message builder, don't repeat it. Every notification in a mature app ends up with the same 6 setters. Centralise:

abstract class PushNotification extends Notification
{
    protected function base(string $title, string $body, array $data = []): OneSignalMessage
    {
        return OneSignalMessage::create()
            ->setSubject($title)
            ->setBody($body)
            ->setData($data + ['app' => config('app.name')])
            ->setSound('default');
    }
}

Multi-device users: return an array from routeNotificationForOneSignal() (or ->setIncludePlayerIds([...]) if you target per-message). OneSignal deduplicates delivery; your job is keeping the array clean — prune stale IDs when OneSignal reports recipients: 0.

On-demand notifications work without a model:

Notification::route(OneSignalChannel::class, $subscriptionId)
    ->notify(new InvoiceReady($invoice));

Note the key: when via() returns the channel class name, the route key must match that string exactly — 'onesignal' won't be picked up. This is a frequent silent no-op.

Rich payloads: buttons, images, and deep links are all message-level.

OneSignalMessage::create()
    ->setSubject('Approve expense?')
    ->setBody("€{$expense->amount} from {$expense->user->name}")
    ->addButton(OneSignalButton::create('approve')->label('Approve'))
    ->addButton(OneSignalButton::create('reject')->label('Reject'))
    ->setBigPicture($expense->receipt_url)
    ->setData(['expense_id' => $expense->id]);

Silent / data-only push: setContentAvailable(true) with no body. The value here is waking the app to refresh state, not showing anything — pair it with a short TTL so stale wakers don't pile up.

Scheduling: pick one clock. Either let OneSignal hold the message (send_after) or let Laravel hold it (->delay() on a queued notification). Doing both means two retry/skip semantics fighting each other, and cancelling becomes impossible.

Testing. Notification::fake() covers the decision layer:

Notification::fake();
$user->notify(new OrderShipped($order));
Notification::assertSentTo($user, OrderShipped::class, function ($n, $channels) {
    return in_array(OneSignalChannel::class, $channels);
});

To test the message, call $notification->toOneSignal($user)->toArray() directly and assert on the payload — fast, no HTTP. To test the transport, bind a fake OneSignalClient in the container or inject a Guzzle MockHandler. Http::fake() will not intercept anything — the package talks to Guzzle directly, not Laravel's HTTP client. If you want Laravel-native faking, that's your strongest argument for wrapping the client yourself.

Integration tips. Keep the player-ID column on the user (or a devices table with user_id, subscription_id, platform, last_seen_at) rather than in a JSON blob — you'll want to delete rows on logout and on 410/recipients: 0. Dispatch notifications from queued listeners, never inline in a request that a mobile client is waiting on.

Gotchas and Tips

HTTP 200 does not mean delivered. OneSignal returns 200 with {"id":"","recipients":0,"errors":["All included players are not subscribed"]} for bad IDs. The package may hand you back a successful-looking response. Log recipients and errors on every send in staging; in production, treat recipients === 0 as a data-quality alarm.

routeNotificationForOneSignal() returning null/empty is a silent skip. The channel bails without throwing. If pushes "just don't arrive" for some users, log the route value before notify(). This one costs teams half a day, repeatedly.

Player/subscription IDs churn. They change on reinstall, restore-from-backup, and OS-level resets, and they're per-device. Either store them aggressively (update on every app launch) or move to external user IDs / aliases, which is where OneSignal's API has been heading. If you pin an old version of this package, check whether it still emits include_player_ids — that endpoint has been deprecated in OneSignal's newer API surface, and the package has had to track that.

Config caching bites. services.onesignal.* is read at container-build time. After changing env values on a server: php artisan config:clear && php artisan config:cache. A stale cached config produces "401 Unauthorized" that looks like a wrong key.

The REST API Key is not the App ID, and not the user-auth key. Mixing these up is the #1 first-day error. Also: keys are per-app, so multi-tenant setups need either multiple clients bound in the container or a runtime config swap before dispatch — and a runtime swap is not concurrency-safe under Octane. Prefer constructing the client yourself for tenant #2.

Platform setup is entirely in the dashboard. APNs certificates/keys, FCM credentials, web push VAPID keys — none of this is in the package, and a perfectly-formed Laravel notification will fail with recipients: 0 until the dashboard is green. Verify with OneSignal's own "Send test message" before blaming your code.

Payload limits are real. Custom data is capped (~4KB) and titles/bodies truncate on lockscreens. Don't ship image URLs inside data if you can use the dedicated picture setters — the OS renders those for free. And data is not a secret channel: anything you put there is readable on the device. Never send tokens, PII beyond what the lockscreen shows, or anything the user shouldn't see.

Queued notifications need ShouldQueue on the notification, not on the channel. The channel is synchronous. Combined with OneSignal's per-app rate limits, a broadcast to 50k users from a single job will throttle or time out — chunk it (Notification::send($chunk)) or use OneSignal's own segment API and skip Laravel entirely for true broadcasts. Knowing when not to use the channel is part of using it well.

Extension points are shallow, and that's fine. There's no events, no macros, no middleware. Your options are: (a) bind a subclass of OneSignalClient in the container for logging/retries, (b) extend OneSignalChannel and override send(), or (c) wrap the whole thing in your own PushGateway service and treat the package as an implementation detail. Pick (c) for anything long-lived — with zero dependents and a small star count, you want a seam that lets you swap providers without touching 40 notification classes.

Pin the version and read the changelog before upgrading. The package tracks OneSignal's API migrations (player IDs → aliases, v1 → v2 endpoints), so minor bumps have historically carried payload-shape changes. composer require laravel-notification-channels/onesignal:^X.Y with a note in the upgrade runbook, plus a single integration test that asserts your real payload reaches a staging app, is cheap insurance.

Verdict for day-to-day use: excellent when OneSignal is already your push vendor and you want transactional push in ~20 lines. Thin, predictable, low-magic. Weak as a general abstraction — no queueing, no retries, no observability, an awkward test story — so wrap it, log the delivery metrics yourself, and don't expect it to grow features you'd get from a first-party integration.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource