Checkout / payments

Server-Side Tracking for Lemon Squeezy

Stop losing conversions to iOS ITP, short-lived cookies, and ad-blockers. TrakSolv hosts a first-party server-side GTM container for your Lemon Squeezy 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 Lemon Squeezy dataLayerGA4 · Meta CAPI · Google Ads
Why it matters for Lemon Squeezy

The pixel alone is leaking your data.

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

Recover conversions lost to ITP & ad-blockers

On Lemon Squeezy, 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 Lemon Squeezy 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 Lemon Squeezy 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 Lemon Squeezy

Paste the TrakSolv loader and the Lemon Squeezy 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 Lemon Squeezy

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

    Open the page that renders your Lemon Squeezy buy buttons

    Navigate

    Your own site / framework — anywhere lemon.js is loaded

    Lemon Squeezy's checkout is an overlay on YOUR site (not a separate domain) — so Traksolv installs on the SAME page as lemon.js. If you use Next.js, paste in <Head>; for WordPress use a code-injection plugin; for a static site, paste in the global template.

  2. 2

    Ensure lemon.js is loaded first

    Navigate

    <script src="https://app.lemonsqueezy.com/js/lemon.js" defer></script>

    Lemon Squeezy already documents this script — make sure it's on the page BEFORE the Traksolv snippets.

  3. 3

    Loader — head half

    Loader • Head

    <head> of every page that has a Lemon Squeezy buy button

  4. 4

    Loader — body half + DataLayer

    Loader • Body

    End of <body> on the same pages → paste the Loader Body snippet FIRST

  5. 5

    DataLayer snippet

    DataLayer

    Same location, after the Loader Body → paste the DataLayer snippet

    Registers a LemonSqueezy.Setup eventHandler. When the overlay checkout fires Checkout.Success, the snippet auto-reads data.data.order.data.attributes (identifier, customer_id, total, currency, tax, first_order_item) and pushes purchase to the dataLayer.

    HTML
    <!-- Traksolv · Lemon Squeezy DataLayer — paste on every page that uses lemon.js checkout -->
    <script>
    (function (w) {
      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; }
      }
    
      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) {}
      };
    
      // The Lemon.js order payload is JSON:API.
      // data.data.order.data.attributes contains:
      //   store_id, customer_id, identifier, order_number, user_email,
      //   user_name, currency, currency_rate, subtotal, setup_fee,
      //   discount_total, tax, total, status, refunded, refunded_at,
      //   tax_inclusive, first_order_item, ...
      function handleCheckoutSuccess(data) {
        try {
          var orderEnvelope = data && data.data && data.data.order;
          var attrs = orderEnvelope && orderEnvelope.data && orderEnvelope.data.attributes;
          if (!attrs) return;
          var oid = attrs.identifier || attrs.order_number || String(Date.now());
          if (!once('tc_purchase_' + oid)) return;
          // Lemon Squeezy returns totals in lowest currency unit (cents).
          var total = (Number(attrs.total) || 0) / 100;
          var tax   = (Number(attrs.tax)   || 0) / 100;
          var discount = (Number(attrs.discount_total) || 0) / 100;
          var currency = attrs.currency || 'USD';
          var firstItem = attrs.first_order_item || {};
          push('purchase', String(oid), {
            ecommerce: {
              transaction_id: String(oid),
              affiliation:    'LemonSqueezy',
              value:          total,
              tax:            tax || undefined,
              discount:       discount || undefined,
              currency:       currency,
              items: [{
                item_id:       String(firstItem.product_id || firstItem.variant_id || 'lemon-product'),
                item_name:     firstItem.product_name || 'LemonSqueezy product',
                item_variant:  firstItem.variant_name || undefined,
                item_category: 'digital',
                price:         total,
                quantity:      Number(firstItem.quantity) || 1
              }]
            },
            user_id:    attrs.customer_id ? String(attrs.customer_id) : undefined,
            user_email: attrs.user_email  || undefined
          });
        } catch (e) {}
      }
    
      function register() {
        try {
          if (!w.LemonSqueezy || typeof w.LemonSqueezy.Setup !== 'function') return false;
          w.LemonSqueezy.Setup({
            eventHandler: function (data) {
              try {
                if (!data || !data.event) return;
                log('observed ' + data.event, data);
                if (data.event === 'Checkout.Success') handleCheckoutSuccess(data);
              } catch (e) {}
            }
          });
          return true;
        } catch (e) { return false; }
      }
    
      // lemon.js may load asynchronously. Poll briefly until window.LemonSqueezy
      // is available, then register our event handler.
      var attempts = 0;
      function tryRegister() {
        if (register()) return;
        if (typeof w.createLemonSqueezy === 'function' && !w.LemonSqueezy) {
          try { w.createLemonSqueezy(); } catch (e) {}
          if (register()) return;
        }
        if (++attempts > 100) return; // ~10s
        setTimeout(tryRegister, 100);
      }
      if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', tryRegister);
      else tryRegister();
    
      w.tcPageType = function () {
        return /thank|success|order|receipt/i.test(location.pathname) ? 'thank-you' : 'other';
      };
    })(window);
    </script>
    <!-- End Traksolv Lemon Squeezy 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

    Test with a $0 product or test mode

    Save

    Lemon Squeezy → Settings → test mode → buy a $0 product to verify the purchase event fires

    ?tcDebug=1 in the URL will log every Lemon.js event the snippet observes alongside the purchase push.

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 Lemon Squeezy, answered

Does server-side tracking work with Lemon Squeezy?

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

How do I install TrakSolv on Lemon Squeezy?

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

What can I track server-side on Lemon Squeezy?

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 Lemon Squeezy 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 Lemon Squeezy conversions — start free.

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

WhatsApp
Server-Side Tracking for Lemon Squeezy — TrakSolv | Traksolv