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 oncheckout.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.comsharing 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:
localStorageor 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_id → user_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.com→checkout.example.com(shares device_id automatically)app.example.com→blog.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
- Pre-redirect: Store
device_idanduser_idin server session or database - Payment callback: Include identifiers in the return URL
- 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")
Deep Link Handoff
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
Consent Mode Integration
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
- Navigate to Data > Events in Amplitude
- Use Event Stream to view real-time events
- Filter by
device_idoruser_id - Verify events from both domains share the same identifier
Monitor User Journeys
- Go to Analytics > User Sessions
- Search for a test user by
user_id - Confirm the session timeline includes events from all domains
- 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
- Start session on Domain A
- Trigger several events (page_view, button_click)
- Navigate to Domain B with proper ID propagation
- Trigger events on Domain B
- Check Amplitude User Sessions: all events should appear in one session
Attribution Validation
Verify UTM parameters persist:
- Land on Domain A with UTM parameters:
?utm_source=google&utm_campaign=summer - Navigate to Domain B
- Complete conversion event
- 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
- Prioritize user_id: Once authenticated, always set
user_idfor most reliable cross-domain and cross-device tracking - Test incognito: Validate cross-domain flows in private browsing to simulate new users
- Monitor session metrics: Track average session duration and events per session to detect fragmentation
- Document flows: Map all cross-domain user journeys and test each regularly
- Implement fallbacks: Have server-side stitching as a backup for privacy-blocking scenarios
- Clean URL parameters: Remove sensitive data from URLs before logging to Amplitude
- Version your implementation: Track schema versions to debug cross-domain issues over time