Published Aug 21, 2026
How to Preserve UTM Parameters, GCLID, and User Identifiers Across Domains and External Systems
A practical architecture guide for preserving campaign attribution and measurement continuity across subdomains, separate checkout domains, booking platforms, iframes, redirects, and external APIs.
Category: Analytics & Conversion Tracking · By Mikalai Sasau
This guide explains how to preserve campaign attribution and measurement identity when a visitor moves from a landing page to a subdomain, a separate checkout domain, a hosted booking service, an embedded iframe, a redirect page, or an external CRM. It compares Google linkers, cookie scope, URL parameters, postMessage, server-side correlation, and vendor metadata, then recommends an architecture for each common user journey.
Practical default: do not make one mechanism carry every identifier. Use GA4 cross-domain measurement for Analytics client and session continuity, Google conversion linking for ad-click measurement, and a short-lived opaque journey_id stored on your server for booking, order, lead, and CRM reconciliation. Pass raw UTM parameters or click IDs to an external system only when its documented interface requires them.
Executive summary
“Preserve tracking across domains” sounds like one task, but it usually combines at least four different jobs:
- preserve the original acquisition context, such as
utm_source,utm_medium, andutm_campaign; - preserve advertising click identifiers, such as
gclid,gbraid, andwbraid; - preserve analytics identity, such as the GA4 client and session identifiers transported through
_gl; - preserve a business identity, such as
journey_id,lead_id,cart_id,booking_id, or a signed-inuser_id.
These values have different owners, lifetimes, privacy implications, and failure modes. GA4 cross-domain measurement can keep one Analytics user and session across two tagged domains, but it does not create an authenticated user, define your first-touch attribution model, or guarantee that a booking platform will return an order identifier. Conversion Linker helps Google advertising tags retain click information, but it is not a CRM integration. A parent-domain cookie can work across trusted subdomains, but it cannot be read by an unrelated booking or payment domain. URL decoration can transport almost any value, but URLs are visible, shareable, logged, and frequently rewritten. postMessage can bridge a cross-origin iframe, but only when both sides cooperate.
The most robust general architecture is therefore layered:
- Capture allowlisted attribution values on the first owned page before redirects or client-side routing can remove them.
- Create a random, non-identifying
journey_idand store the attribution record on the server. - Let GA4 cross-domain measurement and Google conversion linking handle their own Google-specific identifiers where both domains support the tags.
- Pass the
journey_idto the external system through its supported query parameter, hidden metadata field, create-session API, orpostMessagecontract. - Require the external system to return that token in a webhook, API response, export, or redirect.
- Join the completed booking, order, or lead to the stored attribution record on the server.
When the destination is a true black box—no tag access, no accepted parameter, no metadata field, no API, no webhook, no cooperative iframe message, and no shared login—deterministic cross-domain stitching is not available. The honest fallback is to measure the outbound handoff on the owned site, use the provider’s aggregate completion data, and avoid presenting modeled or time-based reconciliation as exact user-level attribution.

First separate the identifiers you are trying to preserve
A large share of broken cross-domain implementations starts with a category error: a team treats UTMs, Google click IDs, GA cookies, and internal customer IDs as interchangeable “tracking parameters.” They are not.
| Identifier layer | Typical examples | What it represents | Preferred continuity method | What it should not be used for |
|---|---|---|---|---|
| Campaign labels | utm_source, utm_medium, utm_campaign, utm_content, utm_term |
Human-readable acquisition metadata defined by the marketer | Capture once into a first-party attribution record; pass to a vendor only through supported tracking fields | They are not a unique user ID and should not be copied through every internal page |
| Advertising click IDs | gclid, gbraid, wbraid, dclid, msclkid, fbclid |
An opaque identifier created by an advertising platform for a click or related ad interaction | Capture exactly, store with the lead or journey, and let the relevant platform tag manage its first-party storage | Do not decode, normalize case, truncate, or use it as a permanent customer identity |
| Google linker payload | _gl |
A short-lived, encoded transport parameter generated by Google tags to move supported measurement state between domains | Configure the official GA4 or Google tag linker on both domains and preserve the parameter through immediate redirects | Do not manually parse it, store it as a durable ID, or build your business attribution model around it |
| Analytics identity | GA4 client ID, session ID, _ga, _ga_* |
Browser and session state used by the analytics platform | GA4 cross-domain measurement, consistent tag configuration, and consent-aware first-party storage | It is not an account login, CRM contact key, or proof that two people are the same individual |
| Business journey identity | journey_id, lead_id, cart_id, order_id, booking_id |
Your own key for joining marketing context to a commercial process | Generate on the server, store in a first-party database, and pass as opaque vendor metadata | Do not expose sequential database IDs or sensitive information in public URLs |
| Signed-in user identity | user_id, account UUID, CRM contact mapping |
A stable pseudonymous identity assigned by the business after authentication or reliable account matching | Shared identity provider, SSO, backend session, or controlled API mapping; set the same non-PII value in each measurement environment | Do not use an email address, phone number, or another value that reveals the person in URLs or GA4 User-ID |
| Event deduplication identity | event_id, transaction ID, webhook event ID |
A key that prevents the same business event from being counted more than once | Generate once per event and retain it across browser, server, and webhook paths | It should not be reused as a user identifier or campaign token |
For a deeper treatment of Google advertising identifiers and CRM matching, see Google Ads identifiers: GCLID, GBRAID, WBRAID, CRM, GA4, and Measurement Protocol. Meta’s equivalent layers are covered in Meta identifiers: fbclid, _fbc, _fbp, and event_id.
The domain boundary decides what is technically possible
Before choosing a tool, classify the route. The phrases “another page,” “another subdomain,” and “another domain” describe materially different browser security boundaries.
Same origin
Two pages have the same origin when their scheme, hostname, and port match. A normal navigation from one path to another on the same origin does not require cross-domain linking. The same first-party cookies and origin-scoped browser storage are available, subject to their own path, security, expiry, and consent settings.
Different subdomains under one registrable domain
www.example.com and checkout.example.com are different origins, but they can receive a cookie scoped to example.com. A host-only cookie set by www.example.com, however, is not available to checkout.example.com. Browser localStorage remains origin-specific and is not automatically shared between the two subdomains.
This makes a parent-domain cookie useful for a low-risk journey token, but it also widens the trust boundary: every included subdomain receives that cookie. Sensitive authentication cookies should normally remain host-bound unless a reviewed SSO design requires otherwise. A separate pseudonymous attribution cookie is safer than broadening the scope of the main login cookie.
Separate domains controlled by the same organization
example.com and example-checkout.com cannot directly read each other’s first-party cookies or localStorage. Continuity requires an explicit bridge: Google’s linker, an allowlisted URL parameter, a backend redirect, a shared identity service, a cooperative popup or iframe message, or a server-to-server API.
An external booking, payment, or lead platform
An external provider controls its own origin, scripts, storage, redirects, and data model. Your code cannot force that provider to accept a token, run your analytics tag, preserve a query parameter, or return metadata. The provider must expose an integration surface that supports the required link:
- a documented tracking parameter;
- a custom field or metadata object;
- a create-session API;
- a webhook containing the same token;
- a return URL with a provider session ID;
- or a documented
postMessageprotocol for an iframe or popup.
If none of these exists, adding more JavaScript to the source page does not remove the browser’s origin boundary.
A different browser or device
No browser cookie, URL linker, or client ID can guarantee continuity when an anonymous user switches from a laptop to a phone or from one browser to another. Deterministic matching then requires a shared signed-in identity, a one-time link, an email or account workflow handled under an appropriate privacy basis, or a platform’s modeled attribution. Cross-domain and cross-device are separate problems.
Comparison of the main continuity methods
| Method | What it can transfer | Prerequisites | Best fit | Main limitation |
|---|---|---|---|---|
| GA4 cross-domain linker | Supported Google Analytics client and session state through _gl |
Compatible Google tag configuration on both domains, normally the same GA4 web data stream, and a navigable link or form | Owned marketing site → owned checkout or application domain | Does not create a business ID, authenticate a user, or make an untagged vendor visible |
| Google Conversion Linker | Google Ads and Floodlight click information used by conversion tags | Google tag or Conversion Linker on relevant pages; compatible cross-domain settings when domains differ | Landing domain → conversion domain for Google advertising measurement | Not a replacement for GA4 identity, UTM storage, or CRM reconciliation |
| Parent-domain cookie | An arbitrary first-party token or server session key | Hosts share a registrable parent domain and are trusted to receive the cookie | www → checkout or app subdomain |
Cannot cross to an unrelated domain; broad Domain scope increases exposure |
| URL decoration | Any allowlisted non-sensitive value accepted by the destination | Control over the outgoing URL and a destination that preserves and consumes the parameter | Redirects, hosted booking links, one-time handoffs, hash-based applications | Visible in history and logs; easy to strip, duplicate, leak, or share |
postMessage |
A structured message between an owned page and a cooperative iframe or popup | A retained window reference and code on both sides that validates origins | Embedded cross-origin booking or payment widget | Cannot work with an uncooperative iframe and does not share cookies by itself |
| Server-side correlation | Any attribution and identity fields stored behind an opaque key | Control over the source server and a way for the destination to return the key | High-value purchases, bookings, leads, long journeys, webhook-driven completion | Requires backend storage, retention rules, idempotency, and integration work |
| External API or vendor metadata | Provider-supported fields such as an internal reference, order ID, UTM set, or metadata object | Vendor documentation and an API, webhook, export, or return payload | Hosted checkout, scheduling, CRM, application, and payment systems | Field support varies; undocumented query parameters may be ignored |
| SSO or shared identity service | A stable pseudonymous account identity | Authentication on both properties and a controlled identity provider | Logged-in journeys across separate owned domains and devices | Only applies after reliable authentication and should not be confused with anonymous acquisition tracking |
| Unwanted referral configuration | No identifier; it tells GA4 not to treat a selected referrer as a new traffic source | GA4 configuration on the owned return destination | Return from a payment processor or password-reset provider | Prevents referral pollution but does not stitch the external steps or restore a lost client ID |
GA4 cross-domain measurement: what it solves and what it does not
GA4 cross-domain measurement is the standard solution when the same organization measures a journey across two or more domains with a compatible Google tag setup. Google Analytics normally writes first-party cookies independently on each domain. Without linking, a visitor can receive a new client and session identity on the second domain. With linking, the source tag adds a short-lived _gl parameter to an eligible outbound link or form, and the destination tag extracts supported measurement values into its own first-party cookies.
The recommended configuration is through the GA4 web data stream:
- Use the same intended GA4 web data stream and compatible Google tag configuration on all domains that belong to the journey.
- Open the web stream’s tag settings and configure the domains that should participate.
- Ensure the destination accepts incoming linker parameters.
- Test a real user navigation rather than loading the destination URL directly in a separate tab.
The linker is designed for an immediate handoff, not durable storage. Google documents linker parameters as short-lived, with a lifetime of approximately two minutes. If a redirector removes _gl, if a link is copied and opened later, or if a JavaScript navigation bypasses the listener that decorates the link, continuity can fail.
Common GA4 linker failures
- Different measurement destinations: the two domains send to different GA4 data streams or use incompatible tag configurations.
- Redirect stripping: an application, CDN, URL shortener, language selector, or login redirect reconstructs the URL without preserving
_gl. - Programmatic navigation: the application calls
location.assign(), router navigation, or another JavaScript transition before Google’s document-level click listener decorates the link. - Stopped event propagation: a component prevents the click event from reaching the document listener.
- Destination timing: the destination fires measurement before it accepts and applies the incoming linker value.
- Consent mismatch: storage and tag behavior differ between the source and destination because consent defaults or updates are not aligned.
- Shared URLs: a user copies a decorated URL and another person opens it while the parameter is still valid, creating an identity-contamination risk.
Do not hand-build or decode _gl. Let the Google tag generate and validate it. If a custom router prevents normal decoration, correct the navigation implementation or use the supported linker configuration rather than copying cookie values into your own query string.
Query versus fragment placement
The Google tag linker supports a normal query-string position and a fragment position. Query is the default. Fragment mode exists for applications whose routing or URL model requires values after #. Changing this setting is not cosmetic: all participating tags must agree on where to read the linker, and redirects or form submissions may treat fragments differently.
Use fragment mode only when the destination application deliberately expects it and end-to-end tests prove that the value is consumed before measurement fires. For most server-rendered and hosted-checkout routes, query placement is easier to preserve and diagnose.
What GA4 cross-domain measurement does not do
- It does not automatically carry your original UTM set as a business record.
- It does not make an external provider part of your GA4 journey unless that provider can run or integrate the required tag.
- It does not generate or synchronize your GA4
user_id. - It does not provide a booking, order, or CRM reference.
- It does not solve cross-device identity for anonymous users.
- It does not replace a webhook as the source of truth for a completed payment or booking.
GA4 User-ID is a separate feature. The business must assign the same unique, non-personally-identifying value in each environment after reliable authentication, and it must clear or set the value appropriately when the user logs out. The linker preserves browser measurement state; it does not authenticate the person.
Conversion Linker: preserve Google ad-click measurement, not the entire journey
Conversion Linker supports Google Ads and Floodlight measurement. It detects supported ad-click information on landing-page URLs and stores that information in first-party cookies and browser local storage. Typical cookies use the _gcl_* prefix, including values such as _gcl_aw; current implementations can also use the _gcl_ls local-storage key.
It is important not to add tags by habit. Current Google Tag Manager documentation states that when a container loads a Google tag on every page, a separate Conversion Linker tag is not also required. Containers with Google Ads or Floodlight tags load a Google tag before sending events. The implementation should therefore be audited as a system: confirm that a Google tag is present and correctly configured, rather than assuming that adding another Conversion Linker tag will repair an unrelated attribution problem.
For a genuine multi-domain conversion route, Conversion Linker can:
- enable linking across selected domains;
- decorate links to auto-linked domains;
- accept incoming linker parameters on the destination;
- decorate forms when query-string placement is used;
- read linker data from the query string or, where appropriate, the fragment.
Google specifically notes that form decoration works with query placement, because browsers may remove the fragment when submitting forms. This is a strong reason not to place critical handoff data after # on a form-based checkout.
Cookie scope and subdomains
Conversion Linker normally writes its cookies at the highest domain level it can use and with a root path. This commonly makes the click information available across subdomains of one parent domain. The advanced override exists for exceptional cases—for example, when two business units on separate subdomains must not share ad-click state. Changing the name prefix, domain, or path without a specific requirement can break the tags that expect the default cookie.
Consent still controls storage
Conversion linking is not a way around consent. Under Google Consent Mode, ad_storage and analytics_storage affect whether relevant cookies can be written or read, while ad_user_data controls consent for sending advertising-related user data to Google. Google also supports a consent-aware URL passthrough mode for specific same-domain situations, but that feature has its own prerequisites and should not be treated as a generic external-domain transport.
For consent timing and tag behavior after a user changes their choice, see Consent Mode v2 after page load: whether blocked tags fire after consent.
Cookies with the correct Domain and SameSite
A cookie is often the simplest continuity mechanism across trusted subdomains, but only when its scope is configured intentionally.
A server-generated journey cookie shared across www.example.com and checkout.example.com could look like this:
Set-Cookie: mf_journey=jrny_8f7d2c1a; Domain=example.com; Path=/; Max-Age=2592000; Secure; HttpOnly; SameSite=Lax
This example has several deliberate properties:
Domain=example.commakes the cookie available to the parent domain and its subdomains.Path=/allows it across all paths.Securelimits transmission to HTTPS.HttpOnlyprevents client-side JavaScript from reading it.SameSite=Laxis usually compatible with a normal top-level navigation while reducing some cross-site request exposure.Max-Ageshould match the justified attribution and retention window, not an arbitrary permanent lifetime.
With HttpOnly, the browser cannot use JavaScript to copy the value into an outgoing URL. That is often a benefit: an owned server endpoint can read the cookie, create the external session, and perform the redirect without exposing the underlying cookie to page scripts. If client-side access is genuinely required, use a separate low-value pseudonymous token rather than weakening an authentication cookie.
What Domain can and cannot do
A host may set a cookie for itself or a permitted parent domain, but not for an unrelated domain and not for a public suffix. www.example.com can set a cookie for example.com; it cannot set one for booking-provider.com. Adding a leading dot to .example.com does not create a special cross-domain capability in modern browsers.
If the Domain attribute is omitted, the cookie is host-only. Host-only cookies are more restrictive and are often preferable for security-sensitive state. Cookies with the __Host- prefix are intentionally host-bound: they require Secure, require Path=/, and cannot include a Domain attribute. Do not broaden those cookies merely to solve marketing attribution.
SameSite=None does not make a cookie cross-domain
SameSite controls whether a cookie is sent with same-site or cross-site requests. It does not allow one domain to read another domain’s cookie. SameSite=None requires Secure and may be needed for a cookie used in an embedded cross-site context, but browser privacy restrictions can still limit third-party cookie behavior. For a cross-origin iframe, a cooperative postMessage or server API is generally more durable than assuming third-party cookies will remain available.
URL decoration: powerful transport, weak storage
URL decoration means adding selected values to the destination URL. It works across unrelated domains because the browser carries the URL itself, not because the domains share storage. This makes it broadly compatible, but also easy to misuse.
Why links containing # frequently break
The fragment begins at # and is processed by the browser. It is not sent to the server in the HTTP request. A token placed inside a hash route may therefore be invisible to a server-side booking endpoint, redirect service, CDN, or API.
For an external single-page booking application, this is usually the safer structure:
https://booking.vendor.example/schedule?journey_id=jrny_8f7d2c1a#/calendar
The following structure places the token inside the fragment and will not send it to the server:
https://booking.vendor.example/schedule#/calendar?journey_id=jrny_8f7d2c1a
The second version can still work if the vendor’s client-side router explicitly parses the fragment and persists the token, but that is a provider-specific integration, not a general web behavior.
Do not concatenate query strings manually
Raw string concatenation often produces malformed URLs when the source already contains a query string or fragment. It also creates duplicate parameters whose interpretation depends on whether the destination reads the first or last value. Use the browser’s URL API and an allowlist:
function buildHandoffUrl(rawUrl, values) {
const url = new URL(rawUrl, window.location.href);
const allowedKeys = [
'journey_id',
'utm_source',
'utm_medium',
'utm_campaign',
'utm_content',
'utm_term',
'gclid',
'gbraid',
'wbraid'
];
for (const key of allowedKeys) {
const value = values[key];
if (typeof value === 'string' && value.length > 0) {
url.searchParams.set(key, value);
}
}
return url.toString();
}
searchParams.set() replaces an existing value rather than appending an uncontrolled duplicate. The URL object also keeps the query string before the fragment.
Even with safe construction, the production default should be to pass only journey_id. Add raw UTM or click identifiers only if the destination has a documented use for them and the transfer complies with the applicable consent, privacy, and platform requirements.
Redirects must preserve the intended allowlist
A redirector should not blindly forward every incoming parameter, because that can transport PII, debugging flags, open-redirect payloads, or unrelated campaign values. It should:
- accept only known destination routes;
- read a strict allowlist of parameters;
- preserve the exact case and complete value of opaque click IDs;
- URL-encode values using a standard URL library;
- put query values before the fragment;
- set
Cache-Control: no-storewhen the redirect is personalized; - use a temporary redirect such as
302or303when the handoff can change; - log the internal journey key rather than the full public URL where possible.
Google Ads documentation explicitly treats gclid as case-sensitive for offline conversion imports. Trimming, lowercasing, decoding and re-encoding incorrectly, or storing it in a field that truncates long values can make later imports fail.
postMessage for cooperative iframes and popups
The same-origin policy prevents a parent page from directly reading storage, DOM fields, or JavaScript state inside a cross-origin iframe. window.postMessage() provides a controlled communication channel, but only when the vendor implements the receiving side.
A safe pattern uses a readiness handshake and an exact destination origin:
const bookingFrame = document.querySelector('#booking-frame');
const bookingOrigin = 'https://booking.vendor.example';
window.addEventListener('message', (event) => {
if (event.origin !== bookingOrigin) return;
if (event.source !== bookingFrame.contentWindow) return;
if (event.data?.type !== 'booking:ready') return;
bookingFrame.contentWindow.postMessage(
{
type: 'merchant:journey-context',
journey_id: 'jrny_8f7d2c1a'
},
bookingOrigin
);
});
The iframe must independently validate the parent:
const allowedParentOrigin = 'https://www.example.com';
window.addEventListener('message', (event) => {
if (event.origin !== allowedParentOrigin) return;
if (event.source !== window.parent) return;
const message = event.data ?? {};
if (message.type !== 'merchant:journey-context') return;
if (typeof message.journey_id !== 'string') return;
saveJourneyReference(message.journey_id);
});
Both sides should validate the exact scheme, hostname, and port. The sender should not use * as targetOrigin when the destination origin is known. The receiver should validate event.origin, event.source, the message type, and the field schema before accepting data.
Pass a short-lived opaque token rather than UTM bundles, email addresses, phone numbers, or authentication credentials. postMessage is transport only; the provider still needs to persist the token and return it with the completed booking or payment.
This method does not apply to a normal top-level redirect after the source page is gone unless the integration retains a popup reference or another active messaging relationship. It also cannot repair an iframe whose provider offers no message listener.
Server-side storage: the most reliable business-level continuity layer
Client-side linking is useful for analytics, but revenue and lead reconciliation should not depend entirely on a URL surviving every browser transition. A server-side attribution record separates transport from attribution logic.
Reference workflow: first owned landing request → validate and capture campaign/click identifiers → create an opaque journey_id → store attribution, consent state, and timestamp on the server → pass only the journey token to the external system → receive the token with a webhook, API response, export, or return → join the completed booking/order/lead → send deduplicated analytics and advertising outcomes from the appropriate browser or server path.
What the server record should contain
| Field group | Recommended contents | Reason |
|---|---|---|
| Primary key | Random journey_id with sufficient entropy |
Safe public correlation without exposing sequential internal IDs |
| Acquisition | Allowlisted UTM values, landing URL, referrer classification, campaign timestamp | Preserves the chosen first-touch or last-touch context independently of later URLs |
| Ad click IDs | Exact gclid, gbraid, wbraid, or other platform IDs that are valid for the route |
Supports platform attribution and offline conversion workflows |
| Analytics context | GA client ID and session ID where legitimately collected and needed for the measurement design | Can support server-side event association, but should not replace the business key |
| Identity | Pseudonymous signed-in user_id or CRM mapping added only after a reliable match |
Supports account-level continuity without putting PII in public transport |
| Consent and policy state | Relevant consent status, collection purpose, jurisdiction or policy version, and timestamp | Prevents a later system from assuming that every captured value can be used for every destination |
| Handoff | Destination provider, route, handoff time, provider session ID, and current status | Makes failures and abandoned journeys diagnosable |
| Outcome | Booking/order/lead ID, value, currency, qualification status, cancellation/refund status | Connects marketing context to the actual commercial result |
| Deduplication | Provider webhook ID, transaction ID, internal event_id, processed timestamp |
Prevents retries or parallel browser/server paths from creating duplicates |
| Retention | Expiry timestamp and deletion status | Enforces a defined retention window rather than permanent identifier accumulation |
Example server handoff endpoint
The following JavaScript-style example illustrates the architecture. In production, the attribution store, consent rules, destination allowlist, error handling, and logging need a full security review.
import crypto from 'node:crypto';
const attributionKeys = [
'utm_source',
'utm_medium',
'utm_campaign',
'utm_content',
'utm_term',
'gclid',
'gbraid',
'wbraid'
];
function collectAttribution(url) {
const values = {};
for (const key of attributionKeys) {
const value = url.searchParams.get(key);
if (value) values[key] = value;
}
return values;
}
app.use(async (request, response, next) => {
const pageUrl = new URL(
request.originalUrl,
'https://www.example.com'
);
const attribution = collectAttribution(pageUrl);
if (Object.keys(attribution).length > 0) {
await firstPartyAttribution.capture(
request,
response,
attribution
);
}
next();
});
app.get('/go/booking', async (request, response) => {
const journeyId = `jrny_${crypto.randomUUID()}`;
const attribution = await firstPartyAttribution.read(request);
await attributionStore.create({
journey_id: journeyId,
attribution,
created_at: new Date().toISOString()
});
const destination = new URL(
'https://booking.vendor.example/schedule#/calendar'
);
destination.searchParams.set('journey_id', journeyId);
response.set('Cache-Control', 'no-store');
response.redirect(302, destination.toString());
});
Because the URL API is used, the output places ?journey_id=... before #/calendar. The external platform still needs to preserve and return the token; the redirect alone cannot guarantee that.
Define attribution rules before implementing transport
The server should not let “whichever URL parameter arrived last” become the attribution model by accident. Decide explicitly:
- whether the record stores first touch, latest eligible touch, both, or a multi-touch history;
- when a new journey starts;
- whether direct visits can overwrite a previous non-direct source;
- how long each click ID remains eligible;
- how consent changes affect later use;
- how rescheduled bookings, refunds, cancellations, and duplicate webhooks update the same business record.
URL transport should carry a key. Attribution policy should live in a tested server or warehouse model.
Pass a reference through the external system’s supported API
When a provider offers a create-session API, use it instead of constructing a public URL with a full attribution payload. The server can create the provider session, assign an internal reference, and store the provider’s session ID before redirecting the browser.
A generic request might look like this:
{
"client_reference_id": "jrny_8f7d2c1a",
"metadata": {
"journey_id": "jrny_8f7d2c1a"
},
"success_url": "https://www.example.com/order/complete?session_id={PROVIDER_SESSION_ID}"
}
The exact field names are provider-specific. Stripe Checkout, for example, documents client_reference_id as a value that can reconcile a Checkout Session with an internal system, and supports metadata for internal identifiers. Its documentation also warns against storing sensitive information in metadata. Calendly webhook payloads expose a tracking object for UTM values associated with an invitee, illustrating a different model in which the scheduling platform natively returns campaign fields.
Do not assume that a parameter accepted on the booking URL will appear in the webhook. Verify the complete contract:
- Which fields can be supplied when the external session is created?
- Which fields are stored by the provider?
- Which fields appear in the webhook or API retrieval?
- Are values length-limited or normalized?
- Can users see them?
- Are they included in exports?
- Does the provider sign webhook requests?
- How are retries and duplicate events identified?
- What happens on cancellation, rescheduling, refund, or session expiry?
The preferred join is usually:
journey_id → provider_session_id → booking/order/lead_id → conversion outcome
This keeps the acquisition payload in your first-party system while the provider receives only the minimum reference needed for reconciliation.
Why manually copying UTMs is fragile
Copying the landing page’s UTM parameters onto every checkout, calendar, account, and CRM link appears convenient. It often creates a larger data-quality problem.
1. It turns transport behavior into attribution policy
A downstream analytics tool may interpret UTMs on an internal handoff as a new campaign context. A booking system may store the last values it sees, while the CRM expects first touch. A warehouse may then contain three different “source” fields that all look authoritative. Store the source once under a defined rule, and pass an internal key.
2. Stale parameters survive longer than the campaign context
A user can bookmark, share, or reopen a decorated link days later. Another person may use the same URL. The resulting booking can inherit a campaign that did not acquire that person. Short-lived opaque tokens and server-side expiry reduce this risk.
3. Duplicate keys create parser-dependent results
A URL can end up with two utm_source or gclid values after several scripts append parameters. One destination may read the first value, another the last, and a third may reject the URL. Use a URL parser and set(), not concatenation and append().
4. Hash routes hide values from servers
A parameter added after # does not reach the server. Conversely, a client-side router may rewrite the fragment and remove an injected value before the widget reads it.
5. Redirects and shorteners frequently strip or rewrite values
Language redirects, authentication gateways, mobile deep-link services, and security proxies may reconstruct the target URL. Test every redirect hop with the browser Network panel and preserve only an approved allowlist.
6. Click IDs are opaque and exact
A click ID should not be lowercased, decoded into another character set, trimmed, split, or placed in a database field that is too short. For Google Ads offline imports, gclid is case-sensitive.
7. URLs are a poor place for identity
URLs can appear in browser history, access logs, analytics payloads, screenshots, support tickets, copied messages, and referrer headers. Never place an email address, phone number, customer name, raw CRM record number, authentication token, or a GA4 User-ID that reveals identity in UTM parameters or a public handoff URL.
8. Technical availability is not permission
A value being present in the landing URL does not automatically authorize storage, sharing with every vendor, or use for advertising. Capture and forwarding rules should reflect the relevant consent state, policy, contract, and data-processing purpose.
9. The vendor may not use the values as expected
Some providers document native UTM fields; others ignore unknown parameters; others preserve them only in the browser but omit them from webhooks. An undocumented parameter is not an integration contract.
10. Every new script becomes another source of truth
When GTM, application code, a CMS plugin, a redirect service, and the provider all decorate URLs independently, debugging becomes nearly impossible. Assign one owner to initial capture and one controlled handoff layer.
Recommended architecture by user journey
| User journey | Recommended stack | Why | What not to rely on |
|---|---|---|---|
| Landing page → another path on the same host | Normal first-party cookie or server session; capture attribution once; Google tag on all relevant pages | No cross-domain boundary exists | Repeated UTM decoration of internal links |
www.example.com → checkout.example.com |
Shared parent-domain journey cookie or backend session; same intended GA4 tag configuration; verify GA client/session continuity; Google conversion linking for ad-click storage | Subdomains can receive a parent-domain cookie, while Google tags handle their own measurement state | Assuming host-only cookies or localStorage automatically cross subdomains |
| Owned marketing domain → separate owned checkout domain | GA4 cross-domain measurement + Google conversion linking + server journey_id; SSO for signed-in identity |
Google linkers preserve platform measurement while the business token reconciles the order | Using _gl as an order key or expecting cookies to cross the root-domain boundary |
| Owned site → hosted checkout created through an API | Create the checkout session on the server; put journey_id in the provider’s internal reference or metadata; store provider session ID; use signed webhooks as the purchase source of truth |
The provider’s API and webhook survive browser return failures | Thank-you-page-only purchase tracking or a full UTM bundle in the public URL |
| Owned site → booking platform with native UTM support | Capture first-party attribution; pass the provider’s documented UTM fields if needed; also pass a supported internal reference where available; reconcile the booking through webhook/API | Native fields are more reliable than undocumented decoration | Assuming the displayed booking URL proves that the webhook retained the same values |
| Owned site → external booking link that accepts one custom parameter | Pass a short-lived journey_id; store all marketing context on your server; require the token in the provider result |
One opaque key minimizes leakage and keeps attribution logic first-party | Passing PII or every analytics cookie in the URL |
| Cross-origin booking iframe with vendor cooperation | Readiness handshake through postMessage; exact origin validation; send journey_id; webhook/API completion |
Works without direct iframe DOM or cookie access | Third-party cookies or reading the iframe DOM from the parent |
| Cross-origin iframe without vendor cooperation | Measure iframe visibility, start click, or outbound handoff only; use provider aggregate reports or request an integration | The same-origin policy blocks deterministic observation of internal completion | Inferring a completed booking from iframe height, focus, or time on page |
| Owned site → redirector → external provider | Server-side allowlisted redirect; capture first; attach journey_id; preserve Google _gl only where the destination supports the compatible linker; log each hop |
Centralizes URL construction and prevents intermediate stripping | Client-side string concatenation or forwarding every query parameter |
SPA route with # → external application |
Place the custom token in the query before the hash; use the URL API; configure Google linker fragment mode only when all participating tags require and support it | The server receives query parameters but never receives the fragment | Putting a server-required token after # |
| Payment processor → return to owned thank-you page | Webhook as purchase truth; provider session/transaction ID for reconciliation; GA4 unwanted-referral configuration for return-source hygiene; deduplicate browser and server events | The user may never return, while the webhook can still confirm payment | Treating unwanted referrals as cross-domain stitching or relying only on the return page |
| Logged-in account across multiple owned domains | SSO or shared identity provider; backend mapping to the same pseudonymous user_id; GA4 linker for anonymous pre-login browser continuity where appropriate |
Authentication is the durable identity layer; analytics linking remains a separate measurement layer | Using email in the URL or assuming the GA client ID is an account ID |
| External black box with no accepted data and no return interface | Track handoff_started on the owned page; use provider totals, campaign-specific links if officially supported, or aggregate reconciliation |
There is no deterministic technical join under the available integration surface | Claiming exact user-level attribution from timing, referrer suppression, or iframe heuristics |
A practical decision workflow
1. Same host? Use normal first-party state. 2. Same parent domain? Consider a scoped parent-domain journey cookie and verify Google tag behavior. 3. Separate domains that you control? Use GA4 cross-domain measurement, Google conversion linking, and a server token. 4. External provider with API, metadata, or webhook? Pass the token through that supported contract. 5. Cooperative iframe or popup? Use validated postMessage. 6. Shared authenticated user? Use SSO and the same pseudonymous business ID. 7. None of the above? Deterministic stitching is unavailable; measure the handoff and report the limitation.
Two additional questions apply at every step:
- What is the minimum data required? Prefer one opaque reference over copying campaign, browser, and identity data into the destination.
- Is the transfer permitted? Consent, platform policy, contracts, privacy notices, security, and retention rules remain part of the architecture.
When cross-domain continuity is physically impossible
Under the current integration surface, exact user-level continuity is unavailable when all viable bridges are absent. Typical examples include:
- the destination is on an unrelated origin and does not run your tag or a compatible linker;
- the provider strips all custom query parameters and offers no metadata field;
- there is no API, webhook, export key, provider session ID, or return parameter that can carry a reference;
- a cross-origin iframe has no cooperative
postMessageimplementation; - the user completes the action on another device without signing in or using a one-time account link;
- the user or browser blocks the storage and measurement methods on which the proposed link depends;
- the applicable consent or platform policy does not permit the intended storage or transfer;
- the provider aggregates results so that individual bookings cannot be associated with individual incoming sessions.
Several commonly suggested “fixes” do not remove those constraints:
- GA4 unwanted referrals can stop a payment domain from becoming the reported source when the user returns, but it does not measure the missing external steps or restore a lost identifier.
- Iframe height, focus, or visibility changes can indicate that something happened, but they do not prove that a booking or payment completed.
- A long timeout does not make two anonymous records the same person.
- Manually copied UTMs provide labels, not deterministic identity, unless the destination stores and returns them under a documented contract.
SameSite=Nonedoes not allow unrelated domains to share a first-party cookie.- Server-side tagging does not grant access to a third party’s browser or backend unless the third party sends data to that server.
The correct response is to document the blind spot and improve the provider integration, not to manufacture precision from weak proxies.
End-to-end testing checklist
Cross-domain measurement should be tested as a complete route, not as isolated tags. Use a fresh test journey and preserve the browser Network log across navigations.
At the first landing
- [ ] Confirm the expected UTM and ad-click parameters are present before any redirect or SPA rewrite.
- [ ] Confirm
gclid,gbraid, andwbraidare stored without case changes or truncation. - [ ] Confirm the attribution rule records first touch, latest touch, or both as designed.
- [ ] Confirm the generated
journey_idis random, non-sequential, and contains no PII. - [ ] Confirm consent state is available before storage or forwarding decisions are made.
At the handoff
- [ ] Click the actual UI element; do not only paste the destination URL into the address bar.
- [ ] Confirm GA4 adds
_glwhen the route is eligible for cross-domain measurement. - [ ] Confirm every redirect preserves the intended
_gl,journey_id, and approved click identifiers. - [ ] Confirm the custom query appears before
#when the server or redirect must read it. - [ ] Confirm no duplicate UTM or click-ID keys appear.
- [ ] Confirm no email, phone, customer name, raw CRM ID, or authentication secret appears in the URL.
- [ ] Confirm a JavaScript router or click handler does not navigate before linker decoration completes.
On the destination
- [ ] Confirm the destination accepts the incoming Google linker before its measurement events fire.
- [ ] Compare the relevant GA4 identifiers across domains; the intended client/session continuity should be visible in cookies and network requests.
- [ ] Confirm Google ad-click information is available to the conversion tags through the expected
_gcl_*storage where consent permits. - [ ] Confirm the external platform stores
journey_idin the documented field rather than only displaying it in the browser URL. - [ ] For an iframe, confirm both sides validate exact origins and the provider acknowledges the message.
- [ ] Test consent-granted and consent-denied states separately.
At completion
- [ ] Confirm the webhook or API result contains the same
journey_idor a provider session ID already mapped to it. - [ ] Validate the webhook signature and reject stale or invalid requests.
- [ ] Make webhook processing idempotent so retries do not duplicate orders or conversion events.
- [ ] Confirm cancellation, refund, reschedule, and failed-payment events update the original record.
- [ ] Deduplicate browser and server conversion paths using a stable transaction or
event_id. - [ ] Verify that a user who never returns to the thank-you page is still recorded through the server outcome.
Negative and edge-case tests
- [ ] Open the decorated URL after the token expires and confirm it cannot attach to the old journey.
- [ ] Share the URL with another browser and confirm identity is not inherited incorrectly.
- [ ] Test direct, organic, paid, email, and returning-user routes.
- [ ] Test mobile in-app browsers and privacy-focused browser settings.
- [ ] Test a redirect that changes language, protocol, hostname, or path.
- [ ] Test a form submission separately from a link click.
- [ ] Test logout so the GA4
user_idis cleared while the anonymous measurement design remains coherent. - [ ] Confirm the attribution record expires and is deleted under the defined retention policy.
For hash-based applications and route-change lifecycle issues, see SPA analytics architecture: page views, events, timers, and state cleanup.
Implementation checklist
- [ ] Inventory every domain, subdomain, iframe, redirector, and external provider in the conversion route.
- [ ] Classify each required value as campaign metadata, click ID, analytics identity, business journey ID, signed-in user ID, or event deduplication ID.
- [ ] Use GA4 cross-domain measurement only for domains that can run the compatible Google tag and belong to the same intended measurement journey.
- [ ] Audit whether the current Google tag already provides conversion linking before adding a separate Conversion Linker tag.
- [ ] Use a parent-domain cookie only across trusted subdomains and only for the minimum state that needs that scope.
- [ ] Keep sensitive authentication cookies host-bound unless a reviewed SSO design requires broader scope.
- [ ] Put server-required custom parameters in the query string before the fragment.
- [ ] Use the URL API and an allowlist; never concatenate arbitrary query strings.
- [ ] Preserve opaque ad-click identifiers exactly and store them with the first-party journey or lead record.
- [ ] Generate a random
journey_idand use it as the default external handoff value. - [ ] Prefer create-session APIs, metadata, and signed webhooks over browser-return-only tracking.
- [ ] Validate exact origins for every
postMessageexchange. - [ ] Keep PII out of URLs, UTM fields, GA4 User-ID, and provider metadata unless a specific approved workflow requires otherwise.
- [ ] Define first-touch, latest-touch, expiry, cancellation, refund, and deduplication rules before deployment.
- [ ] Treat GA4 unwanted referrals as source hygiene, not as cross-domain identity.
- [ ] Document routes where deterministic continuity is unavailable and label aggregate estimates accordingly.
Methodology and sources
This article is based on a review of current official Google Analytics, Google Tag Platform, Google Tag Manager, Google Ads, MDN Web Docs, Stripe, and Calendly documentation, supplemented by established Google Tag Manager implementation guidance from Simo Ahava. The recommendations separate browser-level measurement continuity from server-level commercial reconciliation and prioritize designs that can be verified through network requests, first-party storage, provider APIs, and signed outcome notifications.
Provider capabilities vary by product, plan, embed type, API version, and account configuration. Before implementation, verify that the exact destination supports the parameter, metadata field, webhook property, and return behavior described in its current documentation.
- Google Analytics: Set up cross-domain measurement
- Google Tag Platform: Measure activity across multiple domains
- Google Tag Manager: Conversion Linker
- Google Tag Platform: Set up Consent Mode on websites
- Google Analytics: Measure activity across platforms with User-ID
- Google Analytics: Best practices to avoid sending personally identifiable information
- Google Ads: Set up offline conversions using Google click ID
- Google Analytics: Identify unwanted referrals
- Google Analytics: Cookie usage on websites
- MDN Web Docs:
Set-Cookieheader - MDN Web Docs: Same-origin policy
- MDN Web Docs:
window.postMessage() - MDN Web Docs: URI fragment
- MDN Web Docs:
URLSearchParams.set() - Stripe API Reference: Create a Checkout Session
- Stripe Documentation: Checkout Sessions API and metadata
- Calendly Developer: Webhook payload and tracking fields
- Calendly Developer: Receive scheduled-event data through webhooks
- Simo Ahava: Cross-domain tracking in Google Analytics 4
This article is for technical and operational information only and is not legal advice. Consent, privacy, advertising-platform, cookie, and data-retention requirements vary by jurisdiction and implementation. metricfixer is not affiliated with Google, Stripe, Calendly, MDN, Simo Ahava, or other third-party platforms and publishers mentioned in the article. Browser behavior, platform documentation, identifier formats, APIs, and product capabilities may change after publication.