Amplitude Cross-Domain Tracking | OpsBlu Docs

Amplitude Cross-Domain Tracking

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

Overview

Cross-domain tracking in Amplitude ensures user sessions remain consistent when visitors navigate between multiple domains or applications you own. Amplitude uses device_id and user_id to stitch user journeys together, preventing session fragmentation and maintaining accurate attribution across domain boundaries.

Unlike cookie-based linker parameters, Amplitude relies on persistent first-party storage and explicit identifier propagation to maintain user identity across domains.

When Cross-Domain Tracking Is Required

  • Separate checkout domains: Main site on www.example.com, checkout on checkout.example.com
  • Third-party payment processors: Redirects to payment gateways (Stripe, PayPal, Square) that return users to confirmation pages
  • Multi-brand portfolios: brand-a.com, brand-b.com sharing common user base
  • Regional domains: example.com, example.co.uk, example.de
  • Marketing microsites: Campaign-specific domains that funnel to main conversion points
  • Partner integrations: Embedded experiences or SSO flows across partner domains
  • Web-to-mobile handoffs: Marketing site to app download or web view experiences

Identity Architecture

Device ID Management

Amplitude generates a unique device_id on first visit and stores it in:

  • Browser: localStorage or cookies under the source domain
  • Mobile: Device storage via SDK persistent storage

The device_id must be explicitly propagated across domain boundaries.

User ID After Authentication

When users authenticate, set the user_id to link activity across devices and sessions:

amplitude.setUserId('user_12345');

This creates a mapping: device_iduser_id, enabling cross-device and cross-domain journey attribution.

Configuration Methods

Subdomain Tracking (Same Root Domain)

For subdomains under the same root domain, share storage automatically:

// Initialize with cookieOptions
amplitude.init('YOUR_API_KEY', null, {
  cookieOptions: {
    domain: '.example.com', // Leading dot shares across subdomains
    sameSite: 'Lax',
    secure: true
  }
});

This allows:

  • www.example.comcheckout.example.com (shares device_id automatically)
  • app.example.comblog.example.com

Cross-Domain Tracking (Different Root Domains)

For completely different domains, manually propagate identifiers:

Method 1: Query Parameter Propagation

Capture the device_id before redirect and append to destination URL:

// On source domain (example.com)
const deviceId = amplitude.getDeviceId();
const destinationUrl = `https://checkout-partner.com/cart?amp_device_id=${deviceId}`;
window.location.href = destinationUrl;

On destination domain, restore the device_id:

// On destination domain (checkout-partner.com)
const urlParams = new URLSearchParams(window.location.search);
const deviceId = urlParams.get('amp_device_id');

if (deviceId) {
  amplitude.init('YOUR_API_KEY', null, {
    deviceId: deviceId // Use the propagated device_id
  });
} else {
  amplitude.init('YOUR_API_KEY');
}

Method 2: URL Fragment (Hash) Propagation

For privacy-sensitive scenarios where query parameters shouldn't be logged:

// Source domain
const deviceId = amplitude.getDeviceId();
window.location.href = `https://partner.com/page#amp_device_id=${deviceId}`;
// Destination domain
const hash = window.location.hash;
const deviceId = new URLSearchParams(hash.slice(1)).get('amp_device_id');

amplitude.init('YOUR_API_KEY', null, {
  deviceId: deviceId || undefined
});

Method 3: Server-Side Session Storage

For authenticated flows, store and retrieve device_id server-side:

// Before redirect (client-side)
const deviceId = amplitude.getDeviceId();
fetch('/api/store-device-id', {
  method: 'POST',
  body: JSON.stringify({ deviceId, sessionId: getCurrentSessionId() })
});

// Redirect to partner domain
window.location.href = 'https://partner.com/checkout?session_id=xyz';
// After redirect (destination domain)
const sessionId = new URLSearchParams(window.location.search).get('session_id');

fetch('https://source.com/api/get-device-id?session_id=' + sessionId)
  .then(res => res.json())
  .then(data => {
    amplitude.init('YOUR_API_KEY', null, {
      deviceId: data.deviceId
    });
  });

User ID Propagation After Login

When users authenticate on one domain and redirect to another:

// On login domain
amplitude.setUserId('user_12345');
const userId = amplitude.getUserId();
const deviceId = amplitude.getDeviceId();

// Redirect with both IDs
window.location.href = `https://app.example.com/dashboard?user_id=${userId}&device_id=${deviceId}`;
// On app domain
const urlParams = new URLSearchParams(window.location.search);
const userId = urlParams.get('user_id');
const deviceId = urlParams.get('device_id');

amplitude.init('YOUR_API_KEY', userId, {
  deviceId: deviceId
});

Session Management Across Domains

Session ID Continuity

Amplitude automatically manages session_id based on user activity. For cross-domain continuity:

// Send start_session event explicitly after cross-domain navigation
amplitude.track('start_session');

Alternatively, configure session timeout:

amplitude.init('YOUR_API_KEY', null, {
  sessionTimeout: 30 * 60 * 1000 // 30 minutes in milliseconds
});

Attribution Preservation

Ensure UTM parameters and referrer data persist:

// Capture initial UTM parameters
const utmParams = {
  utm_source: new URLSearchParams(window.location.search).get('utm_source'),
  utm_medium: new URLSearchParams(window.location.search).get('utm_medium'),
  utm_campaign: new URLSearchParams(window.location.search).get('utm_campaign'),
  utm_content: new URLSearchParams(window.location.search).get('utm_content'),
  utm_term: new URLSearchParams(window.location.search).get('utm_term')
};

// Attach to all events
amplitude.setUserProperties(utmParams);

// Include in cross-domain redirects
const redirectUrl = new URL('https://checkout.example.com');
Object.entries(utmParams).forEach(([key, value]) => {
  if (value) redirectUrl.searchParams.set(key, value);
});
window.location.href = redirectUrl.toString();

Third-Party Payment Processor Integration

Supported Flow

example.com → stripe.com → example.com/confirmation
  1. Pre-redirect: Store device_id and user_id in server session or database
  2. Payment callback: Include identifiers in the return URL
  3. Confirmation page: Restore Amplitude instance with stored identifiers

Stripe Example

// Before Stripe redirect
const checkoutSessionId = generateUniqueId();
const deviceId = amplitude.getDeviceId();
const userId = amplitude.getUserId();

// Store mapping server-side
await fetch('/api/store-amplitude-session', {
  method: 'POST',
  body: JSON.stringify({
    checkoutSessionId,
    deviceId,
    userId
  })
});

// Create Stripe session with custom return URL
const session = await stripe.checkout.sessions.create({
  success_url: `https://example.com/success?session_id=${checkoutSessionId}`,
  cancel_url: 'https://example.com/cart'
});

On return, restore identifiers:

// Confirmation page
const sessionId = new URLSearchParams(window.location.search).get('session_id');

fetch(`/api/get-amplitude-session?session_id=${sessionId}`)
  .then(res => res.json())
  .then(data => {
    amplitude.init('YOUR_API_KEY', data.userId, {
      deviceId: data.deviceId
    });

    amplitude.track('purchase_completed', {
      checkout_session: sessionId
    });
  });

Multi-App Journeys (Web + Mobile)

Identity Alignment

Ensure consistent user_id across web and mobile:

// Web implementation
amplitude.init('YOUR_WEB_API_KEY', 'user_12345');

// Mobile (iOS - Swift)
Amplitude.instance().setUserId("user_12345")
Amplitude.instance().logEvent("app_opened")

// Mobile (Android - Kotlin)
Amplitude.getInstance().userId = "user_12345"
Amplitude.getInstance().logEvent("app_opened")

When users transition from web to app:

// Web to App deep link
const userId = amplitude.getUserId();
const deviceId = amplitude.getDeviceId();

const deepLink = `myapp://product/12345?user_id=${userId}&web_device_id=${deviceId}`;
window.location.href = deepLink;

In the mobile app, capture and associate:

// iOS - Handle deep link
func handleDeepLink(url: URL) {
    let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
    let userId = components?.queryItems?.first(where: { $0.name == "user_id" })?.value
    let webDeviceId = components?.queryItems?.first(where: { $0.name == "web_device_id" })?.value

    if let userId = userId {
        Amplitude.instance().setUserId(userId)

        if let webDeviceId = webDeviceId {
            Amplitude.instance().logEvent("web_to_app_transition", withEventProperties: [
                "web_device_id": webDeviceId
            ])
        }
    }
}

Edge Cases and Considerations

iFrame Embeds

When content is embedded via iframes across domains:

// Parent page (example.com)
amplitude.init('YOUR_API_KEY');

// Send device_id to iframe via postMessage
const iframe = document.getElementById('embedded-widget');
iframe.contentWindow.postMessage({
  type: 'amplitude_device_id',
  deviceId: amplitude.getDeviceId()
}, 'https://widget.partner.com');
// iFrame (widget.partner.com)
window.addEventListener('message', (event) => {
  if (event.origin === 'https://example.com' && event.data.type === 'amplitude_device_id') {
    amplitude.init('YOUR_API_KEY', null, {
      deviceId: event.data.deviceId
    });
  }
});

Single Page Applications (SPAs)

For client-side routing with cross-domain navigation:

// Ensure Amplitude is initialized before route changes
router.beforeEach((to, from, next) => {
  // Track route change
  amplitude.track('page_viewed', {
    page_path: to.path,
    page_title: to.meta.title
  });

  next();
});

Browser Privacy Features

Safari ITP and privacy extensions may affect storage:

  • Monitor: Track cross-domain conversion rates for unusual drops
  • Fallback: Implement server-side session stitching using user_id
  • First-party: Use subdomains under same root domain where possible

Respect user consent preferences:

// With consent
if (hasAnalyticsConsent()) {
  amplitude.init('YOUR_API_KEY', null, {
    cookieOptions: {
      domain: '.example.com'
    }
  });
} else {
  // Cookieless mode or delayed initialization
  amplitude.init('YOUR_API_KEY', null, {
    disableCookies: true
  });
}

Regional Data Residency

For EU-resident projects, ensure EU endpoints are configured:

amplitude.init('YOUR_API_KEY', null, {
  serverUrl: 'https://api.eu.amplitude.com/2/httpapi',
  serverUrlV2: 'https://api.eu.amplitude.com/batch'
});

Validation and Testing

Amplitude Event Stream

  1. Navigate to Data > Events in Amplitude
  2. Use Event Stream to view real-time events
  3. Filter by device_id or user_id
  4. Verify events from both domains share the same identifier

Monitor User Journeys

  1. Go to Analytics > User Sessions
  2. Search for a test user by user_id
  3. Confirm the session timeline includes events from all domains
  4. Check that session boundaries are correct (no unexpected splits)

Browser DevTools Testing

// On source domain
console.log('Source Device ID:', amplitude.getDeviceId());
console.log('Source User ID:', amplitude.getUserId());

// After cross-domain navigation
console.log('Destination Device ID:', amplitude.getDeviceId());
console.log('Destination User ID:', amplitude.getUserId());

// Should match source values if propagation worked

Session Continuity Test

  1. Start session on Domain A
  2. Trigger several events (page_view, button_click)
  3. Navigate to Domain B with proper ID propagation
  4. Trigger events on Domain B
  5. Check Amplitude User Sessions: all events should appear in one session

Attribution Validation

Verify UTM parameters persist:

  1. Land on Domain A with UTM parameters: ?utm_source=google&utm_campaign=summer
  2. Navigate to Domain B
  3. Complete conversion event
  4. In Amplitude, verify the conversion attributes to the original UTM source

Troubleshooting

Symptom Likely Cause Solution
Different device_id on second domain ID not propagated Append device_id to redirect URL or use server-side storage
Sessions split at domain boundary Missing start_session or timeout Explicitly track start_session after cross-domain navigation
user_id lost after redirect Not included in redirect flow Propagate user_id along with device_id
Attribution lost (UTM missing) UTM parameters not carried over Include UTM params in cross-domain redirect URLs
Payment return loses session Return URL missing identifiers Store identifiers server-side and restore on callback
EU users failing to track Wrong endpoint region Configure EU server URLs in init options
Safari users losing identity ITP blocking cookies Use first-party subdomain or server-side stitching
iFrame events not tracked Cross-origin restrictions Use postMessage to share device_id with iframe

Decision Matrix: When to Use Each Method

Scenario Recommended Approach Reason
Same root domain (subdomains) Cookie domain sharing Automatic, no manual propagation needed
Different root domains (owned) Query parameter propagation Simple, reliable, works without server
Privacy-sensitive flows URL fragment (hash) Hash params not sent to server logs
Authenticated users only Server-side session storage Most secure, prevents client tampering
Third-party payment gateways Server-side with callback Gateway controls return URL structure
Web-to-mobile handoff Deep link with user_id Aligns identity across platforms
iFrame embeds postMessage API Overcomes cross-origin restrictions

Best Practices

  1. Prioritize user_id: Once authenticated, always set user_id for most reliable cross-domain and cross-device tracking
  2. Test incognito: Validate cross-domain flows in private browsing to simulate new users
  3. Monitor session metrics: Track average session duration and events per session to detect fragmentation
  4. Document flows: Map all cross-domain user journeys and test each regularly
  5. Implement fallbacks: Have server-side stitching as a backup for privacy-blocking scenarios
  6. Clean URL parameters: Remove sensitive data from URLs before logging to Amplitude
  7. Version your implementation: Track schema versions to debug cross-domain issues over time