Customer Service and Message Hooks
The thirteen hooks over the service detail and bulk messaging: the module panel, add-ons, package changes, the usage chart and sending.
Overview
What a customer sees on the service detail, and bulk message sending, live here.
The service filters share a pattern around visibility flags: hiding a section and emptying it give different outcomes. On the message side everything is counted in parts; the recipient count does not give you the cost.
Reference
Changing the module panel
Runs before the panel produced by the server module is shown to the customer. It is the only place to touch the output of the remote panel.
Hook::add('filter:client.service_management_content', 10, function (&$content, $ctx) {
// The output is shown UNESCAPED.
if (($ctx['page'] ?? '') !== 'dashboard') return;
$content .= '<div class="alert alert-info">Acme</div>';
});Changing the add-on lists
Runs once the add-on lists on the service detail are prepared. Two separate lists arrive: the ones owned and the ones on offer.
Hook::add('filter:client.service_detail.addons', 10,
function (&$addonsOwned, &$addonsAvailable, $ctx) {
// Removing an offer also blocks the purchase.
$addonsAvailable = array_values(array_filter($addonsAvailable,
fn ($a) => Acme::offerAllowed($a, $ctx['service'] ?? [])));
});Changing the package change catalogue
Runs once the package upgrade and downgrade options are prepared.
Hook::add('filter:client.service_detail.upgrade_plans', 10, function (&$updown, $ctx) {
// Hiding and emptying are different outcomes.
if (Acme::locked($ctx['service'] ?? [])) $updown['visible'] = false;
});Changing the licence transfer section
Runs once the transfer section of a software licence is prepared.
Hook::add('filter:client.service_detail.license_transfer', 10, function (&$lt, $ctx) {
// Do not show the section at all for a licence that cannot move.
if (Acme::nonTransferable($ctx['service'] ?? [])) $lt['visible'] = false;
});Changing the service timeline
Runs before the history rows of a service reach the screen. The place to put your own events among the core ones.
Hook::add('filter:client.service_detail.activity', 10, function (&$activity, $ctx) {
foreach (Acme::events((int) ($ctx['service_id'] ?? 0)) as $e) $activity[] = $e;
});Changing the usage chart
Runs once the data of the usage chart is prepared.
Hook::add('filter:client.service_metric_chart', 10, function (&$chart, $ctx) {
// A day with no data arrives EMPTY: do not turn it into a zero.
$chart['acme_limit'] = Acme::planLimit($ctx['metric'] ?? '');
});Stopping a message being sent
Runs before a customer sends messages in bulk. The cost is worked out but not yet charged.
Hook::add('gate:client.sms_send', 10, function ($uid, $origin, $quote, $sendCtx) {
// The cost is worked out but NOT yet charged.
if ((int) ($quote['recipients'] ?? 0) > Acme::dailyCap($uid))
return 'This exceeds your daily sending limit.';
return null;
});Following messages being sent
Runs after messages go out in bulk.
Hook::add('action:client.sms_sent', 10, function ($uid, $origin, $quote, $sentCtx) {
// The charge follows the PARTS, not the recipient count.
Acme::recordUsage($uid, (int) ($quote['total_parts'] ?? 0));
});Stopping a sender identity being added
Runs before a customer adds a new sender identity.
Hook::add('gate:client.sms_sender_add', 10, function ($uid, $name) {
// The format is already checked: look at the content.
if (Acme::reservedBrand($name)) return 'This name cannot be used.';
return null;
});Following a sender identity being added
Runs after a new sender identity is added. Being added does not mean it can be used: some countries need a separate application.
Hook::add('action:client.sms_sender_created', 10, function ($uid, $origin) {
// Being added does not mean it can be used.
Acme::noteSender($uid, $origin['name'] ?? '');
});Following a sender application
Runs after an application is made to pre-register a sender identity.
Hook::add('action:client.sms_sender_requested', 10, function ($uid, $origin, $codes) {
// It always arrives as a list.
foreach ($codes as $iso) Acme::trackApplication($uid, $origin['name'] ?? '', $iso);
});Following contacts being imported
Runs after a customer imports a contact list.
Hook::add('action:client.sms_contacts_imported', 10,
function ($uid, $imported, $skipped, $group_id) {
// The header row is not counted among the skipped.
if ($skipped > 0) Acme::warnImportQuality($uid, $imported, $skipped);
});Changing the country price table
Runs once the country prices shown to the customer are prepared.
Hook::add('filter:client.sms_rate', 10, function (&$out, $ctx) {
// The fee is PER PART.
foreach ($out as $iso => $row)
$out[$iso]['rate'] = Acme::applyMargin((float) ($row['rate'] ?? 0));
});Pitfalls
A day with no data collected arrives empty in the usage chart. Turning it into a zero draws a drop that never happened and shows the customer a false picture of their usage. Leave the gap as a gap.
A long message splits into several parts and each part is charged. A calculation treating the recipient count as the cost falls far short on long messages. Use the part count from the send summary.
Related Articles
Thanks for your feedback!
Our support team is here around the clock for anything you can't find above.