GTM 11 min read

Click Identifiers in GTM: The Common Mistakes That Break gclid, fbclid, and wbraid Tracking

If your gclid is dropping between the ad click and the conversion, you're not just losing attribution — you're losing enhanced conversions, offline import matches, and Consent Mode modelling accuracy.

A
Ashwani Bhasin
·

If your gclid is dropping between the ad click and the conversion, you’re not just losing attribution. You’re losing Enhanced Conversions, offline import matches, and Consent Mode modelling accuracy. Most GTM setups mishandle click IDs in at least one of five predictable ways, and the diagnostic pattern is almost always the same: the ID arrives fine at the landing page, then vanishes somewhere between the router push, the cookie banner, and the checkout redirect.

I’ve audited enough Shopify, headless, and lead-gen stacks to know that the click ID capture layer is the single most fragile part of a measurement setup. It looks trivial. It isn’t. Here’s the practitioner playbook Simo’s recent piece hinted at but didn’t fully walk through.

The Six Click Identifiers You Actually Need to Handle

Before touching GTM, understand what you’re capturing. Each parameter has its own lifespan, its own storage rules, and its own downstream consumer. Treating them as one generic “click ID” is where most implementations start going wrong.

ParameterSourcePurposeDefault Cookie LifetimeNotes
gclidGoogle Ads (auto-tagging)Ads → GA4/Ads conversion stitching, Enhanced Conversions, offline imports90 days (_gcl_aw)Standard for Search/Display when user has cookies allowed
gbraidGoogle AdsiOS app-to-web conversions where IDFA is restricted90 days (_gcl_gb)Aggregated, no user-level join
wbraidGoogle AdsWeb conversions from iOS app clicks under ATT restrictions90 days (_gcl_gb)Cannot be used for user-level Enhanced Conversions
fbclidMeta AdsMeta CAPI matching, browser+server dedup90 days (_fbc) recommendedMust be reformatted as fb.1.<timestamp>.<fbclid> before storage
msclkidMicrosoft AdsUET conversion attribution, Enhanced CPC90 days (_uetmsclkid)Auto-tagging must be enabled in Bing UI
ttclidTikTok AdsTikTok Events API deduplication30 days (ttclid cookie)Shorter window than Meta or Google

Two things fall out of this table that most guides gloss over.

First: wbraid and gbraid are not drop-in gclid replacements. If you’re pushing wbraid into an Enhanced Conversions payload expecting user-level matching, you’ll get modelled data at best. Google’s docs are explicit that wbraid conversions are uploaded via a separate endpoint and aggregated.

Second: the _fbc cookie is not just fbclid. Meta expects fb.<subdomainIndex>.<creationTime>.<fbclid>. Storing the raw fbclid and passing it to CAPI is one of the most common mistakes I see, and it silently kills match quality.

Capturing Click IDs Correctly — And Why URL Variables Fail on SPAs

The standard GTM pattern is a URL variable of type Query, pointed at gclid, firing on All Pages. This works fine on a WordPress site with hard navigations. It fails immediately on Next.js, Nuxt, Remix, Shopify Hydrogen, or any React app doing client-side routing.

Here’s why. The Query variable reads document.location.href at the moment the variable is resolved. On an SPA, the user lands on /lp?gclid=ABC123, your Page View tag fires, the click ID is captured. Then the router pushes to /lp/step-2, the URL updates via history.pushState, and the query string is either preserved (rare) or stripped (common). Any tag firing after that navigation reads an empty gclid.

The fix isn’t a History Change trigger alone. History Change fires after the URL has already been rewritten, so if the router stripped the parameter, you’re reading empty. The correct pattern is:

  1. Capture on the earliest possible event (Consent Initialization or Initialization trigger).
  2. Persist to a first-party cookie immediately.
  3. Read from the cookie, not the URL, for all downstream tags.

Here’s the Custom HTML tag I deploy for this. It handles all six parameters, formats fbclid correctly for _fbc, and respects Consent Mode.

<script>
(function() {
  var params = new URLSearchParams(window.location.search);
  var clickIds = {
    gclid:   { cookie: '_aum_gclid',   ttl: 7776000 },
    gbraid:  { cookie: '_aum_gbraid',  ttl: 7776000 },
    wbraid:  { cookie: '_aum_wbraid',  ttl: 7776000 },
    fbclid:  { cookie: '_aum_fbclid',  ttl: 7776000 },
    msclkid: { cookie: '_aum_msclkid', ttl: 7776000 },
    ttclid:  { cookie: '_aum_ttclid',  ttl: 2592000 }
  };

  function setCookie(name, value, ttl) {
    var domain = '.' + location.hostname.split('.').slice(-2).join('.');
    document.cookie = name + '=' + encodeURIComponent(value) +
      '; Max-Age=' + ttl +
      '; Path=/; Domain=' + domain +
      '; SameSite=Lax; Secure';
  }

  Object.keys(clickIds).forEach(function(key) {
    var val = params.get(key);
    if (!val) return;

    if (key === 'fbclid') {
      val = 'fb.1.' + Date.now() + '.' + val;
    }
    setCookie(clickIds[key].cookie, val, clickIds[key].ttl);
  });
})();
</script>

A few deliberate choices in that snippet worth calling out:

  • Domain scope uses the eTLD+1. If you set Domain=www.example.com, the cookie won’t be readable from checkout.example.com. On Shopify especially, this destroys attribution at the checkout hop.
  • SameSite=Lax, not None. Lax survives top-level navigations from ad platforms (which is what a click is). SameSite=None; Secure is only needed if a cross-site iframe reads the cookie, which almost never applies here.
  • 90-day TTL for everything except TikTok, matching each platform’s attribution window. Storing longer is pointless and inflates your cookie footprint for no benefit.
  • First-party namespace (_aum_) so the cookies survive ITP better than platform-native ones and can be forwarded through sGTM.

Once the cookie is set, every downstream variable should be a 1st Party Cookie variable, not a URL variable. This is the single change that fixes the largest class of SPA click ID bugs.

The Correct Trigger Order on SPAs

Even with cookie persistence, trigger sequencing matters. If your click ID capture tag fires on All Pages via a DOM Ready trigger, and your Meta pixel fires on Consent Initialization, the pixel runs first and reads no cookie.

The order I use in every SPA container:

  1. Consent Initialization – All Pages: click ID capture tag runs here, before anything else.
  2. Initialization – All Pages: consent defaults, GTM Consent Mode signals.
  3. All Pages: analytics tags, pixels, etc., all reading from cookies.
  4. History Change: only used to re-fire page_view events, never for click ID capture.

If you’re on GA4 and want to send the click ID as an event parameter, pull it from the cookie variable and attach it to your page_view and purchase events. Do not rely on GA4’s automatic collection alone — it captures gclid into session_start but doesn’t propagate the raw value into downstream events where you might need it for BigQuery joins. Our GA4 setup service handles this pattern by default.

This is where I see the most confusion. Consent Mode v2 doesn’t block your Custom HTML tag from reading URL parameters. It doesn’t stop you from writing a first-party cookie. What it does is govern what Google’s tags do with that data.

Here’s the behaviour matrix I keep pinned:

Consent Stategclid in URL_gcl_aw cookie written by Google tagEnhanced Conversions payload sentModelling contribution
ad_storage=granted, ad_user_data=grantedReadYesYes, with user dataFull user-level
ad_storage=denied, ad_user_data=deniedRead (in memory)NoNoCookieless pings, modelled
ad_storage=granted, ad_user_data=deniedReadYesNo user identifiersAttribution only
analytics_storage=deniedReadNo _gaN/AGA4 modelled

Two operational implications.

First, your custom _aum_gclid cookie is not automatically covered by Consent Mode. If a user denies ad_storage, Google’s own _gcl_aw won’t be written, but your custom cookie will be — unless you explicitly gate it. In a strict interpretation of TCF and GDPR, that’s a compliance problem. Add a consent check to the capture script:

// Wrap the setCookie calls
if (window.google_tag_data && 
    window.google_tag_data.ics && 
    window.google_tag_data.ics.getConsentState('ad_storage') === 2) {
  // ad_storage is granted (2 = granted, 1 = denied)
  setCookie(clickIds[key].cookie, val, clickIds[key].ttl);
}

Second, modelling quality depends on the click ID reaching Google’s servers even under denied consent. Consent Mode v2’s cookieless pings include the gclid (redacted or not depending on region), which is how Google reconstructs modelled conversions. If you strip gclid at the CDN because you’re paranoid about PII, you break modelling. Don’t strip it.

Forwarding to Server-Side GTM Without Losing the ID

Server-side GTM is where click IDs go to die if your CDN, WAF, or consent layer is aggressive. The three failure points I check in every sGTM audit:

1. Cloudflare / CDN rewrites. Cache rules that normalise URLs by stripping query parameters will remove gclid before it reaches your origin. Check your cache key configuration and add an exception for known ad parameters, or bypass cache when they’re present.

2. Consent Management Platform redirects. Some CMPs do a top-level redirect to append a consent hash to the URL. If that redirect drops the original query string, the click ID is gone before your tags fire. Test by clicking your own ad and watching the URL through the CMP’s redirect chain in DevTools’ Network tab with “Preserve log” enabled.

3. sGTM Client claim order. If you have both a GA4 client and a custom client, the GA4 client will claim the request and your custom transformations may not run. Explicit client priority matters.

The right sGTM pattern for enriching outbound events with click IDs is to read them from the incoming request cookies, not to expect the browser tag to send them in the event payload. Here’s the transformation logic in a sGTM variable:

// Sandbox JS variable in sGTM
const getCookieValues = require('getCookieValues');

const gclid = getCookieValues('_aum_gclid')[0];
const fbc   = getCookieValues('_aum_fbclid')[0];
const ttclid = getCookieValues('_aum_ttclid')[0];

return {
  gclid: gclid || undefined,
  fbc: fbc || undefined,
  ttclid: ttclid || undefined
};

Attach this variable to your GA4, Meta CAPI, and TikTok Events API tags. Because sGTM reads the cookies from the request headers, you get the click ID even if the browser event payload is missing it — for example, on a headless checkout webhook. Our GTM implementation team uses this exact pattern for Shopify Plus stores where the checkout runs on a different subdomain.

For Enhanced Conversions specifically, the gclid should be sent alongside the hashed user data (email_address, phone_number). Google’s Ads API prefers user data for matching but uses gclid as a fallback and for offline import reconciliation. Sending both increases match rate by 10-15% in my testing.

Shopify-Specific Gotchas

Shopify deserves its own callout because the checkout is a walled garden. Three things break click ID persistence on Shopify:

  • checkout.shopify.com sub-processor domains on Shopify Plus with checkout extensibility. Cookies scoped to .yourdomain.com won’t be read here. You need to use Shopify’s Web Pixels API or forward click IDs via the checkout.completed webhook to sGTM.
  • The attributes field on the cart object is your friend. Push the click IDs into cart attributes on the storefront, and they persist through checkout and appear on the order. From there, an sGTM tag reading the order webhook can send Enhanced Conversions server-side.
  • App Proxy or headless setups where the storefront and checkout are on completely different domains break third-party cookies entirely. Server-side capture is the only reliable path.

If you’re running headless Shopify, our Shopify development team typically implements a small backend endpoint that receives the click ID from the storefront, stores it against the cart token, and retrieves it at order creation time.

Common Mistakes and Troubleshooting

These are the failure patterns I see in almost every audit:

Mistake 1: Reading gclid from the URL variable inside a Conversion tag on a thank-you page. The user has navigated through 4 pages since the landing. The URL no longer contains gclid. Fix: read from the cookie variable.

Mistake 2: Storing raw fbclid and passing it to Meta CAPI. Meta expects the fb.1.<timestamp>.<fbclid> format. Raw fbclid returns a “low match quality” warning in Events Manager. Fix: format at capture time, not later.

Mistake 3: Not URL-decoding before storage. Some CMPs URL-encode the query string during their redirect. Storing %3D inside your cookie value corrupts the ID. Use decodeURIComponent on the extracted value.

Mistake 4: Setting the cookie on www.example.com instead of .example.com. The checkout on checkout.example.com can’t read it. Match rate craters.

Mistake 5: Firing the capture tag on DOM Ready or Window Loaded. Any tag firing before then reads an empty cookie. Use Consent Initialization.

Mistake 6: Blocking gclid at the CDN “for privacy.” Consent Mode modelling needs the parameter to reach Google. Blocking it downgrades your modelled conversions.

The Debug Checklist

Before signing off on any click ID implementation, run these five tests. All five must pass.

  1. The self-click test. Click your own live Google Ad from an incognito window. In DevTools → Application → Cookies, confirm _aum_gclid, _gcl_aw, and _ga all appear. Note the domain scope of each.
  2. The SPA persistence test. After landing, click through 3 internal links (each triggering a route change). Reload the page. Open the cookie again. The gclid value must still be there.
  3. The subdomain test. Navigate to a subdomain (staging, checkout, blog). Confirm _aum_gclid is still readable. If not, your Domain attribute is wrong.
  4. The consent denial test. Deny cookies in your banner. Confirm _aum_gclid is NOT written (if you gated it correctly), but Google’s cookieless ping still includes gclid in the request payload (check Network → google-analytics.com/g/collect).
  5. The sGTM forward test. In sGTM Preview, trigger a purchase event. In the outgoing GA4 and Meta CAPI request bodies, confirm gclid and fbc are present with correct formatting.

If test 4 fails and the cookieless ping is missing gclid, your consent layer is stripping the URL parameter before Google’s tag can read it. That’s a CMP configuration issue, not a GTM issue.

Key Takeaways

  • Treat each click ID as a distinct object with its own format, TTL, and downstream consumer. wbraid is not gclid, and fbclid needs reformatting before Meta will match it.
  • On SPAs, capture click IDs at Consent Initialization and read from first-party cookies for all downstream tags. URL variables fail the moment the router pushes a new state.
  • Scope your cookies to the eTLD+1 with SameSite=Lax; Secure and a 90-day TTL. Anything narrower breaks cross-subdomain conversions.
  • Consent Mode v2 doesn’t block your custom capture, but it should — gate your cookie writes on `ad_storage=granted
#GTM#Click Identifiers#Attribution#Conversion Tracking

Share this article

Want This Implemented Correctly?

Let our team apply these concepts to your specific setup — with QA validation and 30 days of support.