Server-Side Tracking for BigCommerce
Stop losing conversions to iOS ITP, short-lived cookies, and ad-blockers. TrakSolv hosts a first-party server-side GTM container for your BigCommerce 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 BigCommerce loses a growing share of conversions. Here's what moving to a server-side setup fixes.
Recover conversions lost to ITP & ad-blockers
On BigCommerce, 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 purchases and add-to-carts 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 BigCommerce 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 BigCommerce dataLayer fires every GA4 enhanced-ecommerce event with a stable event_id, so your server-side container can forward purchases and add-to-carts 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 BigCommerce
Paste the TrakSolv loader and the BigCommerce 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 BigCommerce
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 Script Manager
NavigateBC Admin → Storefront → Script Manager
- 2
Loader — head half
Loader • HeadCreate a Script → Location: Head, Pages: All pages, Type: Script
Name it 'Traksolv — Loader Head'. Paste the Head snippet from the Custom Loader card above.
- 3
Loader — body half
Loader • BodyCreate a Script → Location: Footer, Pages: All pages, Type: Script
Name it 'Traksolv — Loader Body'. Paste the Body snippet.
- 4
Advanced DataLayer
DataLayerCreate a Script → Location: Footer, Pages: All pages, Type: Script
Name it 'Traksolv — DataLayer'. Paste the DataLayer snippet from below. Order matters: this one MUST be after the Loader Body script in the same Footer.
HTML<!-- Traksolv · BigCommerce DataLayer — install once in Script Manager (All Pages, Footer) --> <script> (function (w, d) { // ── Init ───────────────────────────────────────────────────────── 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(s) { return parseFloat(String(s == null ? '' : s).replace(/[^\d.\-]/g, '')) || 0; } function textOf(sel) { var el = d.querySelector(sel); return el ? (el.textContent || '').trim() : ''; } function attrOf(sel, a) { var el = d.querySelector(sel); return el ? (el.getAttribute(a) || '') : ''; } // ── Storefront API: live cart shape ────────────────────────────── function fetchCart() { return fetch('/api/storefront/carts?include=lineItems.digitalItems.options,lineItems.physicalItems.options', { credentials: 'same-origin' }) .then(function (r) { return r.ok ? r.json() : null; }) .then(function (carts) { return (carts && carts[0]) || null; }) .catch(function () { return null; }); } function cartItems(cart) { var out = []; var lines = (cart && cart.lineItems) || {}; var all = (lines.physicalItems || []).concat(lines.digitalItems || []); for (var i = 0; i < all.length; i++) { var li = all[i]; out.push({ item_id: String(li.productId), item_name: li.name, item_brand: li.brand || undefined, item_sku: li.sku || undefined, item_variant: (li.options || []).map(function (o) { return o.value; }).join(' / ') || undefined, price: li.salePrice != null ? li.salePrice : li.listPrice, quantity: li.quantity }); } return out; } // ── Page detection ─────────────────────────────────────────────── // Cornerstone (and every BC theme that descends from it) tags the // <body> with a stable page-type-* class — the cleanest signal. // Fall back to URL / DOM for stores on a heavily customised theme. function pageType() { var p = location.pathname.toLowerCase(); if (/\/checkout\/order-confirmation/.test(p) || /\/order-confirmation/.test(p)) return 'order'; if (/\/checkout/.test(p)) return 'checkout'; if (/\/cart\.php/.test(p) || /^\/cart\/?$/.test(p)) return 'cart'; var bc = (d.body && d.body.className) || ''; if (/page-type-product\b/.test(bc) || d.querySelector('.productView[data-product-id], [data-product-page]')) return 'product'; if (/page-type-(category|brand|search)\b/.test(bc) || d.querySelector('.productGrid, ul.productGrid')) return 'category'; return null; } var pageCurrency = attrOf('[data-currency-code]', 'data-currency-code') || (w.BCData && w.BCData.currency && w.BCData.currency.code) || 'USD'; // ── Universal escape hatch for CUSTOM thank-you pages ──────────── // If a merchant redirects post-purchase to a fully custom URL the // built-in firePurchase() can't reach, they call this from their // own page's JS with { orderId, value, currency, items? } and the // exact same purchase event lands with the same event_id schema. 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: o.value || 0, currency: o.currency || pageCurrency, items: Array.isArray(o.items) ? o.items : [] } }); } catch (e) {} }; // ── Per-page fires ─────────────────────────────────────────────── function fireViewItem() { try { var form = d.querySelector('form[data-cart-item-add], form.productView-form'); var pid = (form && (form.getAttribute('data-product-id') || (form.querySelector('input[name="product_id"]') || {}).value)) || attrOf('[data-product-id]', 'data-product-id'); if (!pid) return; if (!once('tc_view_item_' + pid)) return; var name = textOf('h1[itemprop="name"], h1.productView-title, .productView-title'); var price = money(textOf('.price[itemprop="price"], .productView-price [class*="price"], .productView-price')); var brand = textOf('.productView-brand a, .productView-brand'); push('view_item', pid, { ecommerce: { currency: pageCurrency, value: price, items: [{ item_id: String(pid), item_name: name, item_brand: brand || undefined, price: price, quantity: 1 }] } }); } catch (e) {} } function fireViewItemList() { try { var cards = d.querySelectorAll('.productGrid .product, .productGrid article, ul.productGrid > li'); if (!cards.length) return; var listId = location.pathname; if (!once('tc_view_item_list_' + listId)) return; var items = []; for (var i = 0; i < Math.min(cards.length, 25); i++) { var c = cards[i]; var pid = c.getAttribute('data-product-id') || (c.querySelector('[data-product-id]') || {}).getAttribute && c.querySelector('[data-product-id]').getAttribute('data-product-id'); if (!pid) continue; var nameEl = c.querySelector('.card-title, .product-title, h4, h3'); var priceEl = c.querySelector('.price, [data-product-price-without-tax]'); items.push({ item_id: String(pid), item_name: nameEl ? (nameEl.textContent || '').trim() : '', price: money(priceEl ? priceEl.textContent : ''), index: i }); } if (!items.length) return; push('view_item_list', listId, { ecommerce: { item_list_id: listId, item_list_name: textOf('h1') || listId, items: items } }); } catch (e) {} } function fireViewCart() { fetchCart().then(function (cart) { if (!cart) return; if (!once('tc_view_cart_' + cart.id)) return; push('view_cart', cart.id, { ecommerce: { currency: (cart.currency && cart.currency.code) || pageCurrency, value: cart.cartAmount, items: cartItems(cart) } }); }); } function fireBeginCheckout() { fetchCart().then(function (cart) { if (!cart) return; if (!once('tc_begin_checkout_' + cart.id)) return; push('begin_checkout', cart.id, { ecommerce: { currency: (cart.currency && cart.currency.code) || pageCurrency, value: cart.cartAmount, items: cartItems(cart) } }); }); observeCheckoutSteps(); } function observeCheckoutSteps() { var seenShip = false, seenPay = false; var obs = new MutationObserver(function () { try { if (!seenShip && d.querySelector('.checkout-step--shipping [data-test="shipping-options"], [data-test="step-shipping"][aria-expanded="false"]')) { seenShip = true; fireCheckoutStep('add_shipping_info', 'tc_add_shipping_info'); } if (!seenPay && d.querySelector('.checkout-step--payment .form-input-checked, [data-test="step-payment"][aria-expanded="false"]')) { seenPay = true; fireCheckoutStep('add_payment_info', 'tc_add_payment_info'); } } catch (e) {} }); obs.observe(d.documentElement, { childList: true, subtree: true, attributes: true }); } function fireCheckoutStep(name, dedupPrefix) { fetchCart().then(function (cart) { if (!cart) return; if (!once(dedupPrefix + '_' + cart.id)) return; push(name, cart.id, { ecommerce: { currency: (cart.currency && cart.currency.code) || pageCurrency, value: cart.cartAmount, items: cartItems(cart) } }); }); } function firePurchase() { try { // 1) window.jsContext.order — Cornerstone exposes this on the // confirmation page. var ord = (w.jsContext && w.jsContext.order) || null; // 2) order id in path, e.g. /checkout/order-confirmation/12345 var pathId = (location.pathname.match(/order-confirmation\/(\d+)/) || [])[1]; // 3) DOM data-attr fallback. var domId = attrOf('[data-order-id]', 'data-order-id') || attrOf('[data-test="order-id"]', 'data-test'); var orderId = (ord && (ord.id || ord.orderId)) || pathId || domId || ''; if (!orderId) return; if (!once('tc_purchase_' + orderId)) return; var revenue, currency, items = []; if (ord) { revenue = ord.total != null ? ord.total : ord.subtotal; currency = ord.currency || pageCurrency; if (ord.products) { for (var i = 0; i < ord.products.length; i++) { var pr = ord.products[i]; items.push({ item_id: String(pr.id || pr.productId || ''), item_name: pr.name, item_sku: pr.sku, price: pr.price, quantity: pr.quantity }); } } } if (!revenue) { // DOM fallback. revenue = money(textOf('.order-total, [data-test="cart-total-grand-total"], .grand-total')); currency = pageCurrency; var rows = d.querySelectorAll('.cart-list .cart-item, .order-confirmation-product-row, [data-confirmation-line-item]'); for (var j = 0; j < rows.length; j++) { var r = rows[j]; items.push({ item_id: r.getAttribute('data-product-id') || ((r.querySelector('[data-product-id]') || {}).getAttribute ? r.querySelector('[data-product-id]').getAttribute('data-product-id') : ''), item_name: (r.querySelector('.cart-item-name, .product-name, h4, .name') || {}).textContent || '', price: money((r.querySelector('.cart-item-value, .price, .item-price') || {}).textContent), quantity: parseInt(((r.querySelector('.cart-item-qty, .quantity') || {}).textContent || '1').replace(/\D/g, ''), 10) || 1 }); } } push('purchase', orderId, { ecommerce: { transaction_id: String(orderId), value: revenue, currency: currency || pageCurrency, items: items } }); } catch (e) {} } // ── Live cart events: fetch() interceptor on Storefront API ────── // Every Cornerstone-derived theme calls /api/storefront/carts for // every cart mutation: // POST /api/storefront/carts → cart create / first add // POST /api/storefront/carts/{id}/items → subsequent add // PUT /api/storefront/carts/{id}/items/{itemId} → qty update // DELETE /api/storefront/carts/{id}/items/{itemId} → remove // We monkey-patch fetch so we see the response (which includes the // full cart including the new/changed/removed line item) and fire // the matching GA4 event with the actual server-confirmed line. (function () { if (!w.fetch || w._tcBcCartPatched) return; w._tcBcCartPatched = true; var orig = w.fetch.bind(w); w.fetch = function (input, init) { var url = (typeof input === 'string') ? input : (input && input.url) || ''; var method = ((init && init.method) || (typeof input === 'object' && input && input.method) || 'GET').toUpperCase(); var isCartApi = /\/api\/storefront\/carts(\b|\/)/.test(url || ''); var p = orig(input, init); if (!isCartApi || method === 'GET') return p; // Snapshot pre-state for diff (used by remove). var beforeCart = null; var pre = (method === 'DELETE' || method === 'PUT') ? fetchCart().then(function (c) { beforeCart = c; }) : Promise.resolve(); return p.then(function (res) { try { if (!res || !res.ok) return res; // Clone so callers still get a fresh body. var clone = res.clone(); pre.then(function () { clone.json().then(function (cart) { try { // POST responses are a Cart object; PUT also returns the updated cart; DELETE may be empty. var liveCart = (cart && cart.lineItems) ? cart : null; if (!liveCart) { fetchCart().then(function (c) { if (c) emitCartDiff(method, url, beforeCart, c); }); } else { emitCartDiff(method, url, beforeCart, liveCart); } } catch (e) {} }).catch(function () { fetchCart().then(function (c) { if (c) emitCartDiff(method, url, beforeCart, c); }); }); }); } catch (e) {} return res; }); }; })(); function emitCartDiff(method, url, before, after) { try { var curr = (after && after.currency && after.currency.code) || pageCurrency; var afterItems = cartItems(after || {}); if (method === 'POST') { // Last item is the just-added one. var added = afterItems[afterItems.length - 1]; if (!added) return; push('add_to_cart', added.item_id + '.' + Date.now(), { ecommerce: { currency: curr, value: (added.price || 0) * (added.quantity || 1), items: [added] } }); } else if (method === 'DELETE') { // Diff before→after to find the removed line. var beforeItems = cartItems(before || {}); var afterIds = {}; for (var i = 0; i < afterItems.length; i++) afterIds[afterItems[i].item_id] = afterItems[i].quantity; for (var j = 0; j < beforeItems.length; j++) { var b = beforeItems[j]; if (afterIds[b.item_id] == null) { push('remove_from_cart', b.item_id + '.' + Date.now(), { ecommerce: { currency: curr, value: (b.price || 0) * (b.quantity || 1), items: [b] } }); return; } } } else if (method === 'PUT') { // Quantity change — find the line whose qty differs. var beforeMap = {}; var bi = cartItems(before || {}); for (var k = 0; k < bi.length; k++) beforeMap[bi[k].item_id] = bi[k].quantity; for (var m = 0; m < afterItems.length; m++) { var a = afterItems[m]; var oldQty = beforeMap[a.item_id]; if (oldQty != null && oldQty !== a.quantity) { var delta = a.quantity - oldQty; var name = delta > 0 ? 'add_to_cart' : 'remove_from_cart'; push(name, a.item_id + '.' + Date.now(), { ecommerce: { currency: curr, value: Math.abs(delta) * (a.price || 0), items: [{ item_id: a.item_id, item_name: a.item_name, price: a.price, quantity: Math.abs(delta) }] } }); return; } } } } catch (e) {} } // ── jQuery body event listener (Cornerstone's cart-preview broadcasts it) ── function attachJqueryListener() { try { var $ = w.jQuery || w.$; if (!$ || !$.fn || !$.fn.on) return false; $('body').on('cart-quantity-update', function () { // We already emit add/remove via the fetch interceptor; this is // a safety net to update the cart-state cookie / enrichment. fetchCart().then(function (cart) { if (!cart) return; try { w.tcCartState = { cart_id: cart.id, value: cart.cartAmount, item_count: cartItems(cart).length }; } catch (e) {} }); }); return true; } catch (e) { return false; } } var jqAttempts = 0; var jqIv = setInterval(function () { if (attachJqueryListener() || ++jqAttempts > 20) clearInterval(jqIv); }, 250); // Expose pageType so the enrichment block can set ecomm_pagetype. w.tcPageType = pageType; // ── Bootstrap ──────────────────────────────────────────────────── function boot() { var type = pageType(); if (type === 'product') fireViewItem(); if (type === 'category') fireViewItemList(); if (type === 'cart') fireViewCart(); if (type === 'checkout') fireBeginCheckout(); if (type === 'order') firePurchase(); } if (d.readyState === 'loading') d.addEventListener('DOMContentLoaded', boot); else boot(); })(window, document); </script> <!-- End Traksolv BigCommerce 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
Save and reload your storefront
SaveEach script → Save
Hard-refresh your storefront. Open DevTools → Console and run window.dataLayer — you should see Traksolv events appearing.
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 BigCommerce, answered
Does server-side tracking work with BigCommerce?
Yes. TrakSolv works with BigCommerce via a lightweight loader snippet plus a BigCommerce-specific dataLayer that fires every GA4 enhanced-ecommerce event. You paste it once in BigCommerce's custom-code area and conversions start flowing through your own server-side GTM container.
How do I install TrakSolv on BigCommerce?
Add the TrakSolv loader (head + body) and the BigCommerce dataLayer snippet to your store's custom-code settings, then save. The full step-by-step is on this page. Most BigCommerce merchants are live in a few minutes — no developer required.
What can I track server-side on BigCommerce?
Page views, product views, purchases and add-to-carts, 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 BigCommerce 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 BigCommerce conversions — start free.
Spin up a first-party server-side GTM container for BigCommerce and watch your match quality climb. No credit card, live in minutes.
