Server-Side Tracking for ClickFunnels 2.0
Stop losing conversions to iOS ITP, short-lived cookies, and ad-blockers. TrakSolv hosts a first-party server-side GTM container for your ClickFunnels 2.0 store and forwards clean, deduplicated conversions to GA4, Meta CAPI, Google Ads and more. Live in minutes, flat monthly pricing.
The pixel alone is leaking your data.
Browser-side tracking on ClickFunnels 2.0 loses a growing share of conversions. Here's what moving to a server-side setup fixes.
Recover conversions lost to ITP & ad-blockers
On ClickFunnels 2.0, browser-side pixels are throttled by Safari/Firefox ITP, capped 7-day cookies, and blocked outright by ad-blockers and consent walls. Server-side tracking moves the measurement off the browser, so opt-ins and purchases that the pixel silently drops are recovered and reported.
First-party data, served from your own domain
TrakSolv hosts your Google Tag Manager server container on a subdomain of your ClickFunnels 2.0 site. Requests come from a first-party context — they survive 3rd-party cookie deprecation, last far longer than client-side cookies, and look like your own traffic, not a tracker.
Clean, deduplicated conversions to every ad platform
The ClickFunnels 2.0 dataLayer fires every GA4 enhanced-ecommerce event with a stable event_id, so your server-side container can forward opt-ins and purchases to GA4, Meta CAPI, Google Ads Enhanced Conversions, TikTok and more — deduplicated against the browser pixel, with higher match quality.
How to set up server-side tracking on ClickFunnels 2.0
Paste the TrakSolv loader and the ClickFunnels 2.0 dataLayer snippet into your store's custom-code area, save, and you're done. Every step below has the exact snippet with a copy button.
Install guide
How to install on ClickFunnels 2.0
This is your install. Every step below has its exact snippet with a Copy button — go step by step. The Loader card above and DataLayer card below are reference copies of the same snippets.
- 1
Open your Funnel's tracking code
NavigateClickFunnels 2.0 → your Funnel → Settings → Tracking Code
Funnel-level tracking code applies to every step in the funnel — far better than pasting on each step.
- 2
Loader — head half
Loader • HeadFunnel Settings → Tracking Code → HEAD field → paste the Loader Head snippet
- 3
Loader — body half + DataLayer
Loader • BodyFunnel Settings → Tracking Code → FOOTER field → paste the Loader Body snippet FIRST
- 4
DataLayer snippet
DataLayerFunnel Settings → Tracking Code → FOOTER field → append the DataLayer after the Body
On the order page (where customers click Buy) the snippet scrapes .cf-order-summary and stashes totals in sessionStorage. On the thank-you step it restores the stash and auto-fires the purchase event — no per-step setup needed for the common Order → Upsell → Thank-You shape.
HTML<!-- Traksolv · ClickFunnels 2.0 DataLayer — Funnel → Settings → Tracking Code → Head --> <script> (function (w, d) { w.dataLayer = w.dataLayer || []; var DEBUG = false; try { var q = new URLSearchParams(location.search); if (q.get('tcDebug') === '1') { DEBUG = true; try { localStorage.setItem('tcDebug', '1'); } catch (e) {} } else if (q.get('tcDebug') === '0') { DEBUG = false; try { localStorage.removeItem('tcDebug'); } catch (e) {} } else { try { if (localStorage.getItem('tcDebug') === '1') DEBUG = true; } catch (e) {} } } catch (e) {} w.tcDebug = DEBUG; if (DEBUG && w.console && w.console.log) { try { w.console.log('%c[Traksolv]%c dataLayer debug ON — every push will log here. Disable with ?tcDebug=0', 'color:#22c55e;font-weight:600;background:#0a1f0c;padding:2px 6px;border-radius:3px', ''); } catch (e) {} } function log() { if (!DEBUG || !w.console || !w.console.log) return; try { w.console.log.apply(w.console, ['[Traksolv]'].concat([].slice.call(arguments))); } catch (e) {} } function push(eventName, eventId, params) { try { if (params && params.ecommerce !== undefined) w.dataLayer.push({ ecommerce: null }); var ev = { event: eventName, event_id: eventName + '.' + eventId }; if (params) for (var k in params) if (Object.prototype.hasOwnProperty.call(params, k)) ev[k] = params[k]; w.dataLayer.push(ev); log('push ' + eventName, ev); } catch (e) {} } function once(key) { try { if (sessionStorage.getItem(key)) return false; sessionStorage.setItem(key, '1'); return true; } catch (e) { return true; } } function money(v) { return parseFloat(String(v == null ? '' : v).replace(/[^\d.\-]/g, '')) || 0; } function textOf(sel, root) { var el = (root || d).querySelector(sel); return el ? (el.textContent || '').trim() : ''; } // funnel-scoped stash key so multiple funnels on the same domain don't collide. function stashKey() { return 'tc_cf_order_' + (location.pathname.split('/')[1] || 'root'); } w.tcFirePurchase = function (o) { try { if (!o || !o.orderId) return; if (!once('tc_purchase_' + o.orderId)) return; push('purchase', String(o.orderId), { ecommerce: { transaction_id: String(o.orderId), value: Number(o.value) || 0, currency: o.currency || 'USD', items: Array.isArray(o.items) ? o.items : [] } }); } catch (e) {} }; function pageType() { if (typeof w.tcStepType === 'string') return w.tcStepType; var path = (location.pathname || '').toLowerCase(); if (/thank[-_]?you|order[-_]?(confirm|complete|success)|receipt|confirmation/i.test(path)) return 'thank-you'; if (d.querySelector('.cf-order-summary, .elOrderProductOptinProducts')) return 'order'; if (/upsell|downsell|otop?|one[-_]?time[-_]?offer/i.test(path)) return 'upsell'; return 'other'; } w.tcPageType = pageType; // Scrape the order page → stash items + total for the thank-you step. function scrapeOrderPage() { try { var rows = d.querySelectorAll('.cf-order-summary .elOrderProductOptinProducts, .elOrderProductOptinProducts'); var items = []; var total = 0; for (var i = 0; i < rows.length; i++) { var r = rows[i]; var name = textOf('.elOrderProductOptinName, .product-name, h4', r) || ('item.' + i); var price = money(textOf('.elOrderProductOptinPrice, .price', r)); items.push({ item_id: String(name), item_name: name, price: price, quantity: 1 }); total += price; } if (!items.length) { // Fall back: any visible total on the order page (Stripe Elements / iframed checkout). total = money(textOf('.elOrderProductOptinTotal, .elOrderTotal, [data-order-total]')); } var snap = { items: items, value: total, currency: 'USD', t: Date.now() }; try { sessionStorage.setItem(stashKey(), JSON.stringify(snap)); } catch (e) {} // Also push begin_checkout + view_item now that we have the data. var key = location.pathname; if (once('tc_begin_checkout_' + key)) { push('begin_checkout', key, { ecommerce: { currency: 'USD', value: total, items: items } }); } return snap; } catch (e) { return null; } } function firePurchaseOnThankYou() { try { // 1) Explicit override from merchant. if (w.tcOrder && w.tcOrder.orderId) { w.tcFirePurchase(w.tcOrder); return; } // 2) Stash from the order step. var snap = null; try { snap = JSON.parse(sessionStorage.getItem(stashKey()) || 'null'); } catch (e) {} if (!snap) return; // Mint an orderId we can dedup against — date + value is good enough // when CF doesn't surface one. Merchant can override via tcOrder. var orderId = 'cf.' + snap.t + '.' + Math.round(snap.value * 100); w.tcFirePurchase({ orderId: orderId, value: snap.value, currency: snap.currency || 'USD', items: snap.items }); try { sessionStorage.removeItem(stashKey()); } catch (e) {} } catch (e) {} } function boot() { var t = pageType(); if (t === 'order') scrapeOrderPage(); if (t === 'thank-you') firePurchaseOnThankYou(); } if (d.readyState === 'loading') d.addEventListener('DOMContentLoaded', boot); else boot(); })(window, document); </script> <!-- End Traksolv ClickFunnels 2.0 DataLayer --> <!-- Traksolv · DataLayer enrichment — customer lifecycle + remarketing + attribution --> <script> (function (w, d) { function readLT() { try { var raw = localStorage.getItem('tc_lifetime'); var p = raw ? JSON.parse(raw) : null; return { orderCount: (p && p.orderCount) || 0, lifetimeValue: (p && p.lifetimeValue) || 0, firstOrderTs: (p && p.firstOrderTs) || null, lastOrderTs: (p && p.lastOrderTs) || null, lastOrderId: (p && p.lastOrderId) || null }; } catch (e) { return { orderCount: 0, lifetimeValue: 0, firstOrderTs: null, lastOrderTs: null, lastOrderId: null }; } } function writeLT(lt) { try { localStorage.setItem('tc_lifetime', JSON.stringify(lt)); } catch (e) {} } function tier(v) { if (v >= 1000) return 'vip'; if (v >= 250) return 'mid'; if (v > 0) return 'low'; return 'guest'; } function readCookie(n) { try { var m = d.cookie.match(new RegExp('(^|; )' + n + '=([^;]+)')); return m ? decodeURIComponent(m[2]) : null; } catch (e) { return null; } } function readAttr() { try { var r = readCookie('_tc_attr'); return r ? JSON.parse(r) : null; } catch (e) { return null; } } w.tcReadLifetime = readLT; w.tcCustomerTier = function (v) { return tier(v != null ? v : readLT().lifetimeValue); }; w.tcReadAttribution = readAttr; // ── Wrap tcFirePurchase to add customer-lifecycle + attribution ── // before the underlying snippet pushes the purchase event. Idempotent // per orderId, so customer refreshing the thank-you page doesn't // inflate their lifetime counters. var orig = w.tcFirePurchase; if (typeof orig === 'function') { w.tcFirePurchase = function (o) { try { if (!o || !o.orderId) return orig.apply(this, arguments); var lt = readLT(); var firstFire = lt.lastOrderId !== String(o.orderId); var orderCount = firstFire ? lt.orderCount + 1 : lt.orderCount; var lifetimeValue = firstFire ? lt.lifetimeValue + (o.value || 0) : lt.lifetimeValue; var firstOrderTs = lt.firstOrderTs || Date.now(); var newCustomer = orderCount === 1; var daysSinceFirst = Math.floor((Date.now() - firstOrderTs) / 86400000); if (firstFire) { writeLT({ orderCount: orderCount, lifetimeValue: lifetimeValue, firstOrderTs: firstOrderTs, lastOrderTs: Date.now(), lastOrderId: String(o.orderId) }); } var attr = readAttr(); // Build the enriched order. Preserve every original field; add // customer + attribution on top. var enriched = {}; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) enriched[k] = o[k]; enriched.new_customer = newCustomer; enriched.customer_type = newCustomer ? 'new' : 'returning'; enriched.customer_status = 'customer'; enriched.customer_lifetime_value = Math.round(lifetimeValue * 100) / 100; enriched.customer_order_count = orderCount; enriched.customer_aov = orderCount > 0 ? Math.round((lifetimeValue / orderCount) * 100) / 100 : 0; enriched.purchase_number = orderCount; enriched.days_since_first_order = daysSinceFirst; enriched.value_tier = tier(lifetimeValue); if (attr) enriched.attribution = { landing: attr.u || null, referrer: attr.r || null, utm: attr.utm || null, click_ids: attr.cid || null }; return orig.call(this, enriched); } catch (e) { try { return orig.apply(this, arguments); } catch (e2) {} } }; } // ── tc_visitor_context — emits visitor lifecycle + remarketing on // every page-view. GTM / GA4 audience triggers + Google Ads // dynamic remarketing tag read these. function visitorContext() { try { var lt = readLT(); var attr = readAttr(); var pageType = (typeof w.tcPageType === 'function') ? (w.tcPageType() || 'other') : 'other'; w.dataLayer = w.dataLayer || []; w.dataLayer.push({ event: 'tc_visitor_context', event_id: 'tc_visitor_context.' + Date.now() + '.' + Math.random().toString(36).slice(2, 6), customer_status: lt.orderCount > 0 ? 'returning_customer' : 'guest', customer_type: lt.orderCount > 0 ? 'returning' : 'guest', value_tier: tier(lt.lifetimeValue), customer_lifetime_value: Math.round(lt.lifetimeValue * 100) / 100, customer_order_count: lt.orderCount, days_since_first_order: lt.firstOrderTs ? Math.floor((Date.now() - lt.firstOrderTs) / 86400000) : null, ecomm_pagetype: pageType, attribution: attr ? { landing: attr.u || null, referrer: attr.r || null, utm: attr.utm || null, click_ids: attr.cid || null } : null }); } catch (e) {} } if (d.readyState === 'loading') d.addEventListener('DOMContentLoaded', visitorContext); else visitorContext(); })(window, document); </script> <!-- End Traksolv enrichment --> - 5
Optional — override with exact order data on the thank-you step
DataLayer(Optional) Thank-You step → Settings → Tracking Code → FOOTER field
If your funnel has a non-standard thank-you flow or you want to use ClickFunnels merge tags, paste this block on the thank-you step's footer to override the auto-scrape with exact values.
HTML<script> window.tcOrder = { orderId: "order-" + Date.now(), value: 97, /* this offer's price */ currency: "USD", items: [{ item_id: "your-product-id", item_name: "Your Product Name", price: 97, quantity: 1 }] }; </script> - 6
Save and publish
SaveSave → Funnel → Publish all changes
The loader Head + Body snippets are generated for your container inside the TrakSolv dashboard. Create a free account to grab them, or see the full how-it-works walkthrough.
Server-side tracking for ClickFunnels 2.0, answered
Does server-side tracking work with ClickFunnels 2.0?
Yes. TrakSolv works with ClickFunnels 2.0 via a lightweight loader snippet plus a ClickFunnels 2.0-specific dataLayer that fires every GA4 enhanced-ecommerce event. You paste it once in ClickFunnels 2.0's custom-code area and conversions start flowing through your own server-side GTM container.
How do I install TrakSolv on ClickFunnels 2.0?
Add the TrakSolv loader (head + body) and the ClickFunnels 2.0 dataLayer snippet to your store's custom-code settings, then save. The full step-by-step is on this page. Most ClickFunnels 2.0 merchants are live in a few minutes — no developer required.
What can I track server-side on ClickFunnels 2.0?
Page views, product views, opt-ins and purchases, begin-checkout and more — every standard GA4 enhanced-ecommerce event, forwarded server-side to GA4, Meta CAPI, Google Ads and other destinations with deduplication.
How much does server-side tracking for ClickFunnels 2.0 cost?
TrakSolv uses flat monthly pricing — one event sent to many destinations still counts as one event, and paid plans never auto-pause at quota. You can start free and go live in minutes.
Recover your ClickFunnels 2.0 conversions — start free.
Spin up a first-party server-side GTM container for ClickFunnels 2.0 and watch your match quality climb. No credit card, live in minutes.
