If you run a hosting or SaaS business on WHMCS, you've probably seen this happen: invoices go out by email, they land in Promotions or spam, and the customer only notices when the service is suspended. In markets like India, Brazil and Indonesia, people check WhatsApp far more often than email. Sending the important WHMCS events there (new invoice, payment received, ticket reply, suspension warning) cuts down on late payments and "I never got the email" tickets.
This guide shows how to build it yourself with WHMCS hooks and the official WhatsApp Business Platform (Cloud API). At the end I also cover when a ready-made module makes more sense.
What you need
- WHMCS 8.x with access to the
includes/hooks/folder - A Meta Business account with a WhatsApp Business Platform app, a phone number registered to it, and a permanent access token (a System User token, not the 24-hour test token)
- The Phone Number ID from the WhatsApp > API Setup page
- PHP with cURL enabled (WHMCS already requires this)
Step 1: Understand the 24-hour rule (and create templates)
WhatsApp splits messages into two kinds:
- Session messages: free-form text, allowed only within 24 hours of the customer last messaging you.
- Template messages: pre-approved by Meta and allowed at any time.
Invoice and suspension notices are business-initiated, so almost all of them must be templates. In WhatsApp Manager, create a "Utility" template such as:
Name: invoice_created
Body:
Hi {{1}}, invoice #{{2}} for {{3}} has been generated and is due on {{4}}. Pay here: {{5}}
Submit it and wait for approval, which usually takes minutes to a few hours. Make similar templates for payment_received, ticket_reply and service_suspended. Keep the wording factual. Utility templates that read like marketing get re-categorised or rejected.
Step 2: Store credentials safely
Don't hard-code the token in the hook file. The simplest safe option is a file outside the web root, or environment variables set in your PHP-FPM pool:
; /etc/php/8.1/fpm/pool.d/whmcs.conf
env[WA_TOKEN] = "EAAG...your-system-user-token"
env[WA_PHONE_ID] = "123456789012345"
Reload PHP-FPM after editing.
Step 3: Write a small sender function
Create includes/hooks/whatsapp_notify.php:
<?php
if (!defined('WHMCS')) {
die('No direct access');
}
use WHMCS\Database\Capsule;
function wa_normalise_phone(array $client): ?string
{
// WHMCS formats this like "+91.9876543210"
$raw = $client['phonenumberformatted'] ?? '';
$digits = preg_replace('/\D+/', '', $raw);
return strlen($digits) >= 10 ? $digits : null;
}
function wa_send_template(string $to, string $template, array $params, string $lang = 'en'): bool
{
$token = getenv('WA_TOKEN');
$phoneId = getenv('WA_PHONE_ID');
if (!$token || !$phoneId) {
logActivity('WhatsApp: credentials missing');
return false;
}
$payload = [
'messaging_product' => 'whatsapp',
'to' => $to,
'type' => 'template',
'template' => [
'name' => $template,
'language' => ['code' => $lang],
'components' => [[
'type' => 'body',
'parameters' => array_map(
fn($v) => ['type' => 'text', 'text' => (string) $v],
$params
),
]],
],
];
$ch = curl_init("https://graph.facebook.com/v20.0/{$phoneId}/messages");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer {$token}",
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode($payload),
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
logModuleCall('whatsapp', $template, $payload, $response);
return $status === 200;
}
Notes:
- The number must be in international format without the
+, for example919876543210. -
logModuleCallwrites to Utilities > Logs > Module Log (turn module logging on while testing). That's where you'll see Meta's error messages. - Check which Graph API version is current in Meta's docs and update
v20.0if needed.
Step 4: Hook into WHMCS events
Add the hooks below the functions in the same file.
New invoice
add_hook('InvoiceCreated', 1, function ($vars) {
$invoice = localAPI('GetInvoice', ['invoiceid' => $vars['invoiceid']]);
if (($invoice['result'] ?? '') !== 'success' || (float) $invoice['total'] <= 0) {
return;
}
$client = localAPI('GetClientsDetails', ['clientid' => $invoice['userid']]);
$phone = wa_normalise_phone($client);
if (!$phone) {
return;
}
$systemUrl = rtrim(\WHMCS\Config\Setting::getValue('SystemURL'), '/');
wa_send_template($phone, 'invoice_created', [
$client['firstname'],
$invoice['invoicenum'] ?: $invoice['invoiceid'],
$invoice['currencyprefix'] . $invoice['total'],
fromMySQLDate($invoice['duedate']),
"{$systemUrl}/viewinvoice.php?id={$invoice['invoiceid']}",
]);
});
Payment received
add_hook('InvoicePaid', 1, function ($vars) {
$invoice = localAPI('GetInvoice', ['invoiceid' => $vars['invoiceid']]);
$client = localAPI('GetClientsDetails', ['clientid' => $invoice['userid']]);
if ($phone = wa_normalise_phone($client)) {
wa_send_template($phone, 'payment_received', [
$client['firstname'],
$invoice['invoicenum'] ?: $invoice['invoiceid'],
]);
}
});
Staff replied to a ticket
add_hook('TicketAdminReply', 1, function ($vars) {
$ticket = Capsule::table('tbltickets')->where('id', $vars['ticketid'])->first();
if (!$ticket || !$ticket->userid) {
return; // guest tickets have no client record
}
$client = localAPI('GetClientsDetails', ['clientid' => $ticket->userid]);
if ($phone = wa_normalise_phone($client)) {
wa_send_template($phone, 'ticket_reply', [
$client['firstname'],
$ticket->tid,
]);
}
});
You can use the same pattern for AfterModuleSuspend (suspension notice) and AfterModuleCreate (welcome message with the login URL; never send passwords over WhatsApp).
Step 5: Respect opt-in
WhatsApp's policy requires customer opt-in before you send business-initiated messages. An easy way to handle it in WHMCS:
- Create a client custom field (Setup > Custom Fields > Client Custom Fields) of type tick box, named "WhatsApp notifications", and show it on the order form.
- Before sending, check it:
function wa_opted_in(int $clientId): bool
{
$field = Capsule::table('tblcustomfields')
->where('type', 'client')
->where('fieldname', 'WhatsApp notifications')
->first();
if (!$field) {
return false;
}
$value = Capsule::table('tblcustomfieldsvalues')
->where('fieldid', $field->id)
->where('relid', $clientId)
->value('value');
return $value === 'on';
}
Call wa_opted_in($invoice['userid']) at the top of each hook. Customers who have opted in get messages, and everyone else keeps getting email only. This also protects your number's quality rating. Numbers that get blocked or reported have their sending limits cut.
Step 6: Test
- Add your own number to a test client, and tick the opt-in box.
- Create a manual invoice under Billing > Create Invoice.
- Check WhatsApp, then check the Module Log if nothing arrives.
Common errors:
| Error | Cause |
|---|---|
(#132001) Template name does not exist |
Name or language code doesn't match the approved template |
(#131030) Recipient not in allowed list |
You're still using the test number. Add the recipient or go live |
(#190) Access token has expired |
You used the temporary token. Switch to a System User token |
(#132000) Number of parameters does not match |
The {{n}} count differs from your $params array |
Step 7: Production hardening
- Don't block checkout. The 10-second timeout keeps a slow API from hanging WHMCS. For high volume, write messages to a queue table and send them from a cron job instead.
-
Avoid duplicates.
InvoiceCreatedfires once, but custom scripts can re-trigger events, so store a sent flag per invoice if you see repeats. - Monitor quality. WhatsApp Manager shows your number's quality rating. If it drops, tighten the opt-in or the wording.
Build it or use a module?
The hook approach above is about 150 lines and works well if you're comfortable maintaining PHP and keeping up with Meta's API changes. If you'd rather have an admin UI for templates, per-event toggles and delivery logs without touching code, there are ready-made options. For example, I work on the WHMCS WhatsApp Module at ShoHost, which wraps these same events in a settings page for $2/month. Whichever route you take, the key points are the same: approved templates, real opt-in, and good logging.
Have you wired WhatsApp into WHMCS a different way? Let me know in the comments which events your customers find most useful.
Top comments (0)