WHMCS Integration
Send WhatsApp notifications from WHMCS order hooks using WaSphere.
WHMCS Integration
This guide shows you how to send WhatsApp notifications from WHMCS using WaSphere hooks. Common use cases include:
- Order confirmation messages when a client places a new order
- Invoice payment reminders
- Support ticket updates
- Service activation notifications
- Domain expiry reminders
Prerequisites
- WaSphere running with a connected session and an API key with the
messages:sendpermission - Your Workspace ID (from Settings) and Session ID (the name you gave the session, e.g.
support) - WHMCS 8.x with admin access
- PHP 8.0+ (standard with WHMCS 8)
Creating the Hook File
WHMCS hooks live in /path/to/whmcs/includes/hooks/. Create a new file called wasphere_notifications.php.
<?php
// includes/hooks/wasphere_notifications.php
/**
* WaSphere WhatsApp Notifications for WHMCS
* Sends WhatsApp messages via WaSphere API on key WHMCS events.
*/
defined('WHMCS') or die('This file cannot be accessed directly');
// ─── Configuration ───────────────────────────────────────────────────────────
// Store these in WHMCS custom configuration or use PHP constants.
// DO NOT hardcode secrets in production — use getenv() or WHMCS config storage.
// WASPHERE_API_URL is your Dashboard API base (it proxies to the WA Server).
define('WASPHERE_API_URL', getenv('WASPHERE_API_URL') ?: 'https://api.yourdomain.com');
define('WASPHERE_API_KEY', getenv('WASPHERE_API_KEY') ?: '');
define('WASPHERE_WORKSPACE_ID', getenv('WASPHERE_WORKSPACE_ID') ?: '');
define('WASPHERE_SESSION_ID', getenv('WASPHERE_SESSION_ID') ?: '');
// ─── Helper Functions ─────────────────────────────────────────────────────────
/**
* Send a WhatsApp text message via WaSphere.
*
* @param string $phoneNumber International format without +, e.g. "447700900123"
* @param string $message Message text (WhatsApp formatting supported)
* @return bool True on success, false on failure
*/
function waSphereSendMessage(string $phoneNumber, string $message): bool
{
if (empty(WASPHERE_API_KEY) || empty(WASPHERE_WORKSPACE_ID) || empty(WASPHERE_SESSION_ID)) {
logActivity('WaSphere: API key, workspace ID, or session ID not configured');
return false;
}
// Normalise phone number — strip spaces, dashes, parentheses, leading +
$to = preg_replace('/[^0-9]/', '', $phoneNumber);
if (empty($to)) {
logActivity("WaSphere: Invalid phone number: {$phoneNumber}");
return false;
}
// Requests go through the Dashboard API, which proxies to your WA Server
// and injects the server token. The session ID lives in the URL, not the body.
$url = WASPHERE_API_URL
. '/workspaces/' . rawurlencode(WASPHERE_WORKSPACE_ID)
. '/proxy/api/sessions/' . rawurlencode(WASPHERE_SESSION_ID)
. '/messages/text';
$payload = json_encode([
'to' => $to,
'text' => $message,
]);
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_TIMEOUT => 10,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . WASPHERE_API_KEY,
'Content-Type: application/json',
],
]);
$response = curl_exec($ch);
$statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
curl_close($ch);
if ($curlError) {
logActivity("WaSphere: cURL error: {$curlError}");
return false;
}
if ($statusCode < 200 || $statusCode >= 300) {
logActivity("WaSphere: API error {$statusCode}: {$response}");
return false;
}
return true;
}
/**
* Retrieve client phone number from WHMCS.
*/
function waSphereGetClientPhone(int $clientId): string
{
$client = Capsule::table('tblclients')
->where('id', $clientId)
->select('phonenumber', 'phonecc')
->first();
if (!$client || empty($client->phonenumber)) {
return '';
}
// Combine country code + number, strip non-digits
$phone = $client->phonecc . $client->phonenumber;
return preg_replace('/[^0-9]/', '', $phone);
}
/**
* Format currency amounts for display.
*/
function waSphereFormatCurrency(float $amount, string $currency = 'USD'): string
{
return number_format($amount, 2) . ' ' . $currency;
}
Hook: New Order Notification
<?php
// Add this to wasphere_notifications.php
// OrderPaid fires when an order's first invoice is marked paid.
// $vars keys are camelCase: orderId, userId, invoiceId — and there is no amount.
add_hook('OrderPaid', 1, function ($vars) {
$orderId = $vars['orderId'];
$clientId = $vars['userId'];
$invoiceId = $vars['invoiceId'];
$phone = waSphereGetClientPhone($clientId);
if (empty($phone)) {
return;
}
// Fetch order details
$order = Capsule::table('tblorders')
->where('id', $orderId)
->select('ordernum')
->first();
// The amount comes from the paid invoice (OrderPaid does not pass one)
$invoice = Capsule::table('tblinvoices')
->where('id', $invoiceId)
->select('total', 'currencyid')
->first();
$currency = Capsule::table('tblcurrencies')
->where('id', $invoice->currencyid)
->value('code') ?? 'USD';
$formattedAmount = waSphereFormatCurrency((float) $invoice->total, $currency);
$message = implode("\n", [
"✅ *Payment Received — Thank You!*",
"",
"Hi there! We've received your payment for Order #{$order->ordernum}.",
"",
"💳 Amount paid: *{$formattedAmount}*",
"🔢 Order number: *#{$order->ordernum}*",
"",
"Your services are being activated. You'll receive another message when everything is ready.",
"",
"_WaSphere — Your Hosting Team_",
]);
waSphereSendMessage($phone, $message);
});
Hook: Invoice Payment Reminder
<?php
// InvoiceCreated passes `invoiceid` (and `user`, not `userid`) — read the
// client id straight off the invoice row so it's always correct.
add_hook('InvoiceCreated', 1, function ($vars) {
$invoiceId = $vars['invoiceid'];
$invoice = Capsule::table('tblinvoices')
->where('id', $invoiceId)
->select('userid', 'invoicenum', 'total', 'duedate', 'currencyid')
->first();
if (!$invoice) {
return;
}
$phone = waSphereGetClientPhone((int) $invoice->userid);
if (empty($phone)) {
return;
}
$currency = Capsule::table('tblcurrencies')
->where('id', $invoice->currencyid)
->value('code') ?? 'USD';
$dueDate = date('d M Y', strtotime($invoice->duedate));
$total = waSphereFormatCurrency((float) $invoice->total, $currency);
$message = implode("\n", [
"🧾 *New Invoice #{$invoice->invoicenum}*",
"",
"Amount due: *{$total}*",
"Due date: *{$dueDate}*",
"",
"Pay online at: https://yourwhmcs.com/viewinvoice.php?id={$invoiceId}",
"",
"_Reply STOP to unsubscribe from WhatsApp notifications._",
]);
waSphereSendMessage($phone, $message);
});
Hook: Invoice Payment Reminder
<?php
// WHMCS fires InvoicePaymentReminder when its automation sends a payment
// reminder (there is no "InvoiceOverdue" hook). $vars: invoiceid, type.
add_hook('InvoicePaymentReminder', 1, function ($vars) {
$invoiceId = $vars['invoiceid'];
$invoice = Capsule::table('tblinvoices')
->where('id', $invoiceId)
->select('userid', 'invoicenum', 'total', 'duedate', 'currencyid')
->first();
if (!$invoice) {
return;
}
$phone = waSphereGetClientPhone((int) $invoice->userid);
if (empty($phone)) {
return;
}
$currency = Capsule::table('tblcurrencies')
->where('id', $invoice->currencyid)
->value('code') ?? 'USD';
$total = waSphereFormatCurrency((float) $invoice->total, $currency);
$message = implode("\n", [
"⚠️ *Overdue Invoice #{$invoice->invoicenum}*",
"",
"Your invoice for *{$total}* is now overdue.",
"",
"To avoid service interruption, please pay at:",
"https://yourwhmcs.com/viewinvoice.php?id={$invoiceId}",
"",
"If you've already paid, please ignore this message.",
]);
waSphereSendMessage($phone, $message);
});
Hook: Support Ticket Reply
<?php
// TicketAdminReply passes ticketid (and reply details) but NOT the client id —
// read userid off the ticket row. Skip guest tickets (userid = 0).
add_hook('TicketAdminReply', 1, function ($vars) {
$ticketId = $vars['ticketid'];
$ticket = Capsule::table('tbltickets')
->where('id', $ticketId)
->select('userid', 'tid', 'title', 'c')
->first();
if (!$ticket || empty($ticket->userid)) {
return;
}
$phone = waSphereGetClientPhone((int) $ticket->userid);
if (empty($phone)) {
return;
}
// Only notify if client has opted in — check a custom field
// (Omit this block if you notify all clients)
// $optIn = Capsule::table('tblclientsfields')
// ->where('clientid', $clientId)
// ->where('fieldid', WHATSAPP_OPTIN_FIELD_ID)
// ->value('value');
// if ($optIn !== 'Yes') return;
$message = implode("\n", [
"💬 *Support Ticket Update*",
"",
"Ticket #{$ticket->tid}: _{$ticket->title}_",
"",
"Our team has replied to your ticket. View and respond at:",
"https://yourwhmcs.com/viewticket.php?tid={$ticket->c}",
"",
"_Our typical response time is under 4 hours._",
]);
waSphereSendMessage($phone, $message);
});
Hook: Service Activation
<?php
add_hook('AfterModuleCreate', 1, function ($vars) {
$serviceId = $vars['params']['serviceid'];
$clientId = $vars['params']['userid'];
$phone = waSphereGetClientPhone($clientId);
if (empty($phone)) {
return;
}
$service = Capsule::table('tblhosting')
->where('id', $serviceId)
->select('domain', 'username', 'dedicatedip')
->first();
$message = implode("\n", [
"🚀 *Service Activated!*",
"",
"Your hosting service is now live.",
"",
"🌐 Domain: *{$service->domain}*",
"👤 Username: *{$service->username}*",
"",
"Log in to your control panel at:",
"https://yourwhmcs.com/clientarea.php",
"",
"Need help getting started? Reply to this message.",
]);
waSphereSendMessage($phone, $message);
});
Setting Environment Variables
Add to your WHMCS server's environment (not in the PHP file):
# /path/to/whmcs/.htaccess
SetEnv WASPHERE_API_URL "https://api.yourdomain.com"
SetEnv WASPHERE_API_KEY "wsk_..."
SetEnv WASPHERE_WORKSPACE_ID "your-workspace-id"
SetEnv WASPHERE_SESSION_ID "support"# In your Nginx server block
fastcgi_param WASPHERE_API_URL "https://api.yourdomain.com";
fastcgi_param WASPHERE_API_KEY "wsk_...";
fastcgi_param WASPHERE_WORKSPACE_ID "your-workspace-id";
fastcgi_param WASPHERE_SESSION_ID "support";# /etc/php/8.2/fpm/pool.d/whmcs.conf
[whmcs]
env[WASPHERE_API_URL] = https://api.yourdomain.com
env[WASPHERE_API_KEY] = wsk_...
env[WASPHERE_WORKSPACE_ID] = your-workspace-id
env[WASPHERE_SESSION_ID] = supportNever store your API key or webhook secret in the WHMCS hook PHP file. If the file is exposed (a misconfigured web server or a future WHMCS vulnerability), your key would be compromised. Use environment variables or WHMCS's encrypted configuration storage.
Opt-In / Opt-Out Handling
For compliance (GDPR, TCPA), clients should explicitly opt in to WhatsApp notifications. Add a custom client field in WHMCS:
- Go to Setup → Custom Client Fields
- Create a field:
WhatsApp Notifications/ Type:Yes/No - Show it on the client registration form and client area
Then check it before sending:
function waSphereClientOptedIn(int $clientId): bool
{
$optInFieldId = 5; // Replace with your actual custom field ID
$value = Capsule::table('tblclientsfields')
->where('relid', $clientId)
->where('fieldid', $optInFieldId)
->value('value');
return strtolower(trim($value ?? '')) === 'yes';
}
// In your hook:
add_hook('OrderPaid', 1, function ($vars) {
$clientId = $vars['userId'];
if (!waSphereClientOptedIn($clientId)) {
return; // Client hasn't opted in
}
// ... rest of the hook
});
Testing Hooks
WHMCS has a built-in hook tester at Utilities → Hook Tester (WHMCS 8.5+). Alternatively, trigger the hook manually:
// Temporary test script — remove after testing
// Place in /includes/hooks/wasphere_test.php
add_hook('AdminAreaPage', 1, function ($vars) {
if (isset($_GET['test_wasphere']) && $_SESSION['adminid']) {
$result = waSphereSendMessage('447700900123', 'WaSphere WHMCS hook test — ' . date('H:i:s'));
die($result ? 'Message sent successfully' : 'Failed — check WHMCS activity log');
}
});
Visit https://yourwhmcs.com/admin/index.php?test_wasphere=1 to trigger the test.
Delete the test hook file after testing. Leaving admin-only test hooks in production is a security risk — anyone who discovers the URL parameter can trigger WhatsApp messages.
Troubleshooting
Messages not sending
Check the WHMCS Activity Log (Utilities → Logs → Activity Log) — WaSphere hook errors are logged there via logActivity().
Phone numbers formatting incorrectly
WHMCS stores phone numbers in various formats depending on the client's input. The waSphereGetClientPhone() helper strips all non-numeric characters. If numbers still fail, log the raw value:
logActivity("WaSphere debug: raw phone for client {$clientId}: {$client->phonenumber} / cc: {$client->phonecc}");
Hook fires but message doesn't arrive
- Verify the WaSphere session is
connectedin the dashboard - Verify the API key has
messages:sendpermission - Check WaSphere's message queue in the dashboard — the message may be queued due to rate limiting