Google Analytics 4 isn't optional anymore — Universal Analytics is dead, and "I have a Facebook Pixel" doesn't tell you how customers actually behave on your site. WHMCS doesn't ship GA4 integration. You wire it yourself. Done right, you get real ecommerce funnel data: which products people view, where they drop off, what their LTV looks like.
This is the practical GA4 setup for WHMCS.
What you'll track
GA4's ecommerce events for a WHMCS hosting site:
- view_item — customer looks at a product on order form.
- add_to_cart — added to cart.
- begin_checkout — entered the cart/checkout flow.
- purchase — order completed and paid.
- refund — if you process refunds.
Plus optional custom events: signup_step_completed, ticket_opened, support_chat_started.
Step 1 — Create the GA4 property
- analytics.google.com → create property.
- Set the timezone + currency to match your business.
- Create a Web data stream for your WHMCS domain.
- Copy the Measurement ID (looks like
G-XXXXXXXXXX). - In Admin → Data Streams → web stream → Configure tag settings → Show all → Enhanced measurement: leave defaults on. Disable "Site search" if your WHMCS doesn't have a search box.
Step 2 — Decide your tag-management strategy
Two paths:
- Direct GA4 tag — paste the GA4 snippet into your WHMCS theme's
header.tpl. Simplest. Works for basic page-view tracking. Custom events require code changes per event. - Google Tag Manager — install GTM in your theme; manage GA4 (and Facebook Pixel, LinkedIn Insight, etc.) via GTM UI. More work upfront, much easier later when adding/changing tags.
For any serious hosting business, use GTM. The flexibility pays back within months.
Step 3 — Install the tag in WHMCS
If you went GA4 direct, paste into your theme's header.tpl right before </head>:
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-XXXXXXXXXX', {
send_page_view: true,
debug_mode: false
});
</script>
If you went GTM:
<!-- Google Tag Manager -->
<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','GTM-XXXXXXX');</script>
<!-- End Google Tag Manager -->
Add the <noscript> snippet right after <body> per GTM's install docs.
Step 4 — Push ecommerce events from WHMCS
This is where most operators stop and end up with only page-view data. The valuable stuff is in the events.
view_item — product page on order form
In your order form template (/templates/orderforms/{your-template}/products.tpl), after the products loop:
<script>
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
event: 'view_item_list',
ecommerce: {
item_list_name: 'Order form',
items: [
{foreach $products as $product}
{
item_id: '{$product.pid}',
item_name: '{$product.name|escape:'javascript'}',
item_category: '{$productgroup.name|escape:'javascript'}',
price: parseFloat('{$product.pricing.monthly|replace:',':''}') || 0,
currency: '{$currentcurrency.code}',
quantity: 1
},
{/foreach}
]
}
});
</script>
add_to_cart — when item is added
WHMCS doesn't fire a JS event when add-to-cart happens (it's a server-side action). Add a small inline JS that fires on click of the order form's submit button:
document.querySelectorAll('form.order-form').forEach(function (form) {
form.addEventListener('submit', function () {
window.dataLayer.push({
event: 'add_to_cart',
ecommerce: {
currency: '{$currentcurrency.code}',
value: parseFloat(form.querySelector('[name="total"]')?.value || 0),
items: [{
item_id: form.querySelector('[name="pid"]')?.value,
item_name: form.querySelector('[data-product-name]')?.dataset.productName,
quantity: 1
}]
}
});
});
});
begin_checkout — cart page
In viewcart.tpl, fire when the cart page loads:
<script>
window.dataLayer.push({
event: 'begin_checkout',
ecommerce: {
currency: '{$currentcurrency.code}',
value: parseFloat('{$rawtotal}'),
items: [
{foreach $cart.products as $product}
{ item_id: '{$product.pid}', item_name: '{$product.productinfo.name|escape:'javascript'}', price: parseFloat('{$product.pricing.totaltoday}'), quantity: 1 },
{/foreach}
]
}
});
</script>
purchase — order complete
The most important event. Fires on the "thank you" page. /templates/{your-theme}/orderforms/{your-form}/complete.tpl or the equivalent for your order form:
<script>
window.dataLayer.push({
event: 'purchase',
ecommerce: {
transaction_id: '{$invoice_id}',
value: parseFloat('{$invoice_amount}'),
tax: parseFloat('{$invoice_tax}'),
currency: '{$currency}',
items: [
{foreach $orderitems as $item}
{ item_id: '{$item.pid}', item_name: '{$item.productinfo.name|escape:'javascript'}', price: parseFloat('{$item.amount}'), quantity: 1 },
{/foreach}
]
}
});
</script>
If your gateway redirects offsite (PayPal, Stripe Checkout), the "thank you" page is the WHMCS post-success page. WHMCS hooks (ClientAreaPageCheckoutComplete) fire there.
Step 5 — Server-side tracking for refunds
Refunds happen via gateway webhook → WHMCS database update. There's no browser session to fire a JS event. Use server-side GA4 via the Measurement Protocol:
add_hook('InvoiceRefunded', 1, function ($vars) {
$invoiceId = $vars['invoiceid'];
$invoice = Capsule::table('tblinvoices')->where('id', $invoiceId)->first();
$payload = json_encode([
'client_id' => 'whmcs.client.' . $invoice->userid,
'events' => [[
'name' => 'refund',
'params' => [
'transaction_id' => $invoiceId,
'value' => $invoice->total,
'currency' => 'USD',
],
]],
]);
$url = "https://www.google-analytics.com/mp/collect?measurement_id=G-XXXXXXXXXX&api_secret=YOUR_API_SECRET";
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_TIMEOUT => 3,
]);
curl_exec($ch);
curl_close($ch);
});
Get the API Secret from Admin → Data Streams → web stream → Measurement Protocol API secrets → Create.
Step 6 — Cross-domain & cross-device attribution
WHMCS often lives at billing.yourbrand.com while your marketing site is at yourbrand.com. Configure GA4 to recognize them as one journey:
- Same GA4 property on both sites (same Measurement ID).
- In GA4 admin: Data Streams → Configure tag settings → Configure your domains → Add billing.yourbrand.com.
- This enables automatic cross-domain tracking (link decoration with
_glparameter).
Step 7 — Mark purchase as a conversion
GA4 won't show "conversion rate" until you tell it which events count as conversions.
- GA4 → Admin → Events.
- Find the
purchaseevent in the list. - Toggle "Mark as conversion."
Within 24 hours, your reports show conversion rates from each acquisition channel.
How to verify everything works
- GA4 DebugView — GA4 → Admin → DebugView. Enable debug mode in your browser (Chrome extension "GA Debugger" or
?debug_mode=true). Walk through a test order; watch events appear in real time. - Tag Assistant — Chrome extension shows fired tags on every page.
- Real-time reports — after debug, switch to live reporting and confirm the events appear in production (no debug flag).
- Check the data after 48 hours — GA4 reports lag. The full ecommerce funnel should show: view → add → begin → purchase.
Common pitfalls
"Events fire but money values are wrong." Currency code missing or values are strings. GA4 wants numeric values, not "$10.50".
"Duplicate purchase events." Customer reloads the thank-you page; the event fires again. Fire only once: store a flag in sessionStorage keyed to the transaction ID.
"Cross-domain tracking doesn't work." The destination domain is missing from the GA4 cross-domain list. Add both directions (apex + billing subdomain).
"GTM tags don't fire on success page." Some payment gateways redirect to a different domain (Stripe Checkout). Add your gateway's success-redirect URLs to GTM's domain config.
"Refunds aren't showing in GA4." Server-side Measurement Protocol failed silently. Add logging to the curl call; check GA4 DebugView for the refund event.
My take — what GA4 actually tells you
After 90 days of properly-instrumented GA4:
- Where are people dropping off in the funnel? Usually surprise — it's almost never where you'd guess.
- Which acquisition channel produces highest-LTV customers? Optimize spend there.
- Which products are upgraded most often? Build comparison tables that highlight them.
- What's your time-to-purchase by channel? Direct customers buy fast; SEO customers research for days.
This is the data that turns marketing from gut feeling into actual decisions.
Going further
I instrument GA4 + GTM on WHMCS deployments — events, conversions, cross-domain attribution, server-side refunds. If you want real funnel data on your hosting business, tell me your setup and I'll send a quote in 24 hours.