Server-Side Tracking for Gumroad
Stop losing conversions to iOS ITP, short-lived cookies, and ad-blockers. TrakSolv hosts a first-party server-side GTM container for your Gumroad 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 Gumroad loses a growing share of conversions. Here's what moving to a server-side setup fixes.
Recover conversions lost to ITP & ad-blockers
On Gumroad, 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 Gumroad 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 Gumroad 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.
How to set up server-side tracking on Gumroad
Paste the TrakSolv loader and the Gumroad 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 Gumroad
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 Third-party Analytics
NavigateGumroad → Settings → Third-party Analytics → Add snippet
Gumroad lets you paste snippets with a Location: Product page / Receipt / Both, scoped per-product or All Products. We'll add the Loader to BOTH and split the DataLayer into a Product snippet + Receipt snippet.
- 2
Loader — head half (BOTH locations)
Loader • HeadAdd snippet → Location: Both → Products: All Products → paste the Loader Head snippet
- 3
Loader — body half (BOTH locations)
Loader • BodyAdd snippet → Location: Both → Products: All Products → paste the Loader Body snippet
- 4
DataLayer — Product page snippet
DataLayerAdd snippet → Location: PRODUCT page → Products: All Products → paste 'Snippet 1 of 2' from the DataLayer card below
Reads the rendered product card to fire view_item with the product name + price + slug.
HTML/* === Snippet 1 of 2 — Location: PRODUCT page === */ <!-- Traksolv · Gumroad DataLayer — Settings → Third-party Analytics → Add snippet → Location: Product page --> <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; } } function textOf(sel, root) { var el = (root || document).querySelector(sel); return el ? (el.textContent || '').trim() : ''; } function money(v) { return parseFloat(String(v == null ? '' : v).replace(/[^\d.\-]/g, '')) || 0; } 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) {} }; // Product page — read the rendered product card. function fireViewItem() { try { var title = textOf('h1.product-name, h1[itemprop="name"], .product-card h1') || (document.title || ''); var priceTxt = textOf('[itemprop="price"], .product-price, .price') || ''; var price = money(priceTxt); // Gumroad product URL: gumroad.com/l/<permalink> — use the slug as item_id. var slug = (location.pathname.match(/\/l\/([\w-]+)/) || [])[1] || location.pathname; if (!once('tc_view_item_' + slug)) return; push('view_item', String(slug), { ecommerce: { currency: 'USD', value: price, items: [{ item_id: String(slug), item_name: title, item_category: 'digital', price: price, quantity: 1 }] } }); } catch (e) {} } if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', fireViewItem); else fireViewItem(); })(window); </script> <!-- End Traksolv Gumroad DataLayer · Product page --> <!-- 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 --> /* === Snippet 2 of 2 — Location: RECEIPT === */ <!-- Traksolv · Gumroad DataLayer — Settings → Third-party Analytics → Add snippet → Location: Receipt --> <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; } } function textOf(sel, root) { var el = (root || document).querySelector(sel); return el ? (el.textContent || '').trim() : ''; } function money(v) { return parseFloat(String(v == null ? '' : v).replace(/[^\d.\-]/g, '')) || 0; } 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) {} }; // Receipt — Gumroad auto-substitutes these placeholders at render time. // $VALUE → numeric sale value, $CURRENCY → ISO code, $ORDER → order number. var orderId = "$ORDER"; var value = parseFloat("$VALUE") || 0; var currency = "$CURRENCY" || 'USD'; // Try to read product name + permalink from the receipt page DOM // (Gumroad renders the purchased product card on the receipt). var productName = textOf('.product-name, h1[itemprop="name"], .receipt h1') || 'Gumroad product'; var permalink = (textOf('[data-product-permalink]') || (document.querySelector('a[href*="/l/"]') || {}).href || '').match(/\/l\/([\w-]+)/); var itemId = permalink && permalink[1] ? permalink[1] : orderId; w.tcFirePurchase({ orderId: orderId, value: value, currency: currency, items: [{ item_id: String(itemId), item_name: productName, item_category: 'digital', price: value, quantity: 1 }] }); })(window); </script> <!-- End Traksolv Gumroad DataLayer · Receipt --> <!-- 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
DataLayer — Receipt snippet
DataLayerAdd snippet → Location: RECEIPT → Products: All Products → paste 'Snippet 2 of 2' from the DataLayer card below
Uses Gumroad's $VALUE / $CURRENCY / $ORDER macros — Gumroad substitutes them at render time, your purchase event lands with real values.
HTML/* === Snippet 1 of 2 — Location: PRODUCT page === */ <!-- Traksolv · Gumroad DataLayer — Settings → Third-party Analytics → Add snippet → Location: Product page --> <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; } } function textOf(sel, root) { var el = (root || document).querySelector(sel); return el ? (el.textContent || '').trim() : ''; } function money(v) { return parseFloat(String(v == null ? '' : v).replace(/[^\d.\-]/g, '')) || 0; } 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) {} }; // Product page — read the rendered product card. function fireViewItem() { try { var title = textOf('h1.product-name, h1[itemprop="name"], .product-card h1') || (document.title || ''); var priceTxt = textOf('[itemprop="price"], .product-price, .price') || ''; var price = money(priceTxt); // Gumroad product URL: gumroad.com/l/<permalink> — use the slug as item_id. var slug = (location.pathname.match(/\/l\/([\w-]+)/) || [])[1] || location.pathname; if (!once('tc_view_item_' + slug)) return; push('view_item', String(slug), { ecommerce: { currency: 'USD', value: price, items: [{ item_id: String(slug), item_name: title, item_category: 'digital', price: price, quantity: 1 }] } }); } catch (e) {} } if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', fireViewItem); else fireViewItem(); })(window); </script> <!-- End Traksolv Gumroad DataLayer · Product page --> <!-- 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 --> /* === Snippet 2 of 2 — Location: RECEIPT === */ <!-- Traksolv · Gumroad DataLayer — Settings → Third-party Analytics → Add snippet → Location: Receipt --> <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; } } function textOf(sel, root) { var el = (root || document).querySelector(sel); return el ? (el.textContent || '').trim() : ''; } function money(v) { return parseFloat(String(v == null ? '' : v).replace(/[^\d.\-]/g, '')) || 0; } 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) {} }; // Receipt — Gumroad auto-substitutes these placeholders at render time. // $VALUE → numeric sale value, $CURRENCY → ISO code, $ORDER → order number. var orderId = "$ORDER"; var value = parseFloat("$VALUE") || 0; var currency = "$CURRENCY" || 'USD'; // Try to read product name + permalink from the receipt page DOM // (Gumroad renders the purchased product card on the receipt). var productName = textOf('.product-name, h1[itemprop="name"], .receipt h1') || 'Gumroad product'; var permalink = (textOf('[data-product-permalink]') || (document.querySelector('a[href*="/l/"]') || {}).href || '').match(/\/l\/([\w-]+)/); var itemId = permalink && permalink[1] ? permalink[1] : orderId; w.tcFirePurchase({ orderId: orderId, value: value, currency: currency, items: [{ item_id: String(itemId), item_name: productName, item_category: 'digital', price: value, quantity: 1 }] }); })(window); </script> <!-- End Traksolv Gumroad DataLayer · Receipt --> <!-- 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
Save and test with a $0 product
SaveSave each snippet → buy a $0 test product to verify the purchase event fires
?tcDebug=1 in the URL on the receipt page will log the dataLayer push to console.
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 Gumroad, answered
Does server-side tracking work with Gumroad?
Yes. TrakSolv works with Gumroad via a lightweight loader snippet plus a Gumroad-specific dataLayer that fires every GA4 enhanced-ecommerce event. You paste it once in Gumroad's custom-code area and conversions start flowing through your own server-side GTM container.
How do I install TrakSolv on Gumroad?
Add the TrakSolv loader (head + body) and the Gumroad dataLayer snippet to your store's custom-code settings, then save. The full step-by-step is on this page. Most Gumroad merchants are live in a few minutes — no developer required.
What can I track server-side on Gumroad?
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 Gumroad 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 Gumroad conversions — start free.
Spin up a first-party server-side GTM container for Gumroad and watch your match quality climb. No credit card, live in minutes.
