Notification Hooks
The eight hooks behind every message reaching a customer: the dispatch gate, the recipient list, the variables and delivery.
Overview
A notification passes two gates. The first stops the dispatch itself: it reaches nobody. The second stops one delivery: the other recipients still get the message.
Between them sit the recipient list and the variables. The variables filter runs per recipient, so one notification can reach each person with different content.
Reference
Stopping the whole dispatch
Runs before the notification is built. Stopping it means the message reaches nobody.
invoice-created, welcome and the like.true. Unlike the other gates there is no text requirement here; returning a value by accident cuts the notification quietly.Hook::add('gate:notification.dispatch', 10, function ($group, $name, $context) {
// MIND: ANY non-empty return stops the dispatch, even true.
if ($name === 'invoice-reminder' && Acme::quietHours()) return true;
return null;
});Changing the recipient list
Runs after the recipients were resolved, before the messages are built.
invoice-created, welcome and the like.Hook::add('filter:notification.recipients', 10,
function (&$recipients, $group, $name) {
// One person has SEPARATE rows for e-mail and SMS: filter by channel.
if ($name === 'invoice-created')
$recipients = array_filter($recipients,
fn ($r) => ($r['channel'] ?? '') !== 'sms');
});Changing the template variables
Runs before the message text is produced, separately for each recipient.
invoice-created, welcome and the like.Hook::add('filter:notification.render_variables', 10,
function (&$variables, $group, $name, $recipient) {
// It runs PER RECIPIENT: produce your text in their language.
$variables['acme_note'] = Acme::note($recipient['lang'] ?? 'en');
});Stopping a single delivery
Runs immediately before a built message goes out. Stopping it cuts this delivery alone.
Hook::add('gate:notification.deliver', 10, function ($item) {
// It stops THIS delivery alone: the others carry on.
if (Acme::bounced($item['recipient'] ?? '')) return 'the address is dead';
return null;
});Following a dispatch
Runs after the notification went out.
Hook::add('action:notification.dispatched', 10, function ($payload) {
// The batch id ties every message of one dispatch together.
Acme::trackBatch($payload['batch_id'] ?? '', $payload['name'] ?? '');
});Widening the template fields
Runs while the available fields are listed on the template editing screen.
Hook::add('filter:notification.template_merge_fields', 10, function (&$fields) {
// Adding to the list produces NO value: write the variables filter as well.
$fields['acme_note'] = 'Acme note';
});Changing the invoice attachment name
Runs while the invoice document is attached to a notification.
Hook::add('filter:notification.invoice_pdf_name', 10, function (&$name) {
$name = Acme::safeFileName($name);
});Changing a template label
Runs while a notification template's name in the panel is resolved.
Hook::add('filter:notification.template_label', 10, function (&$label) {
$label = Acme::prefixLabel($label);
});Changing the invoice lines
Runs before the invoice lines reach the template. You can add lines, drop them, reorder them or put a field of your own on each one.
Hook::add('filter:notification.invoice_items', 10, function ($result, $invoice, $items) {
// Keep the incoming list: building from scratch erases what others added.
foreach ($result as $i => $line)
$result[$i]['acme_note'] = Acme::noteFor($line['id'] ?? 0);
return $result;
});Changing the order lines
The same job for order notifications. Lines arrive already split by quantity, with live add-ons loaded.
Hook::add('filter:notification.order_items', 10, function ($items, $order) {
// Hide internal lines from the customer.
return array_values(array_filter($items, fn ($x) => ($x['name'] ?? '') !== 'internal'));
});Masking the delivery record
Runs right before an email or SMS record is written to the database. This is where you mask personal data, or keep no record at all.
channel (mail or sms), user_id, reason, content, data, private. Email also carries subject.$entry['abort'] = true and nothing is written; the counter comes back zero. The message still goes out, only the trace is dropped.Hook::add('filter:notification.log_entry', 10, function (&$entry) {
// Never store the body of a password reset.
if (($entry['reason'] ?? '') === 'password-reset') {
$entry['content'] = '[masked]';
return;
}
// Open no record at all for campaign texts.
if (($entry['channel'] ?? '') === 'sms' && ($entry['reason'] ?? '') === 'campaign')
$entry['abort'] = true;
});Giving the preview sample values
Runs while a template is previewed in the panel. The preview sends nothing and knows only the templates that ship with the core, so fields of a template you added come out empty. Supply sample values here.
group, template, lang. Leave without touching anything when it is not yours.Hook::add('filter:notification.preview_variables', 10, function (&$variables, &$ctx) {
// Care only about your own template.
if (($ctx['template'] ?? '') !== 'acme-welcome') return;
$variables = array_merge($variables, [
'acme_plan' => 'Starter',
'acme_quota' => '10 GB',
]);
});Pitfalls
The other gates want a non-empty string; this one reads any non-empty value as a block. Returning something by accident at the end of your closure — even true — means the notification never goes, with no trace anywhere.
The recipient list carries a row per channel: a customer on both e-mail and SMS sits on two rows. A listener deduplicating by account id quietly switches off a whole channel.
On a notification reaching ten people this filter runs ten times. A query or a remote call inside it turns one notification into ten requests. Prepare the shared data once and produce only the per-recipient part inside.
The template fields filter widens the list shown to the operator and nothing else. Add a field without writing the variables filter and the operator puts it in a template, where it reaches the customer empty. The two are written together.
Related Articles
- Knowledge Base and Notification Hooks
- Customer Account Hooks
- Invoice Lifecycle Hooks
Vielen Dank für Ihre Rückmeldung!
Unser Support-Team ist rund um die Uhr für Sie da, wenn Sie oben nicht fündig werden.