Checkout / payments

Server-Side Tracking for Foxy.io

Stop losing conversions to iOS ITP, short-lived cookies, and ad-blockers. TrakSolv hosts a first-party server-side GTM container for your Foxy.io store and forwards clean, deduplicated conversions to GA4, Meta CAPI, Google Ads and more. Live in minutes, flat monthly pricing.

No credit card to startFlat monthly pricingNative Foxy.io dataLayerGA4 · Meta CAPI · Google Ads
Why it matters for Foxy.io

The pixel alone is leaking your data.

Browser-side tracking on Foxy.io loses a growing share of conversions. Here's what moving to a server-side setup fixes.

Recover conversions lost to ITP & ad-blockers

On Foxy.io, 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 checkout completions 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 Foxy.io 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 Foxy.io dataLayer fires every GA4 enhanced-ecommerce event with a stable event_id, so your server-side container can forward checkout completions to GA4, Meta CAPI, Google Ads Enhanced Conversions, TikTok and more — deduplicated against the browser pixel, with higher match quality.

Step by step

How to set up server-side tracking on Foxy.io

Paste the TrakSolv loader and the Foxy.io 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 Foxy.io

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. 1

    Make sure loader.foxycart.com is loaded on your site

    Navigate

    <head>: <script defer src="https://YOUR-STORE.foxycart.com/cart/cart.js"></script>

    Foxy's standard install — must be present BEFORE the Traksolv snippets so FC.client is available when our snippet polls for it.

  2. 2

    Loader — head half (merchant site)

    Loader • Head

    <head> of every page on your site → paste the Loader Head snippet

  3. 3

    Loader — body half + DataLayer (merchant site)

    Loader • Body

    End of <body> on every page → paste the Loader Body snippet FIRST

  4. 4

    DataLayer snippet (merchant site)

    DataLayer

    Same end-of-body location → paste the DataLayer snippet right after the Body

    Subscribes to FC.client events for view_item / add_to_cart / remove_from_cart / view_cart on your site.

    HTML
    <!-- Traksolv · Foxy.io DataLayer — paste in <head> on site AND in Foxy Receipt template -->
    <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) { var el = d.querySelector(sel); return el ? (el.textContent || '').trim() : ''; }
      function attrOf(sel, a) { var el = d.querySelector(sel); return el ? (el.getAttribute(a) || '') : ''; }
    
      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) {}
      };
    
      // Detect which side we're running on:
      //   - Merchant site: has Foxy add-to-cart forms
      //   - Foxy receipt:   window.FC.json present OR /receipt URL
      function side() {
        if (w.FC && w.FC.json) return 'receipt';
        if (/\/(receipt|checkout)\b/.test(location.pathname)) return 'foxy-checkout';
        return 'merchant';
      }
      w.tcPageType = function () {
        var s = side();
        if (s === 'receipt') return 'order';
        if (s === 'foxy-checkout') return 'checkout';
        return 'other';
      };
    
      function foxyProductToItem(p, i) {
        if (!p) return null;
        return {
          item_id:       String(p.code || p.id || p.name || ''),
          item_name:     p.name || '',
          item_category: p.category || undefined,
          item_variant:  p.options ? (Array.isArray(p.options) ? p.options.join(' / ') : String(p.options)) : undefined,
          price:         Number(p.price) || 0,
          quantity:      Number(p.quantity) || 1,
          index:         typeof i === 'number' ? i : undefined
        };
      }
    
      // ── Merchant-site events via FC.client.on() ──
      function wireMerchantSite() {
        function attach() {
          try {
            if (!w.FC || !w.FC.client || typeof w.FC.client.on !== 'function') return false;
    
            w.FC.client.on('cart-submit.done', function (params) {
              try {
                var product = (params && params.product) || (params && params.json && params.json.products && params.json.products[0]) || params || {};
                var item = foxyProductToItem(product);
                if (!item) return;
                push('add_to_cart', item.item_id + '.' + Date.now(), {
                  ecommerce: {
                    currency: (params && params.currency_code) || 'USD',
                    value:    item.price * item.quantity,
                    items:    [item]
                  }
                });
              } catch (e) {}
            });
    
            w.FC.client.on('cart-item-remove.done', function (params) {
              try {
                var product = (params && params.product) || params || {};
                var item = foxyProductToItem(product);
                if (!item) return;
                push('remove_from_cart', item.item_id + '.' + Date.now(), {
                  ecommerce: {
                    currency: (params && params.currency_code) || 'USD',
                    value:    item.price * item.quantity,
                    items:    [item]
                  }
                });
              } catch (e) {}
            });
    
            w.FC.client.on('render.done', function () {
              try {
                // Sidecart re-rendered. If it's now visible AND has items, fire view_cart once per session.
                var sidecart = d.querySelector('#fc, .fc-sidecart, .fc-cart-content');
                if (!sidecart) return;
                var cart = (w.FC && w.FC.json && w.FC.json.cart) || (w.FC && w.FC.cart) || null;
                if (!cart || !cart.products || !cart.products.length) return;
                var key = String(cart.id || cart.session_id || 'cart');
                if (!once('tc_view_cart_' + key)) return;
                var items = (cart.products || []).map(function (p, i) { return foxyProductToItem(p, i); });
                var total = Number(cart.total_order) || items.reduce(function (s, x) { return s + (x.price || 0) * (x.quantity || 1); }, 0);
                push('view_cart', key, {
                  ecommerce: {
                    currency: cart.currency_code || 'USD',
                    value:    total,
                    items:    items
                  }
                });
              } catch (e) {}
            });
    
            return true;
          } catch (e) { return false; }
        }
    
        // Foxy loads async — try the FC.onLoad hook, fall back to polling.
        if (typeof w.FC !== 'undefined' && w.FC.client) { attach(); return; }
        try {
          w.FC = w.FC || {};
          var prev = w.FC.onLoad;
          w.FC.onLoad = function () {
            try { if (typeof prev === 'function') prev(); } catch (e) {}
            attach();
          };
        } catch (e) {}
        var attempts = 0;
        var iv = setInterval(function () {
          if (attach() || ++attempts > 100) clearInterval(iv);
        }, 100);
    
        // PDP view_item — read Foxy add-to-cart form on the page.
        function fireViewItem() {
          try {
            var form = d.querySelector('form.foxycart-add-to-cart, form[action*="foxycart.com/cart"]');
            if (!form) return;
            var code  = (form.querySelector('input[name="code"]') || {}).value ||
                        (form.querySelector('input[name="id"]') || {}).value ||
                        attrOf('meta[itemprop="productID"]', 'content');
            var name  = (form.querySelector('input[name="name"]') || {}).value ||
                        textOf('h1, .product-name, h1[itemprop="name"]');
            var price = money((form.querySelector('input[name="price"]') || {}).value ||
                              textOf('[itemprop="price"], .price'));
            if (!code) return;
            if (!once('tc_view_item_' + code)) return;
            push('view_item', String(code), {
              ecommerce: {
                currency: 'USD',
                value:    price,
                items: [{ item_id: String(code), item_name: name, price: price, quantity: 1 }]
              }
            });
          } catch (e) {}
        }
        if (d.readyState === 'loading') d.addEventListener('DOMContentLoaded', fireViewItem);
        else fireViewItem();
      }
    
      // ── Foxy receipt-template — read window.FC.json + emit purchase ──
      function wireReceipt() {
        try {
          var json = (w.FC && w.FC.json) || {};
          var checkout = json.checkout || json;
          var cart = json.cart || {};
          var products = cart.products || (checkout && checkout.products) || [];
          var oid = checkout.id || cart.id || cart.session_id || ('foxy.' + Date.now());
          if (!once('tc_purchase_' + oid)) return;
          var items = products.map(function (p, i) { return foxyProductToItem(p, i); });
          var total = Number(checkout.total_order) || Number(cart.total_order) ||
                      items.reduce(function (s, x) { return s + (x.price || 0) * (x.quantity || 1); }, 0);
          push('purchase', String(oid), {
            ecommerce: {
              transaction_id: String(oid),
              affiliation:    'Foxy.io',
              value:          total,
              tax:            Number(checkout.total_tax || cart.total_tax) || undefined,
              shipping:       Number(checkout.total_shipping || cart.total_shipping) || undefined,
              discount:       Number(checkout.total_discount || cart.total_discount) || undefined,
              currency:       checkout.currency_code || cart.currency_code || 'USD',
              items:          items
            },
            user_email: (checkout.customer_email) || (cart.customer_email) || undefined
          });
        } catch (e) {}
      }
    
      function boot() {
        var s = side();
        if (s === 'receipt' || s === 'foxy-checkout') wireReceipt();
        else wireMerchantSite();
      }
      if (d.readyState === 'loading') d.addEventListener('DOMContentLoaded', boot);
      else boot();
    })(window, document);
    </script>
    <!-- End Traksolv Foxy.io 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. 5

    ALSO paste DataLayer in Foxy Receipt template (CRITICAL for purchase)

    DataLayer

    Foxy Admin → Templates → Receipt → paste the same DataLayer snippet at the top of the template

    Foxy's checkout + receipt run on a Foxy-hosted domain, NOT your site. window.FC.json with the order data only exists on the receipt template — that's where the purchase event needs to fire. Snippet auto-detects which side it's on.

    HTML
    <!-- Traksolv · Foxy.io DataLayer — paste in <head> on site AND in Foxy Receipt template -->
    <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) { var el = d.querySelector(sel); return el ? (el.textContent || '').trim() : ''; }
      function attrOf(sel, a) { var el = d.querySelector(sel); return el ? (el.getAttribute(a) || '') : ''; }
    
      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) {}
      };
    
      // Detect which side we're running on:
      //   - Merchant site: has Foxy add-to-cart forms
      //   - Foxy receipt:   window.FC.json present OR /receipt URL
      function side() {
        if (w.FC && w.FC.json) return 'receipt';
        if (/\/(receipt|checkout)\b/.test(location.pathname)) return 'foxy-checkout';
        return 'merchant';
      }
      w.tcPageType = function () {
        var s = side();
        if (s === 'receipt') return 'order';
        if (s === 'foxy-checkout') return 'checkout';
        return 'other';
      };
    
      function foxyProductToItem(p, i) {
        if (!p) return null;
        return {
          item_id:       String(p.code || p.id || p.name || ''),
          item_name:     p.name || '',
          item_category: p.category || undefined,
          item_variant:  p.options ? (Array.isArray(p.options) ? p.options.join(' / ') : String(p.options)) : undefined,
          price:         Number(p.price) || 0,
          quantity:      Number(p.quantity) || 1,
          index:         typeof i === 'number' ? i : undefined
        };
      }
    
      // ── Merchant-site events via FC.client.on() ──
      function wireMerchantSite() {
        function attach() {
          try {
            if (!w.FC || !w.FC.client || typeof w.FC.client.on !== 'function') return false;
    
            w.FC.client.on('cart-submit.done', function (params) {
              try {
                var product = (params && params.product) || (params && params.json && params.json.products && params.json.products[0]) || params || {};
                var item = foxyProductToItem(product);
                if (!item) return;
                push('add_to_cart', item.item_id + '.' + Date.now(), {
                  ecommerce: {
                    currency: (params && params.currency_code) || 'USD',
                    value:    item.price * item.quantity,
                    items:    [item]
                  }
                });
              } catch (e) {}
            });
    
            w.FC.client.on('cart-item-remove.done', function (params) {
              try {
                var product = (params && params.product) || params || {};
                var item = foxyProductToItem(product);
                if (!item) return;
                push('remove_from_cart', item.item_id + '.' + Date.now(), {
                  ecommerce: {
                    currency: (params && params.currency_code) || 'USD',
                    value:    item.price * item.quantity,
                    items:    [item]
                  }
                });
              } catch (e) {}
            });
    
            w.FC.client.on('render.done', function () {
              try {
                // Sidecart re-rendered. If it's now visible AND has items, fire view_cart once per session.
                var sidecart = d.querySelector('#fc, .fc-sidecart, .fc-cart-content');
                if (!sidecart) return;
                var cart = (w.FC && w.FC.json && w.FC.json.cart) || (w.FC && w.FC.cart) || null;
                if (!cart || !cart.products || !cart.products.length) return;
                var key = String(cart.id || cart.session_id || 'cart');
                if (!once('tc_view_cart_' + key)) return;
                var items = (cart.products || []).map(function (p, i) { return foxyProductToItem(p, i); });
                var total = Number(cart.total_order) || items.reduce(function (s, x) { return s + (x.price || 0) * (x.quantity || 1); }, 0);
                push('view_cart', key, {
                  ecommerce: {
                    currency: cart.currency_code || 'USD',
                    value:    total,
                    items:    items
                  }
                });
              } catch (e) {}
            });
    
            return true;
          } catch (e) { return false; }
        }
    
        // Foxy loads async — try the FC.onLoad hook, fall back to polling.
        if (typeof w.FC !== 'undefined' && w.FC.client) { attach(); return; }
        try {
          w.FC = w.FC || {};
          var prev = w.FC.onLoad;
          w.FC.onLoad = function () {
            try { if (typeof prev === 'function') prev(); } catch (e) {}
            attach();
          };
        } catch (e) {}
        var attempts = 0;
        var iv = setInterval(function () {
          if (attach() || ++attempts > 100) clearInterval(iv);
        }, 100);
    
        // PDP view_item — read Foxy add-to-cart form on the page.
        function fireViewItem() {
          try {
            var form = d.querySelector('form.foxycart-add-to-cart, form[action*="foxycart.com/cart"]');
            if (!form) return;
            var code  = (form.querySelector('input[name="code"]') || {}).value ||
                        (form.querySelector('input[name="id"]') || {}).value ||
                        attrOf('meta[itemprop="productID"]', 'content');
            var name  = (form.querySelector('input[name="name"]') || {}).value ||
                        textOf('h1, .product-name, h1[itemprop="name"]');
            var price = money((form.querySelector('input[name="price"]') || {}).value ||
                              textOf('[itemprop="price"], .price'));
            if (!code) return;
            if (!once('tc_view_item_' + code)) return;
            push('view_item', String(code), {
              ecommerce: {
                currency: 'USD',
                value:    price,
                items: [{ item_id: String(code), item_name: name, price: price, quantity: 1 }]
              }
            });
          } catch (e) {}
        }
        if (d.readyState === 'loading') d.addEventListener('DOMContentLoaded', fireViewItem);
        else fireViewItem();
      }
    
      // ── Foxy receipt-template — read window.FC.json + emit purchase ──
      function wireReceipt() {
        try {
          var json = (w.FC && w.FC.json) || {};
          var checkout = json.checkout || json;
          var cart = json.cart || {};
          var products = cart.products || (checkout && checkout.products) || [];
          var oid = checkout.id || cart.id || cart.session_id || ('foxy.' + Date.now());
          if (!once('tc_purchase_' + oid)) return;
          var items = products.map(function (p, i) { return foxyProductToItem(p, i); });
          var total = Number(checkout.total_order) || Number(cart.total_order) ||
                      items.reduce(function (s, x) { return s + (x.price || 0) * (x.quantity || 1); }, 0);
          push('purchase', String(oid), {
            ecommerce: {
              transaction_id: String(oid),
              affiliation:    'Foxy.io',
              value:          total,
              tax:            Number(checkout.total_tax || cart.total_tax) || undefined,
              shipping:       Number(checkout.total_shipping || cart.total_shipping) || undefined,
              discount:       Number(checkout.total_discount || cart.total_discount) || undefined,
              currency:       checkout.currency_code || cart.currency_code || 'USD',
              items:          items
            },
            user_email: (checkout.customer_email) || (cart.customer_email) || undefined
          });
        } catch (e) {}
      }
    
      function boot() {
        var s = side();
        if (s === 'receipt' || s === 'foxy-checkout') wireReceipt();
        else wireMerchantSite();
      }
      if (d.readyState === 'loading') d.addEventListener('DOMContentLoaded', boot);
      else boot();
    })(window, document);
    </script>
    <!-- End Traksolv Foxy.io 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 -->
  6. 6

    Save and test

    Save

    Save the Receipt template → place a $0 test transaction → verify purchase fires on the receipt page

    ?tcDebug=1 in the URL will log FC events on the site AND the FC.json object on the receipt page.

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.

FAQ

Server-side tracking for Foxy.io, answered

Does server-side tracking work with Foxy.io?

Yes. TrakSolv works with Foxy.io via a lightweight loader snippet plus a Foxy.io-specific dataLayer that fires every GA4 enhanced-ecommerce event. You paste it once in Foxy.io's custom-code area and conversions start flowing through your own server-side GTM container.

How do I install TrakSolv on Foxy.io?

Add the TrakSolv loader (head + body) and the Foxy.io dataLayer snippet to your store's custom-code settings, then save. The full step-by-step is on this page. Most Foxy.io merchants are live in a few minutes — no developer required.

What can I track server-side on Foxy.io?

Page views, product views, checkout completions, 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 Foxy.io 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 Foxy.io conversions — start free.

Spin up a first-party server-side GTM container for Foxy.io and watch your match quality climb. No credit card, live in minutes.

WhatsApp
Server-Side Tracking for Foxy.io — TrakSolv | Traksolv