How to set up cross-domain tracking in PostHog
Contents
Using multiple website domains or subdomains is a common way to split up a product. For example, a company might have a marketing website, a web app, and documentation, each with their own subdomain.
We recommend using the same PostHog project across your product to ensure accuracy and consistency, but this can require tracking across domains and even across websites. To help with this, PostHog has both automatic cross-domain tracking and manual options for setting up cross-website tracking, and this tutorial shows how to set them up.
Cross subdomain tracking with automatic first-party cookies
If you are using the JavaScript snippet or posthog-js, PostHog automatically sets a first-party cookie that works between subdomains. For example, the same cookie would work for posthog.com, us.posthog.com, and merch.posthog.com. If you use initialize posthog-js or the snippet with either localStorage+cookie (the default) or just cookie, you don’t need to do more work to make this happen.
First-party cookies ensure you get the most data possible, as third-party cookies often get blocked or removed. Similarly, you can set up a reverse proxy to send events from your domain so they aren’t intercepted by tracking blockers.
Cross website tracking by passing IDs across domains
Tracking users across different domains, like posthog.com and hogflix.com, requires some extra work. You need to pass users' distinct_id and session_id between PostHog initializations to ensure they are connected. This not only ensures tracking is accurate and consistent, but session replays and feature flag evaluations work across domains too.
If you are using anonymous events, call posthog.identify() or posthog.createPersonProfile() before leaving the first site, to make sure that initial person properties like $initial_referrer are set correctly.
Getting IDs on the first website
To do this, first, we need to get the distinct_id and session_id from the first website. To do this, we can call posthog.get_distinct_id() and posthog.get_session_id() and pass them in the URL hash to the second domain.
A barebones example in React looks like this:
Bootstrapping the IDs on the second website
On the second website, we can check for the session_id and distinct_id in the URL hash and then bootstrap those values in our PostHog initialization.
In our barebones React example, our second website's main.jsx file would look like this:
This ensures that the distinct_id and session_id are the same on both domains.
Identifying and merging users on the second website
We recommend bootstrapping the IDs if you need to track users across websites or domains. If you can't bootstrap, for instance if you can't access the window object, you can use a combination of identify and alias to merge the users instead. This requires a stable distinct ID like a username, app user ID, or email and won't connect the session replays and feature flag evaluations between the two sites.
The setup on the first website is the same: a link with the distinct_id in the URL hash (we don't use the session_id).
On the second website, we need to use the stable distinct ID to:
identifythe anonymous distinct ID from the second websitealiasthe distinct ID from the first website
In our barebones React example, this looks like this:
This connects the events from both sites to the same user. See our docs on identifying users and using alias for more details.
Tracking journeys through a site you don't control
The methods above assume you run posthog-js on both sites, but many journeys send the user to a partner site you cannot instrument such as a ticketing service, a checkout, or a payment provider. The user leaves your site, acts on the partner site, and comes back. You cannot see the partner leg, but you can keep the same person and session across the trip, and you can measure the leg as a gap between two of your own events.
Put the IDs on the return URL
Most partners let you set a redirect or callback URL, so add distinct_id and session_id to that URL, and read them again when the user comes back.
Note: Be wary about
session_idanddistinct_idin this case. Although neither is a PostHog credential and any script on your own page can already read them, adistinct_idcan hold personal data if you set it to an email or account ID. The values can also be changed in transit, so don't treat the IDs you read back as trusted.
On your return page, read the IDs from the hash and bootstrap them, the same way you do on a second site you own. Turn off automatic pageview capture at the same time, so you can set the correct referrer on the return pageview yourself (see below):
Measure the partner leg with an event pair
You cannot capture events on the partner site. To measure the leg, capture one event when the user leaves and one event when the user comes back. In the example above, the events are checkout_started and a checkout_returned event on the return page.
checkout_started fires right before the browser leaves your site, so the example above captures it with transport: 'sendBeacon' and send_instantly: true. Without these options, event batching can hold the event until after the navigation and drop it, which loses the first funnel step and overstates your conversion rate. Even with them, the browser controls final delivery, so treat it as best effort.
You can then build a funnel with checkout_started as the first step and checkout_returned as the second to see the drop off and time between the two steps.
Fix the referrer and session reset
The return redirect arrives with the partner as the $referrer. This breaks attribution, because PostHog reads the partner as the source of the visit. The redirect can also start a new session, which splits one journey into two.
The bootstrap above fixes the session reset, because it keeps the same session_id across the round trip.
The referrer needs one more step. When posthog.init() runs on the return page, it captures a $pageview automatically, and that pageview reads the partner as the $referrer. Setting the referrer on checkout_returned alone does not fix this, because the automatic pageview carries the partner value too. This is why the return-page init above sets capture_pageview: false. Capture the pageview and the return event yourself, with the correct referrer on both:
Using third-party cookies (or their equivalent)
Another way is using third-party cookies (or an equivalent method) to get the data from one site to another. This isn’t recommended.
For example, you can add an iframe from one website and pull tracking data into another. To apply cookies when viewing an iframe on another website, your website needs to set cookies with the following header:
This effectively makes it a third-party cookie, which many browsers, sites, and extensions block and remove. You can try to use it, but we recommend you use other methods.
Further reading
- How to use PostHog without cookie banners
- Building a tracking cookies opt out banner in React
- The complete guide to event tracking
Subscribe to our newsletter
build mode
Read by 75,000+ founders and builders
We'll share your email with Substack
PostHog is the leading platform for building self-driving products. With a full suite of developer tools – AI observability, product analytics, session replay, feature flags, experiments, error tracking, logs, and more – PostHog captures all the context agents need to diagnose problems, uncover opportunities, and ship fixes. A data warehouse and CDP tie it all together, unifying that context into one source agents can read across. You can steer it all from Slack, the web app, the desktop (PostHog Desktop), or your own editor via the MCP.