---
title: "Notifications — Tickets for Laravel | Roundly"
description: "Ten queued, after-commit notifications in English and Slovak — recipients, visibility re-checks, payloads, swapping classes and other channels."
url: https://roundly-consulting.com/open-source/docs/tickets-for-laravel/notifications
language: en
---

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

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

# Notifications

Ten notification types, on by default (tickets.notifications.enabled) and sent on tickets.notifications.channels (\['mail'\]). Each is a Laravel notification that is ShouldQueue and afterCommit: nothing is sent for a change that rolled back, and the mail is built when the queue delivers it. Plain MailMessage (no views) plus a toArray() for the database channel; every string comes from tickets::notifications in English and Slovak.

## Types and recipients

| Type | Class | Trigger | Recipients |
| --- | --- | --- | --- |
| `created` | `TicketCreatedNotification` | `TicketCreated` | The requester — a receipt with the reference, even when they filed it themselves. |
| `assigned` | `TicketAssignedNotification` | `TicketAssigned` | Whoever the assignee names: Assignee::ticketNotifiables() (a team’s members). |
| `replied` | `TicketRepliedNotification` | a published public reply by an agent or the system | The requester plus the watchers who are not agents for the ticket. |
| `requester_replied` | `TicketRequesterRepliedNotification` | a published public reply by the requester or a participant | The assignee’s notifiables plus the agents watching (the agent scope applies). |
| `note_added` | `TicketNoteAddedNotification` | `TicketNoteAdded` | The assignee’s notifiables plus the agents watching — agents only. |
| `mentioned` | `TicketMentionedNotification` | `TicketMentioned` | The mentioned person, if the message’s own notification does not already reach them. |
| `status_changed` | `TicketStatusChangedNotification` | `TicketResolved, TicketClosed, TicketReopened` | The requester plus every watcher. |
| `overdue` | `TicketOverdueNotification` | TicketOverdue (the sweep) | The assignee’s notifiables — nobody for an unassigned ticket. |
| `due_soon` | `TicketDueSoonNotification` | TicketDueSoon (the sweep) | The assignee’s notifiables. |
| `merged` | `TicketMergedNotification` | `TicketMerged` | The source’s requester. |

- A merge’s close sends merged, not status\_changed. An automation transition carries no actor, so the requester whose reply reopened a ticket gets reopened too.
- A reply held by the host’s moderation, and an internal note’s content, never reach a non-agent.
- No notification goes out for GuestAccessRevoked, and none to agents on created — listen to TicketCreated for a team inbox.

Every recipient list is:

- Visibility-checked — no one gets what they cannot see. Internal content reaches only agents the agent scope lets work on the ticket; anything else only someone who may view the ticket.
- Re-checked on delivery: a watcher removed meanwhile, an agent the scope now excludes, a deleted ticket, a message moved off the ticket — nothing is sent. A due-date notification is dropped when the due date moved or the ticket was resolved.
- Deduplicated — one notification per person, whatever roles they hold.
- Without the one who acted, unless tickets.notifications.notify\_actor (the created receipt always goes out); without guests, unless tickets.notifications.guests (on); notifiable only — a model without routeNotificationFor() is skipped.

## The mail

The subject reads \[TCK-42\] New reply: Printer on floor 3 is jammed, followed by a greeting with the recipient’s display name, the type’s lines and — for messages — the message text. What people wrote is escaped and shown as text, never rendered as Markdown. A guest’s button is a freshly issued signed guest link (none when no link can be issued, or when closing revoked the ticket’s links); anyone else gets tickets.notifications.url with {reference} filled in — blank means no button, because the package is headless. Dates are formatted in the rendering locale and the app’s time zone.

## Payloads

toArray() — the database channel — always carries type, ticket (the reference) and subject, plus:

| Type | Adds |
| --- | --- |
| `assigned` | assignee (display name) |
| `replied, requester_replied, note_added, mentioned` | comment (id), author (display name) |
| `status_changed` | change (resolved, closed, reopened), status (the state value) |
| `overdue, due_soon` | due\_at (ISO-8601) |
| `merged` | target (the target’s reference) |

## Locale

Laravel’s locale rules apply: a notifiable implementing HasLocalePreference gets its preferredLocale(); otherwise the queue worker’s app locale. Publish tickets-translations to change a line or add a language.

## Swapping a class

Extend the packaged class (they are not final) and name the subclass:

```php
use Illuminate\Notifications\Messages\MailMessage;
use RoundlyConsulting\Tickets\Notifications\TicketRepliedNotification;

final class BrandedReplyNotification extends TicketRepliedNotification
{
    public function toMail(object $notifiable): MailMessage
    {
        return parent::toMail($notifiable)->from('help@example.com', 'Example Help Desk');
    }
}
```

```php
// config/tickets.php
'notifications' => ['types' => ['replied' => App\Notifications\BrandedReplyNotification::class, /* … */]],
```

A replacement must extend the packaged class; anything else is a configuration error. The package builds it through the container with named constructor arguments — keep these parameter names in an overridden constructor (other parameters are resolved by the container):

| Type | Arguments |
| --- | --- |
| every type | `ticket` |
| `assigned` | assignee (the assigned model) |
| `replied, requester_replied, note_added, mentioned` | comment (the comments Comment) |
| `status_changed` | change (TicketStatusChangedNotification::RESOLVED / CLOSED / REOPENED) |
| `overdue, due_soon` | dueAt (CarbonImmutable) |
| `merged` | target (the target Ticket) |

## Class hooks

Every type extends the abstract Notifications\\TicketNotification (ShouldQueue, Queueable, afterCommit(), $deleteWhenMissingModels = true, public $ticket). The four message types extend CommentNotification (public $comment, plus author() and excerpt()); the two due-date types extend DueNotification (public $dueAt, plus due()). Override shouldSend() only by calling parent::shouldSend() — it holds the visibility re-check:

| Method | Visibility and use |
| --- | --- |
| `type(): NotificationType` | public, abstract — the type the class sends |
| `via(object $notifiable): array` | public — tickets.notifications.channels |
| `shouldSend(object $notifiable, string $channel): bool` | public — the visibility re-check on delivery; call the parent |
| `toMail(object $notifiable): MailMessage` | public — subject, greeting, lines(), the button from url() |
| `toArray(object $notifiable): array` | public — type, ticket, subject plus data() |
| `internal(): bool` | public — internal content, agents only; true for note\_added and mentioned |
| `subject(): string` | protected, abstract — the mail subject (\[TCK-42\] …) |
| `lines(object $notifiable): array` | protected, abstract — the mail’s lines, user-written text already escaped |
| `data(): array` | protected — what the type adds to toArray() |
| `linkedTicket(): Ticket` | protected — the ticket the button opens (merged points at the target) |
| `url(object $notifiable): ?string` | protected — a guest link for a TicketGuest, else tickets.notifications.url |
| `guestLink(Ticket $ticket, TicketGuest $guest): ?string` | protected — a freshly issued guest link, or null when none can be issued |
| `escapedSubject(): string` | protected — the ticket subject, escaped for a mail line |
| `static line(string $key, array $replace = []): string` | protected — a line of tickets::notifications |

## Recipients from your own code

Notifications\\RecipientResolver (resolve it from the container) answers who would be told: created($ticket), assigned($ticket, $assignee, $actor), replied($ticket, AuthorRole $role, $actor), noteAdded($ticket, $actor), mentioned($ticket, $comment, $mentioned), statusChanged($ticket, $actor), due($ticket) and merged($source, $actor) — each a visibility-checked, deduplicated list<Model> — and replyType(AuthorRole $role). Use it to send the same audience a Slack DM, a push message or a webhook of your own.

## Other channels

Only mail and database are built in. For any other channel, add it to notifications.channels and swap each type for a subclass that implements that channel’s method (toSlack(), toVonage(), …). A TicketGuest routes a channel through the guest field whose route names it (email → mail, phone → vonage in the shipped field set).

## Failures

A notification failure — a broken setting, an unreachable queue, a mail that throws on the sync queue — is reported through your exception handler, never thrown: the change it tells about has already committed. Notifications for a ticket deleted for good before the queue reaches them are dropped. The mentioned type needs comments.mention\_resolver.

## 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/tickets-for-laravel.md)
