Skip to content
Shahid Malla

WHMCS Google Tag Manager and GA4 Setup With a Hook File

How to add Google Tag Manager and GA4 to WHMCS without touching templates: the ClientAreaHeadOutput hook, a dataLayer purchase event, consent, testing, and the personal data to keep out.

· Published · Updated · 9 min read
On this page (10 sections)

To add Google Tag Manager to WHMCS without editing templates, put one PHP file in includes/hooks/. The ClientAreaHeadOutput hook prints the container script inside the head, ClientAreaHeaderOutput prints the noscript frame at the top of the body, and ShoppingCartCheckoutCompletePage pushes a purchase event to the dataLayer. GA4 itself is then set up as tags inside GTM.

Last reviewed 10 October 2026. Disclosure: I sell WHMCS development, including hook files and tracking setups like this one, so I am not neutral about hiring someone for it. The hook names and parameters below were checked against the official WHMCS hooks reference and a WHMCS 9 install.

Why add Google Tag Manager to WHMCS with a hook and not in the template?

Because a hook file survives theme updates and theme changes, and a template edit does not. If you paste the snippet into header.tpl, the next theme update can overwrite it, and every extra theme needs the same edit again. A file in includes/hooks/ is loaded whichever theme is active.

The hook output reaches the page through variables that the stock themes already print: {$headoutput} inside the head, {$headeroutput} after the opening body tag and {$footeroutput} before the closing one. A custom theme that removed those variables prints nothing, so check header.tpl and footer.tpl first. Putting them back is a small WHMCS theme customization job.

Which WHMCS hook prints each part of the GTM snippet?

Three output hooks cover the snippet and one cart hook covers the purchase. Each returns a string of HTML, and WHMCS places it at a fixed spot in the page.

HookWhere the output landsUse it for
ClientAreaHeadOutputInside the head tags of every client area pageConsent defaults, then the GTM container script
ClientAreaHeaderOutputTop of the bodyThe GTM noscript iframe
ClientAreaFooterOutputBefore the closing body tagScripts that can load late, such as a chat widget. GTM does not need it.
ShoppingCartCheckoutCompletePageThe order-complete page onlyThe dataLayer purchase push
AdminAreaHeadOutputThe head of the admin areaNothing here. Marketing tags do not belong in admin.

"Client area" in these names means every page the client theme renders: the homepage, the cart, the knowledgebase, login and the logged-in pages. My WHMCS hooks guide explains priorities and how hook files are loaded.

How do you write the ClientAreaHeadOutput hook for the GTM container?

Create includes/hooks/gtm.php, return the container script from ClientAreaHeadOutput and the noscript iframe from ClientAreaHeaderOutput. Replace GTM-XXXXXXX with your container ID.

<?php
// includes/hooks/gtm.php
if (!defined('WHMCS')) {
    die('This file cannot be accessed directly');
}

use WHMCS\Database\Capsule;

function sm_gtm_id()
{
    // No tracking while a staff member is logged in on this browser.
    if (!empty($_SESSION['adminid'])) {
        return '';
    }
    return 'GTM-XXXXXXX';
}

add_hook('ClientAreaHeadOutput', 1, function ($vars) {
    $id = sm_gtm_id();
    if ($id === '') {
        return '';
    }
    return <<<HTML
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('consent', 'default', {
  ad_storage: 'denied', ad_user_data: 'denied',
  ad_personalization: 'denied', analytics_storage: 'denied'
});
</script>
<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();
    if ($id === '') {
        return '';
    }
    return '<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=' . $id
        . '" height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>';
});

The second script block is the standard container snippet from your GTM account. The first block sets consent to denied before the container loads, which I explain below. Save the file and view the source of any client area page: the snippet should sit in the head.

How do you set up GA4 in WHMCS through GTM?

Add one Google tag in GTM with your GA4 Measurement ID (it starts with G-) and fire it on all pages. That gives you page views on every WHMCS page with no further PHP.

Send each page view once. WHMCS ships its own Google Analytics addon module with ecommerce tracking, which I do not cover here. If it is active, or the gtag.js snippet is already pasted into a template, remove that before publishing the container, or every visit is counted twice. Use the same Measurement ID on your marketing site and on WHMCS, so a visit that moves to the cart stays one session. Separate domains also need cross-domain measurement in the GA4 data stream.

How do you push a GA4 purchase event on the order-complete page?

Use ShoppingCartCheckoutCompletePage, which runs when the checkout complete page is displayed and returns HTML for that page. It receives the order ID, order number, invoice ID, an ispaid flag, the order amount, the payment method and the client's details. Add this to the same file:

add_hook('ShoppingCartCheckoutCompletePage', 1, function ($vars) {
    if (sm_gtm_id() === '') {
        return '';
    }
    $currency = Capsule::table('tblorders')
        ->join('tblclients', 'tblclients.id', '=', 'tblorders.userid')
        ->join('tblcurrencies', 'tblcurrencies.id', '=', 'tblclients.currency')
        ->where('tblorders.id', $vars['orderid'])
        ->value('tblcurrencies.code');

    $event = [
        'event'        => 'purchase',
        'invoice_paid' => (bool) $vars['ispaid'],
        'ecommerce'    => [
            'transaction_id' => (string) $vars['ordernumber'],
            'value'          => (float) $vars['amount'],
            'currency'       => $currency,
        ],
    ];
    $json = json_encode($event, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);

    return "<script>window.dataLayer = window.dataLayer || [];"
        . "dataLayer.push({$json});</script>";
});

The order form's complete.tpl prints the returned HTML through its $addons_html loop, so a custom order form without that loop shows nothing. In GTM, create a Custom Event trigger for the event name purchase and a GA4 Event tag named purchase that sends ecommerce data from the data layer. Know these limits before you trust the numbers:

  • An order is not a payment. With bank transfer the invoice is still unpaid when the page appears, which is why the push carries invoice_paid. Use it as a trigger condition if you only want paid orders.
  • The page is not always shown. Auto Redirect on Checkout, on the Ordering tab of General Settings, can send the customer to the invoice or straight to the payment gateway instead of the order-complete page. A customer who pays on a hosted payment page may never load the complete page. Test each gateway you offer.
  • Renewals never pass through the cart. For exact revenue, WHMCS reports stay the source of truth. To record paid orders regardless of the browser, send the event from the server when the OrderPaid hook runs, using GA4's Measurement Protocol.
  • Item reports need items. GA4 fills its item reports from an items array. Build it from the order's services, addons and domains if you need product-level data.

Always send transaction_id. GA4 uses it to avoid counting the same purchase twice when a customer reloads the page.

How do you keep tracking off admin pages and staff sessions?

The admin area is already excluded, because the ClientArea hooks do not run there. The admin area has its own AdminAreaHeadOutput family, which this file does not use. The leak is staff browsing the client area: testing the cart, or opening a customer's account with Login as Owner. The sm_gtm_id() check above returns nothing when the session holds an admin login.

That session key is not part of the documented hook parameters, so confirm it on your version: log in as admin, open the client area in the same browser and check that the page source has no GTM snippet.

Set a default consent state before the container loads, then let a consent banner update it. The first script in the hook does the default part: it marks ad and analytics storage as denied, so Google's tags set no cookies until the visitor agrees. The banner, loaded through GTM or printed by the same hook, then calls gtag('consent', 'update', ...) with the visitor's choice. Tags from other vendors ignore this signal unless you add consent conditions to them in GTM.

Which visitors need the banner is a legal question, not a WHMCS one. If you sell to the European Union or the United Kingdom, assume you need it and ask whoever advises you on privacy.

How do you test with Tag Assistant and GA4 DebugView?

Use GTM's Preview button, which opens Tag Assistant against your WHMCS URL, and watch DebugView in GA4 at the same time. Work through this list on staging, then again on production after publishing the container:

  1. The container loads on the homepage, the cart, the login page and a logged-in page.
  2. The Google tag fires once per page, not twice.
  3. With consent denied, no analytics cookies are set. After accepting, they are.
  4. A test order shows the purchase event in Tag Assistant with the right order number, total and currency code.
  5. Reloading the order-complete page does not add a second purchase in GA4.
  6. A test order in a second currency carries that currency's code.
  7. Each gateway you offer still reaches the order-complete page, or you know which ones do not.
  8. The same pages viewed while logged in as admin contain no snippet.
  9. The events appear in DebugView.

What should you never send to Google from WHMCS?

Anything that identifies a person: names, email addresses, phone numbers, postal addresses, tax IDs and card details. Google's Analytics policies forbid it, and privacy law makes it your problem. In WHMCS the risk is concrete: the purchase hook receives a full clientdetails array, and it is tempting to push all of it. Push only the order number, value, currency and product data.

Watch page addresses too. GA4 records the URL of each page, and some WHMCS links carry one-time tokens, such as password reset and email verification links. Exclude those pages in GTM or redact the address before the tag fires.

What is the next step?

Create the container, copy the hook file to a staging copy of WHMCS and run the test list before anything reaches production. If you want the items array, server-side payment events or a consent banner wired in, that is WHMCS API integration and automation work, and my hire a WHMCS developer page explains how I scope it. I start with a free scoping call of about 30 minutes and quote within one business day.

Share this article

Shahid Malla

Written by

Shahid Malla

Freelance WHMCS developer and hosting infrastructure engineer with 13+ years of client work. CTO at ElySpace, developer at Marmaids LLC, and founder of WHMCSPilot and MagizAI.

Got a project like this?

Tell me what you need - I'll send a real quote within 24 hours.