---
title: "Other packages’ notifications — Notifications for Laravel | Roundly"
description: "Map other packages’ notification classes to your types so preferences, de-dup, quiet hours and the inbox apply — or suppress them — with fail-open safety."
url: https://roundly-consulting.com/open-source/docs/notifications-for-laravel/interop
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Notifications for Laravel](https://roundly-consulting.com/open-source/docs/notifications-for-laravel.md)

# Other packages’ notifications

Map a notification class another package sends to one of your types, and the package’s rules apply to it — preferences, mandatory flags, de-dup, quiet hours and the inbox. Or suppress it entirely:

```php
// config/notifications.php
'interop' => [
    'map' => [
        RoundlyConsulting\Tickets\Notifications\TicketAssignedNotification::class => 'tickets.assigned',
        RoundlyConsulting\Events\Notifications\EventReminderNotification::class => 'events.reminder',
    ],
    'suppress' => [
        RoundlyConsulting\Events\Notifications\EventUpdatedNotification::class,             // every channel
        RoundlyConsulting\Tickets\Notifications\TicketNoteAddedNotification::class => ['mail'],
    ],
    'database' => 'redirect',   // redirect | keep | both
    'hold' => true,             // hold mapped mail in quiet hours
    'notifiables' => [],        // extra recipient classes
],
```

Or at runtime:

```php
Notifications::interop()->map(TicketAssignedNotification::class, 'tickets.assigned');
Notifications::interop()->suppress(TicketNoteAddedNotification::class, ['mail']);
Notifications::interop()->mapped();       // class => type key
Notifications::interop()->suppressed();
```

The mapped type describes the notification; subjectFrom() tells the package what it is about:

```php
final class TicketAssigned extends NotificationType
{
    public function key(): string { return 'tickets.assigned'; }
    public function category(): string { return 'tickets'; }
    public function subject(): ?string { return Ticket::class; }
    public function subjectFrom(Notification $notification): ?Model { return $notification->ticket; }
    public function window(): ?string { return 'PT15M'; }
    public function title(NotificationPayload $p): string { return __('app.ticket_assigned'); }  // when the notification's data has no "title"
}
```

## How it works

- A NotificationSending listener, asked per notifiable and per channel, after the notification’s own shouldSend(), at delivery time (on the worker for queued notifications). It cancels a channel or lets it through.
- Matching uses is\_a(): a host subclass of a mapped class is mapped; an exact entry beats an ancestor, and the most specific ancestor wins.
- Only recipients — models using ReceivesNotifications, or a class in interop.notifiables — get preferences, de-dup, holds and the inbox redirect. Any other notifiable (a guest, a contact, an on-demand route) gets suppress only.
- Fail-open: apart from suppress, any exception in the listener is reported, NotificationInteropFailed fires, and the notification goes out untouched. An interop bug must never stop a password reset.
- De-dup is claim-first: two workers delivering two mapped notifications of one key at once send one mail.
- A held notification is stored encrypted and released by the sweep after re-checking access and the preference; a tampered or undecryptable payload fails as payload and is never unserialized. A notification that cannot be serialized is not held — it goes out at once.
- Mapped notifications are never digested; the doctor warns about a digestible mapped type. The package’s own mails are never touched.
- Laravel’s Notification::fake() dispatches no NotificationSending, so interop does not run under it.

## Decision table (per notifiable × channel)

| Case | Result |
| --- | --- |
| The class is suppressed (all channels, or this one) | cancelled |
| Not mapped; not a recipient or an on-demand route; a channel other than mail / database | untouched |
| database, redirect (default) | An inbox item through the collapse step (title = the notification’s toDatabase()/toArray() title, else the type title); Laravel’s row cancelled. |
| database, keep / both | Untouched / inbox item + untouched. |
| mail, the subject (subjectFrom(), fresh) is not accessible | cancelled |
| mail, the type has a subject class but subjectFrom() returns null | Preferences and quiet hours apply; no access check, no de-dup (the doctor warns). |
| mail, the preference is off (not mandatory) | cancelled |
| mail, an open delivery in the window | Folded (count++), cancelled. |
| mail, quiet now, type not urgent, interop.hold on | Held (encrypted payload), cancelled now, released after quiet hours. |
| Otherwise | A sending delivery is claimed; Laravel sends; NotificationSent → sent, NotificationFailed → failed(transport). |

## Time-critical guards

Anything that is\_a auth-for-laravel’s AuthenticationNotification (OTPs, magic links, resets), Laravel’s ResetPassword or VerifyEmail may only map to a type that is urgent, has no window and is not digestible — otherwise a second code requested within the window would fold into the first and never arrive. Such a mapping is refused with InvalidInteropMappingException. The map is validated once every provider has booted (your types may be defined in AppServiceProvider::boot()), again lazily on first use, and by the doctor.

## Ready map for Roundly packages

Suggested types for the Roundly packages’ notifications — none is auto-mapped, opt in per class. Class names are relative to RoundlyConsulting\\<Package>\\Notifications\\ and end in Notification:

| Package | Classes | Suggested key | Priority | subjectFrom() |
| --- | --- | --- | --- | --- |
| `tickets` | TicketCreated, TicketAssigned, TicketReplied, TicketRequesterReplied, TicketNoteAdded, TicketMentioned, TicketStatusChanged, TicketMerged | `tickets.created …` | normal | `$n->ticket` |
| `tickets` | TicketDueSoon, TicketOverdue | `tickets.due_soon, tickets.overdue` | normal / urgent | `$n->ticket` |
| `events` | EventInvitation, RsvpConfirmed, WaitlistPromoted, EventUpdated, EventPostponed, EventRescheduled, EventMovedOnline, EventCancelled | `events.invited …` | normal (cancelled: urgent) | `$n->event` |
| `events` | EventReminder | `events.reminder` | normal | `$n->event` |
| `messages` | NewMessage | `messages.new` | normal (window PT10M) | `$n->message->thread` |
| `alerts` | HealthCheckFailed | `alerts.health_check_failed` | urgent | — |
| `auth` | any AuthenticationNotification (EmailOtp, MagicLink, ResetPassword, …) | `auth.*` | urgent, mandatory, no window | — |

## Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

[More ways to support, including crypto](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/notifications-for-laravel.md)
