FIRST-PARTY FORTRESS/MASTER IMPLEMENTATION GUIDE

Master
Implementation
Guide

Phase-by-phase technical setup. Follow in order — each phase depends on the previous one being complete.

PHASE 0

Tech Stack Prerequisites

Confirm the following before starting. Each phase assumes the previous one is in place.

COMPONENTREQUIREMENT
CMSSupports custom hidden fields and custom JavaScript on page load (WordPress + form plugins, Webflow, etc.)
Tag ManagementGoogle Tag Manager (Web Container) installed and loading without deferral
Consent ManagementCMP integrating with Consent Mode v2 (Complianz, Cookiebot, Usercentrics, etc.)
CRMSupports custom text fields + outbound webhooks on contact creation (OnePageCRM, HubSpot, Salesforce, etc.)
Automationn8n (cloud or self-hosted) or Make.com with Google Ads API access
Google AdsActive account with conversion tracking permissions. MCC access preferred.
PHASE 1

Consent Mode Setup

This must be the first tag that fires on every page — before GTAG, before the Conversion Linker, before everything.

1

In GTM, create an HTML tag that fires on Consent Initialization (built-in trigger).

2

Set consent defaults to denied for ad_storage, ad_user_data, ad_personalization, and analytics_storage for your target regions using the region array. Include a wait_for_update of 1000ms.

3

Enable url_passthrough: true in your GTAG / global tag settings. This appends gclid and other click IDs to internal link URLs, preserving them across page navigations even when cookies are blocked.

4

Enable ads_data_redaction: true. When ad_storage is denied, this redacts click IDs in pings while allowing modelled conversions.

5

Integrate your CMP to fire a consent update when the user accepts. The consent update tag must call gtag('consent', 'update', {...}) with the granted categories.

PHASE 2

Frontend Data Capture

2a · UTM STASH (sessionStorage, no consent required)

Create an HTML tag that fires on All Pages with no consent gate. Fires immediately on landing before consent is resolved, preserving click IDs across page navigations.

GTM · HTML Tag · All Pages · No consent gate
(function() {
  var KEYS = ['utm_source','utm_medium','utm_campaign','utm_term',
              'utm_content','gclid','wbraid','gbraid'];
  try {
    var qs = new URLSearchParams(location.search || '');
    var found = false, stash = {};
    KEYS.forEach(function(k) {
      var v = qs.get(k);
      if (v) { stash[k] = decodeURIComponent(v).trim(); found = true; }
    });
    if (found) sessionStorage.setItem('__utm_stash', JSON.stringify(stash));
  } catch(e) {}
})();

2b · COOKIE WRITER (first/last touch, requires ad_storage consent)

Create an HTML tag that fires when ad consent is granted. Writes ft_gclid / ft_utm_* (first-touch, 180-day, written once) and lt_gclid / lt_utm_* (last-touch, 90-day, always overwritten). These extend attribution beyond the current session and survive page reloads.

2c · HIDDEN FIELD SETTER (core engine, DOM Ready, no consent required)

Fires on DOM Ready on landing pages. Resolves gclid via priority chain, injects all values into hidden form fields, generates a UUID, and saves the lead context on form submit.

gclid priority chain: URL param → lt_gclid cookie → ft_gclid cookie → sessionStorage stash → _gcl_aw Conversion Linker cookie (parse: GCL.{timestamp}.{gclid} → take everything after the second dot)

1

Resolve gclid, wbraid, gbraid via priority chain

2

Resolve UTM params from URL or cookies/stash

3

Write all resolved values into hidden form fields via querySelector by field name

4

Generate a UUID (xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx format). Store in sessionStorage as fpf_order_id. Poll for the order_id hidden field (async form builders delay rendering) and write the UUID.

5

On form submit: save {email, order_id} to sessionStorage as __lead_context so the thank-you page GTM tags can read both values.

PHASE 3

GTM Web Configuration — Real-Time Signal

3a · CONVERSION LINKER TAG

Type: Conversion Linker. Settings: enableUrlPassthrough: true. Trigger: All Pages, consent NOT_NEEDED. This writes the _gcl_aw cookie — the consent-immune gclid fallback that Phase 2c relies on.

3b · TRANSACTION ID VARIABLE

Custom JavaScript variable. Reads the order_id from sessionStorage.

GTM · Custom JavaScript Variable · Transaction ID
function() {
  try {
    var ctx = JSON.parse(sessionStorage.getItem('__lead_context') || '{}');
    if (ctx.order_id) return ctx.order_id;
    return sessionStorage.getItem('fpf_order_id') || undefined;
  } catch(e) { return undefined; }
}

3c · USER EMAIL VARIABLE

GTM · Custom JavaScript Variable · User Email
function() {
  try {
    var ctx = JSON.parse(sessionStorage.getItem('__lead_context') || '{}');
    return ctx.email || undefined;
  } catch(e) { return undefined; }
}

3d · USER-PROVIDED DATA VARIABLE

Type: User-Provided Data (Enhanced Conversions). Mode: Manual. Email: {{User Email}}. The tag auto-hashes the email value.

3e · GOOGLE ADS CONVERSION TRACKING TAG (PIXEL)

Conversion IDYour AW-XXXXXXXXXX account ID
Conversion LabelLabel from your Secondary goal (website pixel)
Transaction ID / Order ID{{Transaction ID}} — the variable from 3b
TriggerThank-you page pageview (URL contains /thank-you)
User data (ECL for Web)Do NOT add here — use the User-Provided Data Event tag (3f) instead

3f · USER-PROVIDED DATA EVENT TAG

Type: Google Ads User-Provided Data Event. User Data Variable: {{User-Provided Data}}. Trigger: form submission event. Sends the hashed email to Google for browser-side Enhanced Conversions matching, independent of the conversion pixel.

3h · SERVER-SIDE GTM (OPTIONAL UPGRADE)

Prerequisites: server container on events.yourdomain.com via Stape.io or Google Cloud Run, valid SSL certificate, web container allowed list includes your server container ID.

1

Add a GA4 Client — receives GA4 events forwarded from the web container

2

Add a GA4 tag — re-fires to Google's servers from your server, extending FPID cookie lifetime to 2 years

3

Add a Conversion Linker (Server) tag — fires on all events

4

Add a Google Ads Conversion Tracking (Server) tag — Conversion ID + Label from Secondary goal, Transaction ID from transaction_id event parameter, blocking trigger on x-ga-gcs consent string pattern G10\d

The server-side Google Ads tag is still consent-gated. When a user declines ad consent, the browser forwards the consent signal to the server container and the blocking trigger suppresses the tag. Server GTM improves signal durability for consenting users — it does not bypass consent.

PHASE 4

CRM Configuration — The Vault

1

Create custom text fields: gclid_v2, gbraid, wbraid, gads_order_id. Confirm the email field is standard.

2

Map these to the hidden form fields from Phase 2c. Field names in the form must match field IDs in the CRM integration.

3

Configure a webhook to fire on Contact Created only — not on updates.

4

Verify the webhook payload includes the full contact record with all custom fields. Confirm the contact ID path — CRM vendors wrap payloads differently (e.g., OnePageCRM delivers the contact at body.data.contact, not body.contact).

PHASE 5

Google Ads Configuration

See the Google Ads Configuration Guide for the full goal setup, ctId extraction, and diagnostics protocol. Summary:

1

Create the Primary goal as an Import → UPLOAD_CLICKS action. Set as Primary for bidding.

2

Create the Secondary goal as a Website action. Set as Secondary (not for bidding). Enable Enhanced Conversions for Leads.

3

Note both ctId values from the URL bar. You will need them in Phase 6.

PHASE 6

Automation — n8n (or Make.com)

n8n SETUP (PREFERRED)

1

Create a new workflow.

2

Webhook node: Path matching your client name (e.g., client-oct-bridge). Set responseMode: onReceived — responds immediately to prevent CRM webhook timeout.

3

HTTP Request node (Get Contact): GET {crm_api_base}/contacts/{id}.json. Read contact ID from webhook payload — confirm the payload path (see Phase 4 note).

4

Code node (Extract Fields): Pull gclid, gbraid, wbraid, order_id, email, created_at from the contact record. Treat null, empty, and placeholder values (e.g., the literal string "google") as absent.

5

IF node (Has Google Click ID?): True if any of gclid, gbraid, wbraid is present. Routes to Path A (upload click conversion) or continues to Path B check.

Path A · uploadClickConversions (gclid present)
POST https://googleads.googleapis.com/v20/customers/{customer_id}:uploadClickConversions
{
  "conversions": [{
    "conversionAction": "customers/{customer_id}/conversionActions/{ctId_primary}",
    "conversionDateTime": "{created_at as YYYY-MM-DD HH:mm:ssZ}",
    "conversionValue": {value},
    "currencyCode": "EUR",
    "gclid": "{gclid}",
    "partialFailure": true
  }]
}
Path B · ECL fallback (no gclid, email present)
POST https://googleads.googleapis.com/v20/customers/{customer_id}:uploadClickConversions
{
  "conversions": [{
    "conversionAction": "customers/{customer_id}/conversionActions/{ctId_primary}",
    "conversionDateTime": "{created_at as YYYY-MM-DD HH:mm:ssZ}",
    "conversionValue": {value},
    "currencyCode": "EUR",
    "userIdentifiers": [{
      "hashedEmail": "{sha256_hashed_email}"
    }],
    "partialFailure": true
  }]
}
6

Error Trigger node: Points to the same workflow. On any unhandled error, fires a notification (Slack, email, etc.).

7

Set the Google Ads API credential with: developer token, MCC login-customer-id, client customer-id.

Email hashing in n8n Code node: crypto.createHash('sha256').update(email.toLowerCase().trim()).digest('hex')

MAKE.COM ALTERNATIVE

n8nMake.com
Trigger scopeContact Created only (explicit)Verify — default may include Updates
Error alertingBuilt-in error trigger node → SlackMust configure manually
Conversion valueSet explicitly in payloadVerify hardcoded value is correct
Email hashingExplicit Code node requiredNative module may hash automatically
InfrastructureSelf-hosted / cloud instance you controlThird-party dependency

Make.com: use an HTTP module for the API upload — the native "Upload a Conversion Enhancement" module targets a different endpoint. Use the raw HTTP module with the uploadClickConversions payload above.

PHASE 7

QA & Launch

1

Submit a test lead through the live form. Confirm in the CRM: gclid appears in the contact record, UUID appears as gads_order_id, email is present.

2

In your automation tool, confirm the webhook was received and the upload node returned a response. Even a UNPARSEABLE_GCLID error is acceptable for test gclids — it confirms the pipeline is wired correctly.

3

In Google Ads: Goals → Conversions → your Primary goal → Diagnostics. Wait 48–72 hours after the first real upload. You should see the upload status change to "Recording."

4

Verify the Slack (or equivalent) notification fired for the test lead.

5

Confirm GTM is loading on the thank-you page — open browser devtools, confirm no JS deferral is blocking GTM, confirm the pixel fired in the Network tab.

6

If using a server container: navigate to https://events.yourdomain.com/gtm.js?id=GTM-XXXXXXX in a private browser window. Must return HTTP 200 with JavaScript content and no certificate warnings.