Architecture
Blueprint
The four-phase mental model. Consent-aware frontend capture, real-time GTM signal, CRM vault, and a server-side API loop that bypasses the browser entirely.
SYSTEM OVERVIEW
Frontend Capture & Consent
Consent Mode denies all ad and analytics storage by default for EU/UK/CH visitors. Any system that depends solely on cookies granted after consent will miss 30–40% of conversions before the loop even starts. The frontend layer handles this in three parallel tracks.
TRACK A
UTM Stash
→Fires before consent is resolved
→Saves all UTM params + click IDs to sessionStorage
→No consent required — survives page reloads
TRACK B
Cookie Writer
→Fires when ad consent is granted
→Writes first-touch (180d) and last-touch (90d) cookies
→Extends attribution beyond the current session
TRACK C · CORE
Hidden Field Setter
→Fires on DOM Ready — no consent requirement
→Resolves gclid via priority chain: URL → cookies → sessionStorage → _gcl_aw
→Generates UUID, injects into hidden order_id field
→On form submit: saves email + UUID to sessionStorage
The _gcl_aw Conversion Linker cookie is consent-immune — written by GTM regardless of consent status. It is the last-resort gclid fallback in Track C.
Real-Time Signal — Web GTM
Google Tag Manager fires on the thank-you page, sending an immediate conversion signal to Google Ads via the browser. This registers the click-to-conversion path in Smart Bidding without waiting for the CRM loop.
Three tags fire: a Google Ads Conversion Tracking tag (UUID as Transaction ID), a User-Provided Data Event tag (hashed email for Enhanced Conversions), and a GA4 event routed to the server container.
⚠ These tags are consent-gated. In EU markets expect 60–70% of visitors to grant ad consent. The pixel signal is intentionally partial — Phase 4 provides the consent-independent layer.
⚠ JS deferral tools (WP Rocket, LiteSpeed Cache) wrap GTM in a deferred loader. On thank-you pages, users read the confirmation and close the tab — GTM never loads, the pixel never fires. Exclude GTM's domain from all deferral rules.
PHASE 2b · SERVER GTM (OPTIONAL UPGRADE)
A server container on events.yourdomain.comintercepts the GA4 event before it reaches Google's servers. This extends the first-party cookie lifetime and improves signal quality for consenting users. It does not make conversion tracking consent-independent.
The CRM Vault
The CRM is the secure first-party anchor — the source of truth the API loop reads from. Every new lead record must store gclid, the UUID, and email as structured fields, not just the form submission data.
| FIELD | SOURCE | PURPOSE |
|---|---|---|
| gclid | Hidden field (Track C) | Primary API upload identifier |
| gbraid / wbraid | Hidden field (Track C) | iOS campaign click IDs |
| gads_order_id | Hidden field (Track C) | UUID — deduplication key |
| Form field (standard) | ECL fallback — hashed in automation | |
| UTM params | Hidden fields (Track C) | Campaign attribution context |
Configure the CRM webhook to fire on Contact Created only — not on updates. Every update re-triggers the pipeline and risks uploading duplicate conversions to Google Ads.
The API Loop — n8n / Make.com
The only layer that is genuinely consent-independent. n8n or Make.com reads the CRM contact record and uploads a conversion directly to the Google Ads API — bypassing the browser and its consent state entirely.
PATH A · PRIMARY
gclid present
→uploadClickConversions endpoint
→Uses gclid (or gbraid / wbraid)
→Primary UPLOAD_CLICKS goal
→Immediate — 90%+ EU lead coverage
PATH B · FALLBACK
No gclid — email present
→Same endpoint — ECL path
→userIdentifiers: SHA-256 hashed email
→Probabilistic identity match
→25–45% match rate · 24–48h delay
PATH C · NO ATTRIBUTION
No gclid · no email
→Organic / direct traffic
→No Google click to attribute
→Log and notify — no upload
→Expected — not a tracking failure
n8n is preferred: Contact Created trigger is explicit, built-in error alerting, owned infrastructure. Make.com works — verify trigger scope is Contact Created only, check the hardcoded conversion value, and add a manual error notification branch.
Conversion Action Hierarchy
The most commonly misconfigured element. Two conversion actions, two distinct roles. If the website pixel is set as Primary, Smart Bidding trains on a systematically biased subset of your leads.
| ACTION | SOURCE | GOAL TYPE | REACH (EU) |
|---|---|---|---|
Primary — CRM Feed Import → UPLOAD_CLICKS | n8n / Make.com via Google Ads API | PRIMARY · BIDDING | 90%+ |
Secondary — Web Pixel Website (GTM tag) | Google Tag Manager — thank-you page | SECONDARY · DIAGNOSTIC | ~65% |
Key Failure Points
JS deferral tools (WP Rocket, LiteSpeed, etc.)
Performance plugins that defer JavaScript prevent GTM from loading on thank-you pages where users close the tab immediately. GTM's domain and your server container domain must be excluded from all deferral rules.
Server container SSL
The server container runs on a custom subdomain. If the SSL certificate expires, GTM tags fail silently. Verify: GET https://events.yourdomain.com/gtm.js must return HTTP 200. Check after every infrastructure change.
CRM webhook trigger scope
"Contact Updated" re-fires on every CRM edit. Scope the webhook to "Contact Created" only — every update-triggered upload is a potential duplicate conversion in Google Ads.
UPLOAD_CLICKS ≠ Enhancement
The UPLOAD_CLICKS endpoint (uploadClickConversions) uploads a new conversion using a gclid. The RESTATEMENT adjustment endpoint modifies an existing conversion's value. The primary API loop always uses uploadClickConversions.
Server-side GTM is still consent-gated for ads
The server container's Google Ads tag reads the consent signal forwarded from the browser. If the user declined ads consent, the server-side pixel does not fire either. Server GTM is an accuracy upgrade, not a consent bypass.
Double-counting
Do not fire an additional server-side conversion tag from the same web event that already fires a browser-side pixel for the same conversion action. Each conversion should be recorded by exactly one path.
Google Ads API version sunset
The API version in the upload URL (/v20/) is periodically deprecated. When Google sunsets a version, all uploads silently fail. Monitor the deprecation schedule and update the endpoint URL before the cutoff.