TikTok Ads Cross-Domain Tracking | OpsBlu Docs

TikTok Ads Cross-Domain Tracking

How to track users across multiple domains and subdomains with TikTok Ads. Covers cross-domain configuration, cookie handling, session stitching, and.

Cross-Domain Tracking Overview

Cross-domain tracking enables you to track users as they move between different domains (e.g., from marketing site to checkout) while maintaining a single user session.

When You Need Cross-Domain Tracking

Scenario Example
Separate checkout domain shop.example.com → checkout.example.com
Third-party payment example.com → payments.stripe.com
Subdomain separation www.example.com → blog.example.com
Multiple brand domains brandA.com → brandB.com (same business)
Landing page domains campaign.example.com → www.example.com

How Cross-Domain Tracking Works

The Challenge

Without cross-domain tracking:

  1. User clicks ad on TikTok → lands on shop.example.com
  2. TikTok cookie stored on shop.example.com domain
  3. User navigates to checkout.example.com
  4. New domain = new session (cookie not accessible)
  5. Conversion doesn't attribute back to TikTok ad

The Solution

Cross-domain tracking passes the TikTok click ID (ttclid) in the URL:

  1. User clicks ad → lands on shop.example.com?ttclid=ABC123
  2. User navigates to checkout.example.com?ttclid=ABC123
  3. TikTok recognizes same user via ttclid
  4. Conversion properly attributes

Implementation Methods

Method 1: Automatic Linker (Subdomains)

For subdomains under the same root domain (e.g., shop.example.com and checkout.example.com):

// Load pixel with cookie domain configuration
ttq.load('YOUR_PIXEL_ID', {
  cookie_domain: '.example.com'  // Note the leading dot
});
ttq.page();

This shares cookies across all *.example.com subdomains.

Implementation:

<!-- On all subdomains -->
<script>
!function (w, d, t) {
  // TikTok Pixel base code...

  ttq.load('YOUR_PIXEL_ID', {
    cookie_domain: '.example.com'  // Shared cookie domain
  });
  ttq.page();
}(window, document, 'ttq');
</script>

For completely different domains, manually append ttclid to cross-domain links:

// Function to get ttclid from URL or cookie
function getTTClid() {
  // Check URL first
  var urlParams = new URLSearchParams(window.location.search);
  var ttclid = urlParams.get('ttclid');

  if (ttclid) return ttclid;

  // Check cookie
  var cookies = document.cookie.split(';');
  for (var i = 0; i < cookies.length; i++) {
    var cookie = cookies[i].trim();
    if (cookie.indexOf('_ttp=') === 0) {
      return cookie.substring('_ttp='.length);
    }
  }

  return null;
}

// Function to append ttclid to URL
function appendTTClid(url) {
  var ttclid = getTTClid();
  if (!ttclid) return url;

  var separator = url.indexOf('?') > -1 ? '&' : '?';
  return url + separator + 'ttclid=' + encodeURIComponent(ttclid);
}

// Decorate all cross-domain links
function decorateLinks() {
  var crossDomainHosts = [
    'checkout.example.com',
    'payment.example.com'
  ];

  var links = document.querySelectorAll('a[href]');

  links.forEach(function(link) {
    var url = link.href;

    // Check if link is cross-domain
    crossDomainHosts.forEach(function(host) {
      if (url.indexOf(host) > -1) {
        link.href = appendTTClid(url);
      }
    });
  });
}

// Run on page load
window.addEventListener('load', decorateLinks);

// Run on dynamically added links
var observer = new MutationObserver(decorateLinks);
observer.observe(document.body, {
  childList: true,
  subtree: true
});

Method 3: Form Decoration

For forms that submit to different domains:

// Decorate form action URLs
function decorateForms() {
  var forms = document.querySelectorAll('form');

  forms.forEach(function(form) {
    var action = form.action;

    // Check if form submits to cross-domain
    if (action.indexOf('checkout.example.com') > -1) {
      form.action = appendTTClid(action);
    }
  });
}

// Or add hidden input
function addTTClidToForm(formId) {
  var form = document.getElementById(formId);
  var ttclid = getTTClid();

  if (ttclid && form) {
    var input = document.createElement('input');
    input.type = 'hidden';
    input.name = 'ttclid';
    input.value = ttclid;
    form.appendChild(input);
  }
}

// Usage
window.addEventListener('load', function() {
  addTTClidToForm('checkout-form');
});

Complete Implementation Examples

Example 1: Marketing Site → Checkout Domain

Domain Setup:

  • Marketing: www.example.com
  • Checkout: checkout.example.com
  • Payment: payments.example.com

On Marketing Site (www.example.com):

<script>
!function (w, d, t) {
  // TikTok Pixel base code...

  ttq.load('YOUR_PIXEL_ID');
  ttq.page();
}(window, document, 'ttq');

// Cross-domain link decoration
(function() {
  function getTTClid() {
    var urlParams = new URLSearchParams(window.location.search);
    var ttclid = urlParams.get('ttclid');
    if (ttclid) return ttclid;

    var match = document.cookie.match(/_ttp=([^;]+)/);
    return match ? match[1] : null;
  }

  function decorateCrossDomainLinks() {
    var ttclid = getTTClid();
    if (!ttclid) return;

    var links = document.querySelectorAll('a[href*="checkout.example.com"]');
    links.forEach(function(link) {
      var url = new URL(link.href);
      url.searchParams.set('ttclid', ttclid);
      link.href = url.toString();
    });
  }

  window.addEventListener('load', decorateCrossDomainLinks);

  // Handle dynamically added links
  setInterval(decorateCrossDomainLinks, 1000);
})();
</script>

On Checkout Domain (checkout.example.com):

<script>
!function (w, d, t) {
  // TikTok Pixel base code...

  ttq.load('YOUR_PIXEL_ID');
  ttq.page();
}(window, document, 'ttq');

// Preserve ttclid from URL
(function() {
  var urlParams = new URLSearchParams(window.location.search);
  var ttclid = urlParams.get('ttclid');

  if (ttclid) {
    // Store in cookie for this domain
    document.cookie = '_ttp=' + ttclid + '; domain=checkout.example.com; path=/; max-age=' + (60*60*24*7);
  }

  // Decorate links to payment domain
  function decoratePaymentLinks() {
    var currentTTClid = ttclid || getCookie('_ttp');
    if (!currentTTClid) return;

    var links = document.querySelectorAll('a[href*="payments.example.com"]');
    links.forEach(function(link) {
      var url = new URL(link.href);
      url.searchParams.set('ttclid', currentTTClid);
      link.href = url.toString();
    });
  }

  function getCookie(name) {
    var match = document.cookie.match(new RegExp(name + '=([^;]+)'));
    return match ? match[1] : null;
  }

  window.addEventListener('load', decoratePaymentLinks);
})();
</script>

Example 2: Subdomain Setup

For subdomains (blog.example.com, shop.example.com):

<!-- Same pixel code on all subdomains -->
<script>
!function (w, d, t) {
  // TikTok Pixel base code...

  ttq.load('YOUR_PIXEL_ID', {
    cookie_domain: '.example.com'  // Share cookies across subdomains
  });
  ttq.page();
}(window, document, 'ttq');
</script>

No link decoration needed - cookies automatically shared!

Example 3: Multiple Brand Domains

For completely different domains owned by same company:

Brand A (brandA.com):

<script>
!function (w, d, t) {
  ttq.load('YOUR_PIXEL_ID');
  ttq.page();
}(window, document, 'ttq');

// Decorate links to Brand B
window.addEventListener('load', function() {
  var ttclid = new URLSearchParams(window.location.search).get('ttclid') ||
               document.cookie.match(/_ttp=([^;]+)/)?.[1];

  if (ttclid) {
    document.querySelectorAll('a[href*="brandB.com"]').forEach(function(link) {
      link.href += (link.href.includes('?') ? '&' : '?') + 'ttclid=' + ttclid;
    });
  }
});
</script>

Brand B (brandB.com):

<script>
!function (w, d, t) {
  ttq.load('YOUR_PIXEL_ID');  // Same pixel ID
  ttq.page();
}(window, document, 'ttq');

// Capture ttclid from URL
(function() {
  var ttclid = new URLSearchParams(window.location.search).get('ttclid');
  if (ttclid) {
    document.cookie = '_ttp=' + ttclid + '; path=/; max-age=' + (60*60*24*7);
  }
})();
</script>

JavaScript Helper Library

Create a reusable cross-domain tracking library:

// tiktok-cross-domain.js
(function(window) {
  'use strict';

  var TikTokCrossDomain = {
    config: {
      crossDomains: [],
      paramName: 'ttclid',
      cookieName: '_ttp',
      cookieDuration: 7 * 24 * 60 * 60 * 1000  // 7 days
    },

    init: function(domains) {
      this.config.crossDomains = domains || [];
      this.captureTTClid();
      this.decorateLinks();
      this.observeDOM();
    },

    getTTClid: function() {
      // From URL
      var urlParams = new URLSearchParams(window.location.search);
      var ttclid = urlParams.get(this.config.paramName);
      if (ttclid) return ttclid;

      // From cookie
      var match = document.cookie.match(new RegExp(this.config.cookieName + '=([^;]+)'));
      return match ? match[1] : null;
    },

    captureTTClid: function() {
      var ttclid = new URLSearchParams(window.location.search).get(this.config.paramName);
      if (ttclid) {
        this.setCookie(this.config.cookieName, ttclid, this.config.cookieDuration);
      }
    },

    setCookie: function(name, value, maxAge) {
      document.cookie = name + '=' + value + '; path=/; max-age=' + (maxAge / 1000);
    },

    decorateUrl: function(url) {
      var ttclid = this.getTTClid();
      if (!ttclid) return url;

      try {
        var urlObj = new URL(url);
        urlObj.searchParams.set(this.config.paramName, ttclid);
        return urlObj.toString();
      } catch (e) {
        // Fallback for relative URLs
        var separator = url.indexOf('?') > -1 ? '&' : '?';
        return url + separator + this.config.paramName + '=' + encodeURIComponent(ttclid);
      }
    },

    isCrossDomain: function(url) {
      var self = this;
      return this.config.crossDomains.some(function(domain) {
        return url.indexOf(domain) > -1;
      });
    },

    decorateLinks: function() {
      var self = this;
      var links = document.querySelectorAll('a[href]');

      links.forEach(function(link) {
        if (self.isCrossDomain(link.href)) {
          link.href = self.decorateUrl(link.href);
        }
      });
    },

    decorateForms: function() {
      var self = this;
      var forms = document.querySelectorAll('form');

      forms.forEach(function(form) {
        if (self.isCrossDomain(form.action)) {
          var ttclid = self.getTTClid();
          if (ttclid) {
            var input = document.createElement('input');
            input.type = 'hidden';
            input.name = self.config.paramName;
            input.value = ttclid;
            form.appendChild(input);
          }
        }
      });
    },

    observeDOM: function() {
      var self = this;
      var observer = new MutationObserver(function() {
        self.decorateLinks();
        self.decorateForms();
      });

      observer.observe(document.body, {
        childList: true,
        subtree: true
      });
    }
  };

  // Expose globally
  window.TikTokCrossDomain = TikTokCrossDomain;

})(window);

Usage:

<script src="tiktok-cross-domain.js"></script>
<script>
// Initialize with cross-domain list
TikTokCrossDomain.init([
  'checkout.example.com',
  'payments.example.com',
  'app.example.com'
]);
</script>

Testing Cross-Domain Tracking

Manual Testing Steps

  1. Add ttclid to URL manually

    https://www.example.com/product?ttclid=TEST123456
    
  2. Navigate to cross-domain

    • Click link to checkout.example.com
    • Verify ttclid appears in URL
  3. Check cookie on destination

    // In browser console on checkout.example.com
    document.cookie
    // Should show: _ttp=TEST123456
    
  4. Fire conversion event

    ttq.track('CompletePayment', {
      value: 1.00,
      currency: 'USD',
      order_id: 'TEST_' + Date.now()
    });
    
  5. Verify in Events Manager

    • Check Test Events tool
    • Should show same ttclid

Automated Testing

// test-cross-domain.js
describe('TikTok Cross-Domain Tracking', function() {

  it('should preserve ttclid across domains', function() {
    // Simulate landing with ttclid
    window.history.pushState({}, '', '?ttclid=TEST123');

    // Initialize cross-domain tracking
    TikTokCrossDomain.init(['checkout.example.com']);

    // Check ttclid extracted
    var ttclid = TikTokCrossDomain.getTTClid();
    expect(ttclid).toBe('TEST123');

    // Check link decoration
    var link = document.createElement('a');
    link.href = 'https://checkout.example.com/cart';
    document.body.appendChild(link);

    TikTokCrossDomain.decorateLinks();

    expect(link.href).toContain('ttclid=TEST123');
  });

  it('should store ttclid in cookie', function() {
    window.history.pushState({}, '', '?ttclid=TEST456');
    TikTokCrossDomain.captureTTClid();

    var cookie = document.cookie;
    expect(cookie).toContain('_ttp=TEST456');
  });
});

Troubleshooting

Common Issues

Issue Cause Solution
ttclid not in URL Links not decorated Check decorateLinks function runs
Cookie not set on destination ttclid not captured Verify captureTTClid runs on page load
Conversion not attributing Different pixel IDs Use same pixel on all domains
ttclid value incorrect Encoding issues Use encodeURIComponent
SameSite cookie issues Browser restrictions Set SameSite=None; Secure

Debug Mode

// Add debug logging
var TikTokCrossDomainDebug = {
  log: function(message, data) {
    if (localStorage.getItem('ttDebug') === 'true') {
      console.log('[TikTok Cross-Domain]', message, data);
    }
  }
};

// Enable debugging
localStorage.setItem('ttDebug', 'true');

// Add to functions
TikTokCrossDomain.decorateUrl = function(url) {
  var decorated = // ... decoration logic
  TikTokCrossDomainDebug.log('URL decorated', {
    original: url,
    decorated: decorated
  });
  return decorated;
};

Best Practices

  1. Use same Pixel ID across all domains
  2. Test thoroughly before launch
  3. Document domains that need tracking
  4. Monitor attribution after implementation
  5. Use HTTPS on all domains
  6. Set proper cookie settings for cross-site scenarios
  7. Implement on page load not on click
  8. Handle dynamic content with MutationObserver
  9. Validate ttclid format before using
  10. Log implementations for debugging

Modern browsers require SameSite attribute for cross-domain cookies:

function setTTClidCookie(ttclid) {
  var cookieString = '_ttp=' + ttclid +
    '; path=/' +
    '; max-age=' + (60*60*24*7) +
    '; SameSite=None' +  // Required for cross-site
    '; Secure';           // Required with SameSite=None (HTTPS only)

  document.cookie = cookieString;
}

Next Steps

  1. Identify your domains that need cross-domain tracking
  2. Choose implementation method (subdomain vs different domains)
  3. Implement tracking code on all domains
  4. Test thoroughly using manual and automated tests
  5. Monitor Events Manager for proper attribution
  6. Document setup for team reference