# Notification Hooks

https://dev.wisecp.com/es/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

gatenotification.dispatch

`Notification::dispatch()` a different return contract

Runs before the notification is built. Stopping it means the message reaches **nobody**.

Parameters 3

$groupstringThe notification group: invoice, service, order, domain, tickets.

$namestringThe notification key: `invoice-created`, `welcome` and the like.

$contextarrayThe raw context: the record ids involved, extra parameters, manually added recipients, forced channels.

Return 1

mixed**Any non-empty return** stops the dispatch — even `true`. Unlike the other gates there is **no text requirement** here; returning a value by accident cuts the notification quietly.

Listener PHP

```php
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

filternotification.recipients

`Notification` a row per channel

Runs after the recipients were resolved, before the messages are built.

Parameters 3

$recipientsarrayrefThe recipient rows: channel, account, address, name, cc, language. One person can appear on **several rows**: e-mail and SMS are separate.

$groupstringThe notification group: invoice, service, order, domain, tickets.

$namestringThe notification key: `invoice-created`, `welcome` and the like.

Return 1

voidThe value changes **by reference**; the return is not read.

Listener PHP

```php
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

filternotification.render_variables

`Notification` runs per recipient

Runs before the message text is produced, **separately for each recipient**.

Parameters 4

$variablesarrayrefThe variable set for this recipient. The placeholders in the template fill from here.

$groupstringThe notification group: invoice, service, order, domain, tickets.

$namestringThe notification key: `invoice-created`, `welcome` and the like.

$recipientarrayThe recipient in hand: channel, language, account, address. The language field matters: produce your text in theirs.

Return 1

voidThe value changes **by reference**; the return is not read.

Listener PHP

```php
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

gatenotification.deliver

`Notification` the message is ready

Runs immediately before a built message goes out. Stopping it cuts **this delivery alone**.

Parameters 1

$itemarrayThe item about to be delivered: channel, account, address, name, cc, subject, body, attachments, reason. The text and attachments are **ready**: this is where a last check belongs.

Return 1

stringA non-empty string stops **this delivery**; the other recipients still get theirs.

Listener PHP

```php
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

actionnotification.dispatched

`Notification` one array parameter

Runs after the notification went out.

Parameters 1

$payloadarrayA single array: group, name, account, variables, batch id. The batch id ties **every message** of one dispatch together.

Return 1

voidThe return is ignored.

Listener PHP

```php
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

filternotification.template_merge_fields

`AdminNotifications` the list shown to the operator

Runs while the available fields are listed on the template editing screen.

Parameters 1

$fieldsarrayrefThe field list shown to the operator. Adding one here **shows it in the list**; producing its value wants the variables filter too.

Return 1

voidThe value changes **by reference**; the return is not read.

Listener PHP

```php
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

filternotification.invoice_pdf_name

`Notification` the file name

Runs while the invoice document is attached to a notification.

Parameters 1

$namestringrefThe attachment's file name. The name on the customer's machine, so keep to **safe characters**.

Return 1

voidThe value changes **by reference**; the return is not read.

Listener PHP

```php
Hook::add('filter:notification.invoice_pdf_name', 10, function (&$name) {
    $name = Acme::safeFileName($name);
});
```

### Changing a template label

filternotification.template_label

`AdminNotifications` it shows in the panel

Runs while a notification template's name in the panel is resolved.

Parameters 1

$labelstringrefThe template's display name. Only the operator sees it; it does not touch the text reaching a customer.

Return 1

voidThe value changes **by reference**; the return is not read.

Listener PHP

```php
Hook::add('filter:notification.template_label', 10, function (&$label) {
    $label = Acme::prefixLabel($label);
});
```

### Changing the invoice lines

filternotification.invoice_items

`Notification::build_items` changes by returning

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.

Parameters 3

$resultarrayThe lines prepared for the template. This is what you change.

$invoicearrayThe invoice itself.

$itemsarrayThe raw lines as they came from the database. Use this to recover a field the prepared list dropped.

Return 1

array|nullAn array you return **replaces the whole list**. The trap: with several listeners the last return wins and earlier additions vanish. To add, take the incoming list and build on it.

Listener PHP

```php
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

filternotification.order_items

`Notification::build_order_items` changes by returning

The same job for order notifications. Lines arrive already split by quantity, with live add-ons loaded.

Parameters 2

$itemsarrayThe lines prepared for the template.

$orderarrayThe order itself, raw line data included.

Return 1

array|nullAn array replaces the whole list; anything else is ignored.

Listener PHP

```php
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

filternotification.log_entry

`LogManager` passed by link personal data

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.

Parameters 1

$entryarrayby linkThe record about to be written. Shared fields: `channel` (mail or sms), `user_id`, `reason`, `content`, `data`, `private`. Email also carries `subject`.

Return 1

voidThe return is ignored; you change the array in place. Set `$entry['abort'] = true` and **nothing is written**; the counter comes back zero. The message still goes out, only the trace is dropped.

Listener PHP

```php
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

filternotification.preview_variables

`AdminNotifications` passed by link

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.

Parameters 2

$variablesarrayby linkWhat the template will receive, name against value. Add your own fields and leave the existing ones alone.

$ctxarrayby linkWhich template is on screen: `group`, `template`, `lang`. Leave without touching anything when it is not yours.

Return 1

voidThe return is ignored; you add values by writing into the array.

Listener PHP

```php
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 dispatch gate reads any return as a block**
> 
> 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.

> **One person appears more than once**
> 
> 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**.

> **The variables filter runs once per recipient**
> 
> 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.

> **Listing a field produces no value**
> 
> 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](https://dev.wisecp.com/en/customer-account-hooks)
- [Invoice Lifecycle Hooks](https://dev.wisecp.com/en/invoice-lifecycle-hooks)
