pixellint

Identity · marketers · engineers · PMs

gclid, fbclid, ttclid, msclkid, li_fat_id

The landing page is where the click id arrives; the thank-you page is where you spend it. A pretty redirect that strips the query is an attribution outage that still shows as a click.

Who owns which parameter

gclid is Google Ads auto-tagging. fbclid is Meta. ttclid is TikTok. msclkid is Microsoft Advertising. li_fat_id is LinkedIn. twclid is X (Twitter). They are not interchangeable, and putting gclid in a Meta CAPI body does not fill fbc. Each platform joins on its own token. A shared click_id column that always stores gclid will starve every other graph.

Some Google clicks also carry gbraid or wbraid when cookies are thin. Treat those as first-class click ids for Google, not as UTM content. Google Ads UploadClickConversions wants one of gclid, gbraid, wbraid, or userIdentifiers. utm_source=meta never becomes fbclid. utm_campaign is a report dimension. It is not a join key the bidder uses the way it uses gclid.

UTMs are not click ids

UTM parameters tell analytics which campaign creative you think this session came from. They are first-party query you control. They are not proof of a paid click the ads platform issued. Last-click UTM in GA4 can look attributed while Google Ads has no gclid and will not count the conversion against the bid.

If a redirect kept utm_source and dropped gclid, GA4 still tells a story and Ads does not. Keep both. If a marketer pastes UTMs onto a Meta ad URL and never enables the fbclid that Meta already appends, you still need fbclid from Meta's click, not a homemade utm_content.

Capture once, send twice

Read the query on the first landing hit. Write a first-party cookie or a row on the session. Fire the conversion with the stored value, not with whatever happens to be on the thank-you URL (often nothing, after a hosted checkout). Browser tags that only scrape window.location at conversion time miss every cross-domain cart.

Server CAPI that never received the landing query has no click id to send. The storage hop is the product. Persist gclid, fbclid, ttclid, msclkid, li_fat_id, and twclid server-side keyed by a session you own. TTL them past your longest click-through window, not past the cookie's max-age, because Safari will expire the JS cookie first.

Redirects eat them

http to https, apex to www, and locale prefixes that drop the query string will eat gclid. Debug hop by hop. A 302 that lands cleanly with a pretty URL is the usual paid-search incident. Ad-server click chains should append the vendor id, not replace the whole query.

If a hop 200s in the middle, the user never reaches you and you will not notice until the platform asks where the conversions went. Link shorteners, app deep-link wrappers, and 'remove tracking parameters' privacy extensions are the other eaters. You cannot fix the extension. You can stop your own redirects from stripping unknown query keys.

What to send on CAPI

Meta wants fbc built from fbclid, not a raw fbclid in a random field, when you have the cookie shape. Google Ads wants gclid or gbraid or wbraid on the conversion upload. TikTok wants ttclid in the Events API context. Microsoft wants msclkid on UET. LinkedIn wants li_fat_id on Insight / CAPI mappings. X wants twclid. Sending all of them in one JSON blob to Meta does not help Meta.

Pixellint does not invent a universal click-id rule. It checks vendor fields that those packs document. Capture is still your job. Pixellint is not affiliated with Google, Meta, TikTok, Microsoft, LinkedIn, or X.

// WRONG: thank-you page only
const gclid = new URLSearchParams(location.search).get('gclid');

// RIGHT: first landing hit, persist, reuse at purchase
const landing = new URLSearchParams(firstRequest.url.split('?')[1] || '');
session.gclid = landing.get('gclid') || session.gclid;
session.fbclid = landing.get('fbclid') || session.fbclid;
session.ttclid = landing.get('ttclid') || session.ttclid;
session.msclkid = landing.get('msclkid') || session.msclkid;
session.li_fat_id = landing.get('li_fat_id') || session.li_fat_id;
session.twclid = landing.get('twclid') || session.twclid;

Check the artifact

Paste the pixel URL or JSON body into the playground. Same engine as pixellint validate. Nothing leaves the tab.