Technical Deep-Dive · 11 min read
Click ID Server-Side Recovery: gclid, fbclid, ttclid for CAPI
gclid (Google), fbclid (Meta), ttclid (TikTok), msclkid (Microsoft), and 13 other click identifiers are the only stable signal that links an ad click to a conversion across iOS 17 ATP, third-party cookie deprecation, and bot traffic. This playbook shows the technical recovery pattern: capture click IDs at landing, persist them in a first-party cookie, and forward them via server-side CAPI for 95%+ match rate.
What is a click ID, and why does it carry the attribution
A click ID is a short opaque string an ad network attaches as a query parameter to the landing URL when a user clicks one of its ads. The network minted the string at click time on its own servers, so it is the only identifier guaranteed to map the downstream conversion back to the specific ad click that drove it. Google Ads attaches gclid; Meta attaches fbclid; TikTok attaches ttclid; Microsoft Ads attaches msclkid; and so on for each network. The format and length differ per network, but the function is identical — a one-to-one key between a click event in the ad network's logs and a conversion event in your store.
Click IDs matter for two reasons that compound. First, they are the only identifier the ad network itself trusts when you send a conversion back via CAPI or Enhanced Conversions; pixel cookies and IP-based fingerprints are weaker match signals because they can be spoofed or shared. Second, they are the only identifier that survives the parts of the modern web that break other signals — third-party cookie deprecation, Safari ITP's seven-day localStorage cap, and cross-device journeys where the user clicks on mobile and converts on desktop. Without click IDs, the ad network's bidding algorithm receives degraded conversion signal and over-allocates to lower-intent audiences.
Without click IDs flowing back to the ad network, the typical match rate for a DTC store post-iOS 17 falls to roughly sixty percent. With click IDs captured at landing and forwarded via server-side CAPI, the match rate stays at ninety-five percent or higher. The fifteen to thirty-five point gap is the difference between a campaign that the algorithm correctly attributes and a campaign that the algorithm under-credits, leading to under-spend on what is actually your highest-LTV channel.
Why click IDs are the only stable cross-device, cross-cookie attribution signal
Every other signal you might attempt to attribute on has been degraded by browser and OS-level privacy changes over the past three years. Third-party cookies are deprecated on Safari (since 2020) and Brave (since launch), are blocked by default on Firefox, and are scheduled for full deprecation on Chrome under the Privacy Sandbox roadmap. Device fingerprinting is restricted by Safari ITP's storage partitioning and by Brave's randomization. IP-based geolocation is increasingly proxied through iCloud Private Relay (default-on for iOS 15+ Safari) and standard mobile carrier NAT, which makes IP a coarse signal at best.
Click IDs are not affected by any of these mechanisms because they live in the URL itself, not in browser storage. When a user clicks an ad, the network appends the click ID to the destination URL as a query string, and the user's browser passes it to your server in the GET request. The click ID exists in the request line itself, before the browser has executed any JavaScript, before any cookie has been read or set, before any fingerprint can be computed. That earliness is what makes it stable.
The one threat to click ID stability is iOS 17 Link Tracking Protection, which strips known click identifiers from URLs opened in Mail, Messages, and Private Browsing Safari before the destination page loads. Apple maintains the strip-list internally and updates it with each iOS minor release. The mitigation pattern is documented in the iOS 17 attribution recovery playbook — capture click IDs via the network's pixel SDK at the ad-network side (where Apple cannot strip), forward via CAPI, and on the destination side use a first-party redirect that survives ATP.
Cross-link: iOS 17 Attribution Recovery for the ATP-specific recovery pattern. This article documents the click-ID capture and CAPI-forwarding layer; the iOS 17 article documents the upstream recovery when Apple strips the ID before it reaches the destination.
The 17 click ID formats Admaxxer captures
The list of click identifiers a modern DTC store needs to capture is longer than "fbclid and gclid" — there are seventeen distinct formats across nine ad networks as of May 2026, and the list grows as new networks launch and existing networks add new attribution surfaces. Admaxxer's first-party pixel captures every format listed below by default; the captured value is written to a 365-day first-party cookie and mirrored to a server-side session.
| Click ID parameter | Network / Surface |
|---|---|
fbclid | Meta (Facebook + Instagram + Messenger + WhatsApp ads) |
gclid | Google Ads — search and shopping |
dclid | Google Display Network (DoubleClick) ads |
wbraid | Google Ads iOS app-to-web (Web-to-Browser App Identifier) |
gbraid | Google Ads iOS app-to-web (Google Browser App Identifier) |
gad_source | Google Performance Max source qualifier |
ttclid | TikTok Ads (in-feed + Spark Ads + TopView) |
msclkid | Microsoft Ads (Bing search + Microsoft Audience Network) |
li_fat_id | LinkedIn Ads conversion tracking ID |
epik | Pinterest Ads |
ScCid | Snapchat Ads (capitalized variant) |
sc_cid | Snapchat Ads (lowercase variant) |
twclid | X/Twitter Ads |
igshid | Instagram Stories share-ID |
yclid | Yandex Direct |
s_kwcid | Adobe Search keyword campaign ID |
q_at | RTBHouse retargeting click-attribution token |
The capture surface is the same for all seventeen — a single pass over the URL query parameters at landing, writing every recognized key to the first-party store. The forwarding surface differs by network: gclid flows to Google Enhanced Conversions; fbclid composes the fbc field for Meta CAPI; ttclid flows to the TikTok Events API. Adding a new network to the list is a single line in the click-IDs registry plus a new CAPI forwarder; the rest of the recovery pipeline is identical.
Why localStorage is not enough — three failure modes
The naive implementation of click ID capture is to read the URL query string in JavaScript at landing, parse out fbclid and gclid, and call window.localStorage.setItem. This works in a one-page-load demo and fails in production for three independent reasons that compound.
- Safari ITP caps localStorage to a 7-day TTL when the script that wrote it is loaded as a third-party. The cookie API has the same restriction, but localStorage is hit harder because Safari treats it as ephemeral storage from third-party origins. A user who clicked an ad on day 1 and returns to convert on day 8 has lost the click ID entirely if it was stored on a third-party script's storage.
- iOS 17 Link Tracking Protection strips known click identifiers from URLs opened in Mail, Messages, and Private Browsing Safari before any JavaScript runs. The JavaScript that would have read the URL never sees the parameter — the OS removed it during URL resolution. localStorage capture only works after the JS has executed, which is too late.
- Cross-device journeys are invisible to localStorage by design. localStorage is per-device, per-browser, per-profile. A user who clicks an ad on their iPhone in the morning and converts on their desktop browser in the evening has two separate localStorage stores. The click ID captured on the iPhone is irrelevant to the desktop conversion event.
The first-party cookie pattern (described below) addresses failure mode one. The server-side session mirror (also below) addresses failure mode three. The iOS 17 ATP recovery via first-party redirect addresses failure mode two — but ATP recovery is documented in its own playbook because it predates the click ID capture layer in the recovery chain.
The server-side session mirror — surviving cross-device journeys
First-party cookies fix the storage-TTL problem and the ATP stripping problem on the same device. They do not fix cross-device journeys: a click ID captured in the iPhone Safari cookie store is not visible to the same user's desktop Chrome browser when they convert. To attribute the cross-device journey, the click ID must live somewhere both devices can reach — a server-side session keyed on a pseudonymous identifier that bridges the two devices.
Admaxxer's server-side session is keyed on a visitor's pseudonymous ID, generated client-side at first-touch from a stable browser fingerprint plus a randomized salt. The visitor ID is written to the same first-party cookie store as the click IDs. When the user later submits an email (newsletter signup, checkout email field, or login), the server hashes the email and writes a deterministic mapping from email-hash to visitor ID in the session store. On a subsequent device, when the user submits the same email (e.g., during checkout on the new device), the server looks up the email-hash, finds the prior visitor ID, and retrieves the original click IDs captured on the first device.
The mapping is one-way and hashed — Admaxxer stores SHA-256 hashes of the email plus a per-workspace salt, never the cleartext email. The pseudonymous visitor ID is opaque and does not itself contain personally identifying information. This satisfies both GDPR and CCPA pseudonymization standards while preserving the cross-device attribution chain.
The session mirror also serves as the durable store that survives a user's cookie wipe. If a user clears their browser cookies on the desktop (a common Chrome incognito or Safari clear-history behavior), the cookie store on that browser is wiped — but the email-keyed server-side session is intact. On the user's next login, the visitor ID is rebound to the now-empty cookie store and the prior click ID attribution is recovered.
The Meta CAPI payload — fbc and fbp per the v25 spec
Meta's Conversions API expects click ID data in the user_data block of the event payload, in two specific fields: fbc (Facebook click identifier, derived from fbclid) and fbp (Facebook browser identifier, set by the Meta Pixel as a first-party cookie called _fbp). The fbc field is not the raw fbclid value — it is a composite string in the format "fb.1.{timestamp}.{fbclid}", where the timestamp is the Unix epoch milliseconds at click time. The fbp field is the literal _fbp cookie value if present, otherwise omitted.
Admaxxer's server-side CAPI forwarder constructs the fbc field on the fly from the captured fbclid plus the click timestamp Admaxxer recorded when the cookie was written. The order webhook handler retrieves the click ID from the visitor's session, builds the fbc + fbp pair, and posts the full event to Meta's /events endpoint with the event_id set to the Shopify order number (this is the deduplication key — see the next section).
The full Meta CAPI POST body for a typical Purchase event looks like the example below. The data array contains one event; in production Admaxxer batches up to fifty events per request for rate-limit efficiency, but the per-event shape is identical.
POST https://graph.facebook.com/v25.0/{PIXEL_ID}/events
Content-Type: application/json
Authorization: Bearer {CAPI_ACCESS_TOKEN}
{
"data": [{
"event_name": "Purchase",
"event_time": 1747526400,
"event_id": "shopify-order-5839124",
"event_source_url": "https://store.example.com/checkout/thank-you",
"action_source": "website",
"user_data": {
"em": ["e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"],
"ph": ["7d793037a0760186574b0282f2f435e7d4e23d5f4f3b4f5f7b3e4e5d6c5a4b3a"],
"fbc": "fb.1.1747526240000.IwAR2x3PqK8jM4nW9zY1aB7cD0eF6gH3iJ5kL7mN9oP1qR3sT5uV",
"fbp": "fb.1.1747512000000.2098534201",
"client_ip_address": "203.0.113.42",
"client_user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_2 like Mac OS X) AppleWebKit/605.1.15"
},
"custom_data": {
"currency": "USD",
"value": 142.50,
"content_ids": ["sku-blue-tee-m"],
"content_type": "product",
"num_items": 1
}
}],
"partner_agent": "admaxxer-1.0"
}
Notice that email (em) and phone (ph) are SHA-256 hashed — Meta's spec requires hashing for personal identifiers in the user_data block. Admaxxer hashes server-side before the POST; the cleartext email and phone never leave the order webhook. The client_ip_address and client_user_agent are not hashed because they are not personally identifying in isolation. The partner_agent field identifies Admaxxer as the CAPI integration source — Meta uses this to credit the integration partner and to surface integration-specific health checks in the pixel diagnostic UI.
Google Enhanced Conversions — gclid plus hashed user data
Google's equivalent of Meta CAPI is Enhanced Conversions, which posts conversion events to the Google Ads API with the captured gclid plus hashed user identifiers (email, phone, address). The POST shape is different from Meta's — Google uses the googleads.googleapis.com Conversion Upload endpoint, and the payload structure follows the protobuf schema documented in the Google Ads API reference.
Admaxxer's server-side Enhanced Conversions forwarder uses the Google Ads API directly via the developer token + refresh token paste flow. When an order webhook fires, the forwarder reads the captured gclid from the visitor's session, hashes the email and phone with SHA-256 (Google requires the same hashing as Meta), and posts the conversion event. The conversion_action resource_name references the Google Ads conversion action you've defined for online purchases.
POST https://googleads.googleapis.com/v17/customers/{CUSTOMER_ID}:uploadClickConversions
Authorization: Bearer {OAUTH_ACCESS_TOKEN}
developer-token: {DEVELOPER_TOKEN}
Content-Type: application/json
{
"conversions": [{
"gclid": "Cj0KCQjwzN-vBhCkARIsAEU8aXKvW1c9Q4M6f2I3jH5pL7rR9sT0uV3xY8zA1bB4cD6eF8gH",
"conversion_action": "customers/1234567890/conversionActions/987654321",
"conversion_date_time": "2026-05-18 14:00:00-07:00",
"conversion_value": 142.50,
"currency_code": "USD",
"order_id": "shopify-order-5839124",
"user_identifiers": [
{
"hashed_email": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
{
"hashed_phone_number": "7d793037a0760186574b0282f2f435e7d4e23d5f4f3b4f5f7b3e4e5d6c5a4b3a"
}
]
}],
"partial_failure": true
}
The order_id field is critical for deduplication — Google uses it to suppress duplicate conversion uploads if the same order is posted twice (a common occurrence when an order webhook fires both client-side via gtag and server-side via CAPI). The partial_failure flag instructs Google to accept the request even if a subset of conversions fail validation, returning per-conversion error details in the response. Admaxxer's forwarder reads the failure response and writes any rejections to the /attribution/health surface so the operator can fix them rather than discover the data loss weeks later.
Performance Max campaigns lean particularly hard on Enhanced Conversions — the algorithm's bidding logic uses the hashed user identifiers to model audience overlap and predict conversion likelihood. A Performance Max campaign running without Enhanced Conversions is the most common DTC under-performance pattern; the campaign appears to ROAS poorly because the bidding model has degraded signal.
Deduplication via event_id — the shared key that prevents double-counting
A single conversion event will fire to Meta twice — once from the client-side Meta Pixel running in the user's browser, and once from the server-side CAPI POST that Admaxxer's order webhook sends. The same conversion will fire to Google twice for the same reason. Without an explicit deduplication key, both networks would record the conversion as two separate events, inflating reported ROAS and corrupting the bidding model's training signal.
The deduplication key is the event_id field on both networks. Admaxxer sets event_id to the Shopify order number (or the equivalent transaction identifier on non-Shopify storefronts). The client-side pixel reads the order number from the Shopify checkout completion page and includes it in the pixel event; the server-side CAPI POST reads the same order number from the webhook payload and includes it in the CAPI event. The two events arrive at Meta with identical event_ids; Meta's deduplication logic recognizes the pair as a single conversion and counts it once.
- Pixel fires: event_id = "shopify-order-5839124", source = browser, with client-side cookies + browser fingerprint
- CAPI fires: event_id = "shopify-order-5839124", source = server, with hashed email + fbc + fbp + IP + UA
- Meta receives both, matches on event_id, counts the conversion exactly once, uses the union of the two payloads' user_data for match-rate scoring
The deduplication is asymmetric: Meta will count the conversion once even if one of the two events is missing, but the match rate scoring favors the pair. If only the pixel fires (CAPI is broken), Meta records the conversion but with weaker user_data; if only CAPI fires (pixel is blocked by an ad blocker), Meta records the conversion with stronger user_data because CAPI carries the hashed email. The dual-feed design therefore both prevents double-counting AND maximizes the per-conversion match rate.
Consent and privacy — CMP integration, LDU, and session-only fallback
Click ID capture and forwarding interact with the user's consent state in three distinct regulatory regimes — GDPR in Europe, CCPA/CPRA in California, and a growing list of state-level privacy laws in the US (Colorado, Virginia, Connecticut, Utah). The capture layer must respect the consent signal captured by your CMP (Consent Management Platform) — common implementations include Cookiebot, Termly, OneTrust, Osano, and Iubenda.
When the user has granted marketing-cookie consent, Admaxxer's pixel writes the click ID cookies with the standard 365-day TTL and forwards captured IDs via CAPI as documented in the prior sections. When consent is denied or pending, the pixel switches to session-only mode: click IDs are captured in an in-memory server-side store keyed on the request session, never written to a persistent cookie. The session-only store is dropped when the session expires (typically 30 minutes of inactivity). This satisfies the consent requirement while preserving session-scoped attribution for the user's current visit.
California's CCPA/CPRA opt-out is handled via Meta's Limited Data Use (LDU) flag and Google's user_data restricted_data_processing setting. When the CMP reports a CCPA opt-out ("Do Not Sell or Share My Personal Information" toggle), Admaxxer's CAPI payload includes LDU=true and Google's payload includes restricted_data_processing=true. Both networks honor the flag by limiting the data processing they perform on the event — the conversion still counts for attribution but is not used to seed look-alike audiences or to enrich the user's advertising profile.
GDPR enforcement is stricter — when consent is denied under GDPR, both Meta and Google require that the conversion not be sent at all (not LDU-limited; not sent). Admaxxer's CMP integration reads the GDPR consent state separately from the CCPA state and short-circuits the CAPI POST entirely when GDPR consent is denied. The order is still recorded in Admaxxer's internal pipe for first-party analytics (you can see the conversion in your dashboard), but it is not forwarded to the ad networks.
For the architectural background on Admaxxer's ITP mitigation and first-party cookie strategy, see Safari ITP Mitigation. For the canonical attribution-model reference, see Attribution Models.
Verifying capture — what a healthy match rate looks like
After the click ID capture layer is installed, the canonical verification surface is Admaxxer's /attribution/health page. It shows three rolling 24-hour parity numbers — pixel events received, CAPI events received, Shopify orders recorded — plus the match rate per network (Meta, Google, TikTok, Pinterest). A healthy steady-state DTC store sees the following:
- Pixel events vs Shopify orders: within 2% (pixel is slightly lower due to ad-blocker browsers)
- CAPI events vs Shopify orders: within 1% (CAPI is server-side and not blocked by browser-level blockers)
- Meta match rate (deduplicated pairs / total events): 95% or higher
- Google Enhanced Conversions match rate: 92% or higher (Google's reported match-rate calculation is stricter than Meta's)
- TikTok Events API match rate: 90% or higher
- Click ID capture rate per network: 65-85% for Meta + Google traffic, 95%+ for Microsoft + TikTok + Pinterest (the iOS 17 ATP strip affects Meta + Google harder)
Below those steady-state numbers, the page flags any 24-hour window where a metric drifted by more than 5 percent versus its 7-day rolling baseline. Typical drift causes: a CAPI access token expired and needs rotation; the CMP added a new consent gate that is denying the marketing-cookie category; the Shopify theme was modified and the pixel script tag was moved or deleted; a new ad platform was added but its CAPI forwarder was not configured.
For new-customer onboarding, Admaxxer runs a 30-minute parity test as part of the install: the operator places one test order, the system confirms the order appears in the pixel feed, the CAPI feed, and the Shopify webhook feed simultaneously, and the event_id deduplication is verified end-to-end. The test order can be refunded immediately afterward; the test report is captured in the /attribution/health audit log for post-install reference.
Cross-link: Tracking Health documents the parity check architecture in depth; Sources & Attribution documents the first-touch attribution logic that consumes the captured click IDs.
Frequently asked questions
- What is a click ID and why does it matter for ad attribution?
- A click ID is a unique identifier that an ad network attaches to a landing page URL when a user clicks an ad. gclid identifies a Google Ads click; fbclid identifies a Meta (Facebook/Instagram) Ads click; ttclid identifies a TikTok Ads click. The ad network uses the click ID to match a downstream conversion (purchase, signup) back to the specific click that drove it. Without the click ID, the ad network reports the conversion as 'organic' — your campaigns get under-credited and the bidding algorithm misallocates spend.
- Why do click IDs disappear if I just store them in localStorage?
- Three reasons. (1) Safari ITP caps localStorage to 7 days for third-party scripts. (2) iOS 17 Link Tracking Protection strips the click ID from the URL before the page loads in Mail/Messages — your JS never sees it. (3) Cross-device journeys: a user clicks an ad on mobile but converts on desktop; the mobile localStorage is irrelevant. Admaxxer's first-party pixel writes click IDs to a 365-day first-party cookie on your own domain + mirrors to a server-side session keyed on the visitor's pseudonymous ID. That survives all three.
- Does Admaxxer capture every click ID format or only Meta + Google?
- Every major format. Current list (2026-05): fbclid (Meta), gclid (Google Ads), dclid (Google Display), wbraid + gbraid (Google iOS app-to-web), gad_source (Google Performance Max), ttclid (TikTok), msclkid (Microsoft/Bing), li_fat_id (LinkedIn), epik (Pinterest), ScCid + sc_cid (Snapchat), twclid (X/Twitter), igshid (Instagram Stories), yclid (Yandex), s_kwcid (Adobe Search), ircid (Impact), q_at (RTBHouse). The list is maintained at server/lib/clickIds.ts; new formats ship with the next deploy when a new network launches.
- How does Admaxxer forward click IDs to Meta CAPI and Google Enhanced Conversions?
- On the server side. When an order completes, Admaxxer's order webhook handler reads the visitor's session, retrieves the captured click IDs from the first-party store, and posts them to (a) Meta CAPI in the user_data block (fbc + fbp fields per Meta's spec), (b) Google Enhanced Conversions for Leads via gclid + user-provided email/phone hash. Both posts include the order's transaction_id as event_id for deduplication with the client-side pixel.
- Is storing click IDs in a first-party cookie compliant with GDPR/CCPA?
- Yes when configured correctly. Click IDs themselves are pseudonymous — they identify a click, not a person. Admaxxer's pixel respects the consent state captured by your CMP (e.g., Cookiebot, Termly, OneTrust). When consent is denied, the click ID is captured in a session-only memory store and never written to a persistent cookie. When consent is granted, the 365-day first-party cookie applies. Admaxxer also supports Meta's Limited Data Use (LDU) flag for California residents — the CAPI payload is hashed with LDU=true when CCPA opt-out is detected.
Run this playbook in your own dashboard
Admaxxer ships the pixel + Meta CAPI + Google Enhanced Conversions + Maxxer AI agent + cohort analytics out of the box. The playbook above becomes a live surface in your account after a 5-minute setup.