To add Google Tag Manager and GA4 to WHMCS, put one hook file in includes/hooks/. ClientAreaHeadOutput prints the container script in the head of every client area page, ClientAreaHeaderOutput prints the noscript frame at the top of the body, and ShoppingCartCheckoutCompletePage pushes a GA4 purchase event to the data layer. GA4 itself is configured as tags inside GTM.
Should you use the WHMCS Google Analytics addon or Google Tag Manager?
Use the addon if GA4 is the only tag you need, and GTM if you run several tags or want control of the data. WHMCS ships a Google Analytics addon module that supports GA4. You activate it at Configuration > System Settings > Addon Modules and enter your Measurement ID.
| WHMCS Google Analytics addon | GTM through a hook file | |
|---|---|---|
| Setup | Activate and enter a Measurement ID | One PHP file plus tags in GTM |
| Other tags (Ads, chat, consent banner) | No | Yes, all managed in GTM |
| Conversions | Goal tracking on cart.php?a=complete | A purchase event you define, with items |
| Offline gateways | The documentation says conversions cannot be tracked for Bank Transfer or Mail In Payment | Sent with invoice_paid false when the complete page loads |
Never run both for the same Measurement ID, or page views can be counted twice. Remove the addon, or any gtag.js snippet pasted into a template, before you publish the container.
Which WHMCS hooks print each part of the GTM snippet?
Two output hooks place the container code, and one cart hook handles the purchase. Google's install instructions ask for the script as high in the head as possible and the noscript block immediately after the opening body tag. The output hooks reference says where each hook's HTML lands.
| Hook | Where WHMCS puts the output | Use it for |
|---|---|---|
ClientAreaHeadOutput | Within the head tags | The GTM container script |
ClientAreaHeaderOutput | At the top of the body | The GTM noscript iframe |
ClientAreaFooterOutput | Before the closing body tag | Nothing. GTM does not need it. |
ShoppingCartCheckoutCompletePage | The order complete page | The purchase push |
A hook file works whatever theme is active, which a template edit does not. The one condition is that the theme keeps the output variables of the stock header and footer, as the theme documentation recommends. The addon page names one of them: {$footeroutput} must sit immediately before </body>. My WHMCS theme structure guide explains child themes.
How do you write the ClientAreaHeadOutput hook for the GTM container?
Create includes/hooks/gtm.php with the two output hooks and a check that skips staff. Replace GTM-XXXXXXX with your container ID.
<?php
if (!defined('WHMCS')) {
die('This file cannot be accessed directly');
}
function sm_gtm_id(): string
{
$user = new \WHMCS\Authentication\CurrentUser();
// No tracking for staff, including Login as Owner sessions.
if ($user->isAuthenticatedAdmin() || $user->isMasqueradingAdmin()) {
return '';
}
return 'GTM-XXXXXXX';
}
add_hook('ClientAreaHeadOutput', 1, function ($vars) {
$id = sm_gtm_id();
if ($id === '') {
return '';
}
return <<<HTML
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','{$id}');</script>
HTML;
});
add_hook('ClientAreaHeaderOutput', 1, function ($vars) {
$id = sm_gtm_id();
return $id === '' ? '' : '<noscript><iframe src="https://www.googletagmanager.com/ns.html?id='
. $id . '" height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>';
});
The staff check uses CurrentUser, which the developer documentation describes in its Authentication section. Admin pages are excluded already, because ClientArea hooks do not run there. My WHMCS hooks guide covers hook priorities and debugging.
How do you set up GA4 for WHMCS inside GTM?
Add a Google tag with your GA4 Measurement ID, which starts with G-, and fire it on all pages. That records page views across WHMCS with no further PHP. For purchases, Google's GA4 ecommerce guide for Tag Manager describes the setup:
- Create a Google Analytics: GA4 Event tag with the event name
purchase. - Under More Settings > Ecommerce, tick Send Ecommerce data and choose Data Layer as the source.
- Fire it on a Custom Event trigger whose event name is
purchase.
How do you push a GA4 purchase event when a WHMCS order completes?
Add a ShoppingCartCheckoutCompletePage hook to the same file. The shopping cart hook reference says it receives the order ID, order number, invoice ID, an ispaid flag, the amount, the payment method and the client's details, and returns HTML for the page. GA4's purchase event requires transaction_id and items, and currency whenever you send value. The hook builds them with two documented API calls, GetOrders and GetClientsDetails:
add_hook('ShoppingCartCheckoutCompletePage', 1, function ($vars) {
if (sm_gtm_id() === '') {
return '';
}
$orders = localAPI('GetOrders', ['id' => $vars['orderid']]);
$order = $orders['orders']['order'][0] ?? null;
if (!$order) {
return '';
}
$client = localAPI('GetClientsDetails', ['clientid' => $order['userid']]);
$items = [];
foreach ($order['lineitems']['lineitem'] ?? [] as $line) {
$items[] = [
'item_name' => $line['product'],
'item_category' => $line['type'],
'price' => (float) $line['amount'],
'quantity' => 1,
];
}
$event = [
'event' => 'purchase',
'invoice_paid' => (bool) $vars['ispaid'],
'ecommerce' => [
'transaction_id' => (string) $vars['ordernumber'],
'value' => array_sum(array_column($items, 'price')),
'currency' => $client['client']['currency_code'] ?? '',
'items' => $items,
],
];
$json = json_encode($event, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
return '<script>window.dataLayer = window.dataLayer || [];'
. 'dataLayer.push({ecommerce: null});dataLayer.push(' . $json . ');</script>';
});
The {ecommerce: null} push clears the previous ecommerce object, as Google's examples do. Google defines value as the sum of price times quantity, without tax or shipping, so compare it with the invoices of a few test orders that include setup fees and a promotion. Google's events reference says transaction_id helps avoid duplicate events for one purchase, which matters when a customer reloads the page.
What will browser-side tracking in WHMCS miss?
Any purchase whose order complete page never loads in the customer's browser, and any money that never passes through the cart.
- An order is not a payment. With an offline gateway the page appears before any money arrives. Use
invoice_paidas a trigger condition if you only want paid orders. - Delayed gateway notices. The addon documentation says that when the gateway has not yet told WHMCS about the payment, the customer is sent to the unpaid invoice with an "awaiting notification" message instead of the complete page. Test every gateway you offer.
- Renewals. They are invoices, not cart orders, so WHMCS reports stay the source of truth for revenue.
- Server-side events. The
OrderPaidhook runs when the first invoice of a new order is paid, whatever the browser does. Sending that to GA4 needs the Measurement Protocol, which Google says supplements tagging rather than replacing it. For new server-to-server integrations Google now points to its Data Manager API.
How do you set up consent mode for WHMCS?
Set default consent states before any tag sends data, then update them from the visitor's choice in a banner. Google's consent mode guide covers both parts.
- Basic or advanced. Basic blocks Google tags until the visitor answers the banner, and sends nothing before that. Advanced loads the tags with defaults set to denied and sends measurements without cookies until consent is granted. Your company's guidelines decide which one you use.
- With GTM, load the banner in the container. Google recommends a consent mode template from your consent platform or the Community Template Gallery, using
setDefaultConsentStateandupdateConsentState, and says not to usegtag('consent', 'update', ...)in place ofupdateConsentState. The hook file stays as it is. - The four consent types are
ad_storage,ad_user_data,ad_personalizationandanalytics_storage. Defaults can be scoped to regions. - Tags without built-in consent checks, which includes many non-Google tags, need checks added in GTM under Advanced > Consent Settings.
Who must see a banner is a legal question, not a WHMCS one. My WHMCS GDPR guide covers the WHMCS side of privacy.
What personal data must stay out of the data layer?
All of it: names, email addresses, phone numbers, postal addresses, tax IDs and card details. The purchase hook receives the full clientdetails array, and every tag in the container can read the data layer, not only GA4. Push the order number, amounts, currency and product names, nothing more. Also look at the addresses of password reset and email verification pages on your install. If they carry a token, exclude those pages in GTM or redact the URL before tags fire.
How do you test GTM and GA4 on WHMCS?
Click Preview in GTM, enter your WHMCS URL and connect Tag Assistant. Tag Assistant also switches on GA4 debug mode, so events appear under Admin > Data display > DebugView. On a staging copy, then again after publishing, check that:
- The container loads on the homepage, the cart, the login page and a logged-in page.
- The Google tag fires once per page.
- With consent denied, no analytics cookies are set.
- A test order shows
purchasewith the right order number, items, value and currency code. - Reloading the complete page does not add a second purchase.
- A client in a second currency sends that currency's code.
- Pages viewed while logged in as admin contain no container.
If you want server-side purchase events or a consent setup wired into WHMCS, that is WHMCS API integration and automation work, and my hire a WHMCS developer page explains how I scope it.
Last reviewed 12 October 2026. Disclosure: I sell WHMCS and web development work, so I am not neutral. Hook names come from developers.whmcs.com and the GA4, GTM and consent details from Google's documentation as I read them on that date.