Google AdsGuide

Google Ads Attribution Setup Guide

End-to-end Google Ads measurement: gtag, Enhanced Conversions for Web and Leads, Consent Mode v2, offline imports, gclid/gbraid/wbraid, sGTM, and Customer Match.

May 4, 202630 min read

The Google Attribution Stack in 2026

Google Ads measurement is no longer "drop a conversion pixel and you're done." Cookies are restricted, iOS strips identifiers, GA4 replaced Universal Analytics in July 2024, and Consent Mode v2 has been enforced in the EEA since 2024-03-06. The accurate setup is a stack — each layer covers a different gap.

LayerPurposeWhat it solves
Google tag (gtag.js)Single page tag for Google Ads + GA4 + FloodlightReplaces the legacy gtag('config', ...) per-product loaders. One tag, multiple destinations.
Conversion linkerCaptures gclid/gbraid/wbraid from URL into a first-party cookie (_gcl_aw)Multi-page funnels — the click ID is on the landing page, the conversion is 3 pages later.
Enhanced Conversions for WebSends hashed first-party PII alongside the conversionITP/cookie loss, cross-device, signed-in users. ~5–10% lift in modeled conversions is typical.
Enhanced Conversions for LeadsOffline closed-loop: form fill on site -> CRM close -> uploaded with same hashed identifiersB2B / lead-gen where conversion happens days/weeks after the click.
Consent Mode v2Sends consent state to Google so it can model unconsented conversionsRequired to keep ad personalization & remarketing working in the EEA, UK, and Switzerland.
Offline conversion imports (gclid)Upload gclid + conversion within 90 daysPhone calls, sales-assisted closes, anywhere a gclid is the only signal you have.
GA4 -> Google Ads conversion importMirror GA4 key events into Google AdsWhen you've already standardized measurement on GA4 and want bidding to use the same definitions.
Customer MatchHashed first-party audience lists for targeting / suppressionRetention, suppression of existing customers, lookalike-style seed lists.
Server-side Google Tag (sGTM)First-party endpoint that proxies to Google + other vendorsAd-blocker / ITP resilience, payload control, PII redaction before it leaves your domain.

Rule of thumb: Google tag + Conversion linker + Enhanced Conversions + Consent Mode v2 is the modern baseline. Everything else is additive.

Click ID Landscape: gclid, gbraid, wbraid

Google emits one of three click identifiers depending on platform and ad surface. Capturing all three is required — older code that only handles gclid silently drops iOS attribution.

ParamWhen it firesSurfaceNotes
gclidClick on a Google ad on web (non-iOS, or iOS with ATT consent)Search, Display, YouTube, Shopping webDeterministic, one ID per click. Retained by Google for 90 days for offline imports.
gbraidiOS user clicks a web ad that lands on an iOS appApp campaigns, iOS app installsPrivacy-preserving, aggregated (multiple users may share an ID). Introduced after iOS 14.5 (Apr 2021).
wbraidiOS user clicks inside an iOS app and lands on a websiteIn-app placements -> web destinationSame aggregated/privacy model as gbraid.

Why three identifiers exist

iOS 14.5 introduced App Tracking Transparency (ATT). Without ATT consent, Google cannot send gclid (which can identify a single user) on iOS surfaces. gbraid and wbraid are aggregated across users so they survive ATT — at the cost of some attribution granularity.

Auto-tagging: the prerequisite

gclid/gbraid/wbraid are only appended when Auto-tagging is enabled at the Google Ads account level. With auto-tagging off, Google falls back to UTM-only tracking and Enhanced Conversions / GCLID-based offline imports stop working.

In Google Ads -> Admin -> Account settings -> Auto-tagging -> Tag the URL that people click through from my ad. Default for new accounts is on; legacy accounts may have it off.

If your final URL has its own query string (e.g. ?utm_source=google), Google appends &gclid=.... If you're using server-side redirects that strip query params, the click ID is lost before it reaches the landing page. Test with a real ad click, not a manual URL.

Capturing click IDs on the landing page

Capture all three. Even on a non-iOS site you'll occasionally see wbraid from in-app YouTube traffic.

// Run on every landing page, before any conversion logic
function captureGoogleClickIds() {
  const url = new URLSearchParams(window.location.search);
  const ids = {
    gclid:  url.get('gclid'),
    gbraid: url.get('gbraid'),
    wbraid: url.get('wbraid'),
  };

  for (const [key, value] of Object.entries(ids)) {
    if (value) {
      // 90-day cookie, lined up with Google's retention window
      const expires = new Date(Date.now() + 90 * 24 * 60 * 60 * 1000).toUTCString();
      document.cookie = `_orbit_${key}=${encodeURIComponent(value)}; expires=${expires}; path=/; SameSite=Lax; Secure`;
      // Also keep in localStorage for SPA navigation
      try { localStorage.setItem(`_orbit_${key}`, value); } catch {}
    }
  }
}
captureGoogleClickIds();

If you have the Conversion linker tag installed (GTM) or the Google tag (gtag.js) loaded with conversion_linker: true (default), Google does this for you and stores the value in a first-party cookie called _gcl_aw. You only need the manual capture above when you intend to send the click ID server-side from your own backend.

Official ref: Conversion Linker tag. The _gcl_aw cookie format is GCL.{timestamp}.{click_id} — parse it server-side if you're sending click IDs to the Ads API yourself.

Google Tag Setup

There are two install paths. Pick one — running both creates duplicate hits and breaks deduplication.

Path A — Direct gtag.js install

Add to <head>, as high as possible, on every page. Replace AW-XXXXXXXXXX with your Google Ads conversion ID (Tools -> Conversions -> any conversion action -> "Tag setup -> Install the tag yourself").

<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=AW-XXXXXXXXXX"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());

  // Default consent state — required for Consent Mode v2.
  // See "Consent Mode v2" section below for the full configuration.
  gtag('consent', 'default', {
    ad_storage: 'denied',
    ad_user_data: 'denied',
    ad_personalization: 'denied',
    analytics_storage: 'denied',
    wait_for_update: 500
  });

  gtag('config', 'AW-XXXXXXXXXX', {
    // conversion_linker: true is the default — leaving this on
    // is what populates the _gcl_aw first-party cookie from gclid/gbraid/wbraid.
    conversion_linker: true,
    // Optional: bind a stable user_id for cross-device matching
    user_id: window.__currentUserId
  });

  // If you also use GA4, add the same script with G-XXXXXXX — one snippet, multiple IDs.
</script>

Fire a conversion event:

gtag('event', 'conversion', {
  send_to: 'AW-XXXXXXXXXX/AbC-D_efG-h12_34-XYZ',  // conversion label from Ads UI
  value: 49.99,
  currency: 'USD',
  transaction_id: 'order_5193'  // dedup key — required if you also import the same order via API
});

Path B — Google Tag Manager (web container)

For most agency clients GTM is easier to maintain. Setup:

  1. Install the GTM container snippet (<script> in <head>, <noscript> after <body>) on every page.
  2. Add a Google Tag (the unified tag, not the legacy "Google Ads Conversion Tracking" tag) configured with your AW-XXXXXXXXXX ID. Trigger: All Pages.
  3. Add a Conversion Linker tag. Trigger: All Pages. This is what enables _gcl_aw cookie writes; without it, multi-page funnels lose attribution.
  4. Add a Google Ads Conversion Tracking tag for each conversion action, with the conversion label. Trigger on the relevant funnel event (e.g. a purchase confirmation page or a custom dataLayer event).
// Push from your purchase confirmation page
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
  event: 'purchase',
  ecommerce: {
    transaction_id: 'order_5193',
    value: 49.99,
    currency: 'USD'
  },
  user_data: {
    email: 'customer@example.com',  // raw — GTM will hash for Enhanced Conversions
    phone: '+12125551234'
  }
});
If you migrated from a pre-2023 GTM setup, audit for the legacy "Google Ads" account-level tag. Running both the unified Google Tag and the legacy conversion tag on the same page will produce duplicate gtag/js loads and inflate metrics.

Conversion Actions: Settings That Actually Matter

When you create a conversion action in Google Ads (Tools -> Conversions -> + New conversion action) the defaults are not always right.

SettingWhat it doesWhat to actually pick
Goal & action typeBucket for reporting + Smart BiddingMatch the funnel stage. Don't lump all leads under "Submit lead form".
Primary vs. SecondaryPrimary actions feed Smart Bidding and the "Conversions" column. Secondary actions are observation-only.One primary per Smart Bidding goal. Move soft conversions (newsletter, scroll depth) to secondary.
Value"Use the same value for each", "Use different values", or "Don't use a value"For lead-gen B2B, set an estimated lead value. Performance Max + Maximize Conversion Value won't bid intelligently without one.
Count"Every" (every conversion counts — best for ecom) or "One" (best for lead gen — one signup per click max)Ecom: Every. Lead-gen / SaaS trials: One.
Click-through windowHow long after a click a conversion can be attributedDefault 30 days. Long sales cycles: 90 days.
View-through windowSame, for impressionsDefault 1 day. Don't extend without a reason — view-through is noisy.
Attribution modelLast-click or Data-driven (DDA)DDA is the default for new actions since June 2023. Last-click only for compliance/baselining.
Include in "Conversions"Toggles whether this action feeds Smart BiddingOff for any soft/observational action.
Google sunset First Click, Linear, Time Decay, and Position-Based attribution models in October 2023. The only models you can pick today are Data-driven and Last click. If you're reading older docs that reference the others, they no longer apply.

Enhanced Conversions for Web

Enhanced Conversions sends SHA-256 hashed first-party data alongside each conversion event. Google matches the hash against signed-in Google accounts to recover conversions lost to cookie deletion, ITP, or cross-device journeys.

Three setup methods

MethodBest forEffort
Google tag — auto-detectSites where the conversion page renders the email/phone in the DOM (order confirmation pages, "Thanks Jane" headers)Low — Google scrapes the DOM. Validate matches in the diagnostics report.
Google tag — manual JSSPAs, sites where PII isn't rendered, or you want explicit control over what gets sentMedium — call gtag('set', 'user_data', ...) before the conversion event
Google Ads APIServer-side: send hashed identifiers from your backend with the gclid (or with no gclid for closed-loop CRM)Highest — full control, survives ad blockers

Required user fields

At minimum send one of: email, OR full address (first name + last name + postal code + country). More fields = higher match rate.

FieldHashed?Normalization rules
sha256_email_addressYesLowercase + trim whitespace before hashing
sha256_phone_numberYesE.164 format (+12125551234) before hashing
sha256_first_nameYesLowercase + trim, strip non-letter chars except hyphens
sha256_last_nameYesSame as first name
sha256_streetYesLowercase + trim, expand abbreviations (st -> street)
postal_codeNoSend raw
countryNoTwo-letter ISO (US, GB) — raw

Manual JS configuration (Google Tag)

Call gtag('set', 'user_data', ...) before the conversion event fires:

async function sha256(value) {
  const buf = new TextEncoder().encode(value);
  const hashBuf = await crypto.subtle.digest('SHA-256', buf);
  return Array.from(new Uint8Array(hashBuf))
    .map(b => b.toString(16).padStart(2, '0')).join('');
}

function normalizeEmail(e)  { return e.trim().toLowerCase(); }
function normalizePhone(p)  { return p.replace(/[^\d+]/g, ''); }  // assume already E.164
function normalizeName(n)   { return n.trim().toLowerCase().replace(/[^a-z\-]/g, ''); }

async function setEnhancedConversionsUserData({ email, phone, firstName, lastName, postalCode, country }) {
  const userData = {};
  if (email)     userData.sha256_email_address = await sha256(normalizeEmail(email));
  if (phone)     userData.sha256_phone_number  = await sha256(normalizePhone(phone));
  if (firstName) userData.address = userData.address || {};
  if (firstName) userData.address.sha256_first_name = await sha256(normalizeName(firstName));
  if (lastName)  userData.address.sha256_last_name  = await sha256(normalizeName(lastName));
  if (postalCode) userData.address = { ...userData.address, postal_code: postalCode };
  if (country)    userData.address = { ...userData.address, country };

  gtag('set', 'user_data', userData);
}

// On purchase confirmation:
await setEnhancedConversionsUserData({
  email: 'jane@example.com',
  phone: '+12125551234',
  firstName: 'Jane',
  lastName: 'Doe',
  postalCode: '10001',
  country: 'US'
});

gtag('event', 'conversion', {
  send_to: 'AW-XXXXXXXXXX/AbC-D_efG-h12_34-XYZ',
  value: 49.99,
  currency: 'USD',
  transaction_id: 'order_5193'
});

For high-trust, ad-blocker-resilient setup, upload from your backend. Use the google-ads-api Node client or raw REST.

// Node 20+ / google-ads-api v17+
import { GoogleAdsApi } from 'google-ads-api';
import crypto from 'node:crypto';

const client = new GoogleAdsApi({
  client_id: process.env.GOOGLE_ADS_CLIENT_ID,
  client_secret: process.env.GOOGLE_ADS_CLIENT_SECRET,
  developer_token: process.env.GOOGLE_ADS_DEVELOPER_TOKEN,
});

const customer = client.Customer({
  customer_id: process.env.GOOGLE_ADS_CUSTOMER_ID,  // 10 digits, no dashes
  login_customer_id: process.env.GOOGLE_ADS_MCC_ID, // if accessing via MCC
  refresh_token: process.env.GOOGLE_ADS_REFRESH_TOKEN,
});

const sha256 = (v) => crypto.createHash('sha256').update(v).digest('hex');

async function uploadEnhancedConversion({ gclid, orderId, value, currency, email, phone }) {
  const userIdentifiers = [];
  if (email) userIdentifiers.push({ hashed_email: sha256(email.trim().toLowerCase()) });
  if (phone) userIdentifiers.push({ hashed_phone_number: sha256(phone.replace(/[^\d+]/g, '')) });
  // up to 5 user_identifiers per conversion

  const conversion = {
    conversion_action: `customers/${process.env.GOOGLE_ADS_CUSTOMER_ID}/conversionActions/123456789`,
    conversion_date_time: new Date().toISOString().replace('T', ' ').slice(0, 19) + '+00:00',
    conversion_value: value,
    currency_code: currency,
    order_id: orderId,                 // dedup key — same as gtag transaction_id
    gclid,                             // if you have it; recommended even with user_identifiers
    user_identifiers: userIdentifiers,
    consent: {
      ad_user_data: 'GRANTED',         // pass through actual consent state
      ad_personalization: 'GRANTED'
    }
  };

  return customer.conversionUploads.uploadClickConversions({
    customer_id: process.env.GOOGLE_ADS_CUSTOMER_ID,
    conversions: [conversion],
    partial_failure: true
  });
}
If you send the same conversion via both gtag (browser) and the API (server), set the same transaction_id / order_id on both. Without that key Google double-counts. This is the single most common Enhanced Conversions misconfiguration we see.

Enhanced Conversions for Leads

Different feature, similar name. EC for Leads is the closed-loop offline flow: a user fills a form on your site, you receive their (hashed) identifiers via the Google tag, and weeks later when the lead converts in your CRM you upload the conversion with the same hashed identifiers. Google matches it back to the original click.

Flow

[1] User clicks ad -> lands on site (gclid captured)
        |
[2] User submits lead form
        - Google tag fires lead_submit event with email/phone (hashed)
        - Same identifiers stored in your CRM with the lead record
        |
[3] (days/weeks pass)
        |
[4] Lead closes in CRM (deal won)
        - Your backend calls Google Ads API with hashed email/phone + conversion value
        - Google matches the hash to the original gclid -> attributes conversion

Key differences from EC for Web

EC for WebEC for Leads
TriggerOnline conversion (purchase, signup)Offline conversion (deal won, qualified lead)
Conversion action typeWEBPAGEUPLOAD_CLICKS
gclid required?No (auto from cookie)No (matched via user_identifiers) but recommended if you have it
Time-to-conversionReal-timeUp to 90 days post-click
Send methodgtag / GTM / APIAPI only (or Google Sheets / Salesforce / HubSpot connector)

Configure the Google tag for EC for Leads

In Google Ads -> Tools -> Conversions -> diagnostics for the lead conversion action, enable Turn on Enhanced Conversions for Leads and choose either Google tag or GTM. The tag will start collecting hashed form-field values. Then you upload the closed deal:

// CRM webhook handler (Node) — fires when deal moves to "won"
import { GoogleAdsApi } from 'google-ads-api';
import crypto from 'node:crypto';

const sha256 = (v) => crypto.createHash('sha256').update(v).digest('hex');

async function uploadLeadConversion({ leadEmail, leadPhone, dealValue, dealClosedAt, conversionActionId }) {
  const customer = client.Customer({ /* ... as above ... */ });

  const conversion = {
    conversion_action: `customers/${process.env.GOOGLE_ADS_CUSTOMER_ID}/conversionActions/${conversionActionId}`,
    conversion_date_time: dealClosedAt,        // 'YYYY-MM-DD HH:MM:SS+00:00'
    conversion_value: dealValue,
    currency_code: 'USD',
    user_identifiers: [
      { hashed_email: sha256(leadEmail.trim().toLowerCase()) },
      { hashed_phone_number: sha256(leadPhone.replace(/[^\d+]/g, '')) }
    ],
    consent: { ad_user_data: 'GRANTED', ad_personalization: 'GRANTED' }
  };

  return customer.conversionUploads.uploadClickConversions({
    customer_id: process.env.GOOGLE_ADS_CUSTOMER_ID,
    conversions: [conversion],
    partial_failure: true
  });
}
The conversion action's type must be UPLOAD_CLICKS for EC for Leads. If you accidentally upload to a WEBPAGE conversion action, the API returns INVALID_CONVERSION_ACTION_TYPE and the conversion is silently dropped on partial-failure mode.

Mandatory in the EEA, UK, and Switzerland since 2024-03-06 if you want ad personalization, remarketing, or modeled conversions to keep working. v2 added two parameters on top of v1's ad_storage / analytics_storage:

ParamControls
ad_storageCookies/local storage for advertising
analytics_storageCookies/local storage for analytics
ad_user_data(v2) Whether user data may be sent to Google for advertising
ad_personalization(v2) Whether the user may receive personalized ads / remarketing

Basic vs. Advanced

ModeBehavior on consent deniedModeling quality
BasicGoogle tags don't load at all until consent. No pings sent.Lower — Google has no signal to model from.
AdvancedGoogle tags load before consent. On denied, send cookieless pings (no identifiers, just event).Higher — Google can model conversions for unconsented users.

Advanced is the recommended setup. It requires a Google-certified CMP (OneTrust, Cookiebot, CookieHub, Usercentrics, etc.) — DIY consent banners that just toggle gtag on/off don't qualify.

Wiring it up (gtag, Advanced mode)

<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}

  // 1. Default — DENIED for everything before the user has chosen
  gtag('consent', 'default', {
    ad_storage: 'denied',
    ad_user_data: 'denied',
    ad_personalization: 'denied',
    analytics_storage: 'denied',
    wait_for_update: 500,                  // ms — gives the CMP time to update before pings fire
    region: ['BE','BG','CZ','DK','DE','EE','IE','GR','ES','FR','HR','IT','CY','LV','LT',
             'LU','HU','MT','NL','AT','PL','PT','RO','SI','SK','FI','SE','IS','LI','NO','GB','CH']
    // Outside EEA/UK/CH you can default to 'granted' — not legally required.
  });

  gtag('js', new Date());
  gtag('config', 'AW-XXXXXXXXXX');
</script>

<!-- Later, when the CMP receives consent: -->
<script>
  // Called by your CMP's onAccept handler
  function onConsentAccepted() {
    gtag('consent', 'update', {
      ad_storage: 'granted',
      ad_user_data: 'granted',
      ad_personalization: 'granted',
      analytics_storage: 'granted'
    });
  }
</script>
Google began enforcing the EU User Consent Policy aggressively in July 2025. Advertisers in EEA traffic without correctly-passing v2 signals had ad personalization features (remarketing, optimized targeting, Customer Match) disabled at the account level. Check your account-level diagnostics in Google Ads -> Admin -> "EU user consent policy" status.

When uploading conversions via the Google Ads API, always include the consent object — Google needs to know whether to use the conversion for personalization vs. measurement-only.

{
  // ... rest of conversion
  consent: {
    ad_user_data:       userConsented ? 'GRANTED' : 'DENIED',
    ad_personalization: userConsented ? 'GRANTED' : 'DENIED'
  }
}

Offline Conversion Imports

Three import paths. Use the right one for your stack.

MethodConversion action typeWhen to useGotchas
gclid uploadUPLOAD_CLICKSYou captured gclid at the form submit and want to import the closed dealClick ID retained for 90 days only
Enhanced Conversions for LeadsUPLOAD_CLICKSYou have hashed email/phone but no gclid (or both)Same 90-day window; preferred over raw gclid since 2024
Google Sheets / SFTP / Data ManagerEitherNo-code, low-volume, manualLookback only 90 days; less granular error reporting

Required CSV schema for gclid upload

If you're using the legacy CSV uploader (Tools -> Conversions -> Uploads -> + New upload), the file needs these columns at minimum:

ColumnExample
ParametersTimeZone=America/New_York (first row only)
Google Click IDEAIaIQobChMI...
Conversion NameSales-Closed (must match the Conversion action name exactly)
Conversion Time2026-04-15 14:32:01
Conversion Value499
Conversion CurrencyUSD
After creating a new offline conversion action, wait 4–6 hours before uploading. Uploads in the first 4–6 hours can take up to 2 days to surface in reports. Yes, this trips up every first-time integrator.

The 90-day rule

Google retains gclid for 90 days. Conversions uploaded more than 90 days after the click are silently dropped — they don't error, they just don't show up. If your sales cycle is longer than 90 days, EC for Leads with hashed identifiers (no gclid required) is the only path that works.

GA4 → Google Ads Conversion Import

When to use this path: you already have GA4 instrumented with key events (purchase, generate_lead, sign_up) and don't want to maintain a parallel Google Ads conversion stack. Bidding will work off the same events your analytics team already trusts.

Setup

  1. Link the accounts. GA4 -> Admin -> Product Links -> Google Ads Links -> Link. Pick the Google Ads account, enable Personalized advertising if you want audiences too. Auto-tagging must be on in Google Ads.
  2. Mark the GA4 event as a key event. GA4 -> Admin -> Events -> toggle "Mark as key event" on the event you care about.
  3. Import in Google Ads. Tools -> Conversions -> + New conversion action -> Import -> Google Analytics 4 properties -> select your event(s).
  4. Wait up to 24h for conversion data to populate.

When to use vs. native conversion tracking

Use GA4-imported conversions when...Use native Google Ads conversions when...
GA4 is your source of truth for measurementYou need real-time bidding feedback (GA4 import has up to 24h lag)
You want consistency between Ads & Analytics reportsYou're using Enhanced Conversions for Web/Leads
Multiple ad platforms import from GA4 alreadyThe conversion is offline (CRM, phone calls)
Don't run both native gtag conversions and GA4-imported conversions for the same event without picking one as Primary. Smart Bidding double-optimizes and your CPA reports become noise.
Official refs: Create Google Ads conversions based on Google Analytics key events · Universal Analytics fully sunset 2024-07-01 — GA4 is the only Google analytics source for Ads now.

Customer Match

Hashed first-party audiences for targeting, suppression, and lookalikes. Useful for retention campaigns, suppressing existing customers from acquisition, and seeding optimized targeting in Performance Max.

Supported identifier types

IdentifierHashingFormat
EmailSHA-256Lowercase + trim, then hash
PhoneSHA-256E.164 (+12125551234), then hash
Mailing addressSHA-256First name, last name, postal code, country — hash names; raw postal/country
Mobile device ID (IDFA / GAID)None — send rawiOS IDFA (UUID) or Android GAID
User-provided ID (CRM ID)SHA-256Your internal user identifier

List size minimums

Google reduced minimum audience size to 100 active users across Search, Display, and YouTube networks for all segment types (down from 1000 historically). You still typically need a few thousand for meaningful reach.

Upload via API

// Excerpt — google-ads-api OfflineUserDataJobService
const job = await customer.offlineUserDataJobs.create({
  type: 'CUSTOMER_MATCH_USER_LIST',
  customer_match_user_list_metadata: {
    user_list: `customers/${customerId}/userLists/${userListId}`,
    consent: { ad_user_data: 'GRANTED', ad_personalization: 'GRANTED' }
  }
});

await customer.offlineUserDataJobs.addOperations({
  resource_name: job.resource_name,
  operations: customers.map(c => ({
    create: {
      user_identifiers: [
        { hashed_email: sha256(c.email.trim().toLowerCase()) },
        c.phone ? { hashed_phone_number: sha256(c.phone) } : null
      ].filter(Boolean)
    }
  }))
});

await customer.offlineUserDataJobs.run({ resource_name: job.resource_name });
Starting April 1, 2026, OfflineUserDataJobService and UserDataService Customer Match uploads will fail for developer tokens that haven't sent a Customer Match request in the previous 180 days. If your developer token is dormant, run a small test upload before this date to keep access. New integrations should plan to migrate to the Data Manager API as Google rolls it out.

Server-side Google Tag (sGTM)

sGTM gives you a tagging endpoint on your own domain (e.g. metrics.yoursite.com) that proxies events to Google Ads, GA4, and other vendors. Why it matters in 2026:

  • First-party context: cookies set by sGTM are first-party, far less likely to be capped at 7 days by ITP.
  • Resilience: ad blockers don't have a generic blocklist for your metrics.yoursite.com endpoint the way they do for googletagmanager.com.
  • Payload control: you can redact PII before it hits Google, attach server-side data (LTV, plan tier), and mirror the same event to multiple destinations.

Reference architecture (Cloud Run)

Browser                                Your domain                    Google
   |                                       |                             |
   | --(POST /g/collect)------------------>|                             |
   |                                  [sGTM container                    |
   |                                   on Cloud Run]                     |
   |                                       |---Google Ads conversion --->|
   |                                       |---GA4 measurement protocol->|
   |                                       |---Meta CAPI / TikTok Events |

Setup outline

  1. Create a server container in GTM (tagmanager.google.com -> Admin -> + New Container -> Server).
  2. Deploy on Cloud Run. GTM provides a one-click deploy script — pick a region close to your users, set min instances to ≥3 for production (cold starts add latency).
  3. Map a custom domain. In Cloud Run, add a custom domain mapping for metrics.yoursite.com (or a subpath on your main domain). Point a DNS A/AAAA record at the load balancer the integration provisions. Wait for the SSL cert to provision.
  4. Configure GTM. Server container -> Admin -> Container Settings -> add your server URL.
  5. Point your web container at sGTM. In the web container's Google Tag config -> Configuration settings -> set transport_url to https://metrics.yoursite.com.
  6. Add the Google Ads Conversion Tracking tag in the server container.
// Web container: route Google tag through sGTM
gtag('config', 'AW-XXXXXXXXXX', {
  transport_url: 'https://metrics.yoursite.com',
  first_party_collection: true
});
A subdomain works (metrics.yoursite.com) but a same-origin path (yoursite.com/metrics) gives you slightly more durable cookies. The trade-off: same-origin requires routing through your edge / load balancer, which most teams find more operationally fragile than a subdomain CNAME.

Attribution Models in 2026

What you can pick today:

ModelWhen to use
Data-driven attribution (DDA)Default for new conversion actions since June 2023. Uses ML on your conversion path data to credit each touch fractionally. Required to use most effectively for Performance Max.
Last clickLegal/baseline reporting only, or when you're seeding a brand-new account with too little data for DDA.

The other four (First click, Linear, Time decay, Position-based) were sunset in October 2023 and converted to DDA automatically.

What "data-driven" actually means

Google trains a model per conversion action on your account's last 30 days of conversion paths. It compares the paths of converters vs. non-converters and assigns fractional credit to each interaction (search click, display impression, YouTube engaged-view) based on uplift. Floor for eligibility used to be 3,000 ad interactions and 300 conversions per 30 days; that bar was effectively removed in 2023, making DDA the default for almost every active conversion action.

Performance Max attribution

PMax campaigns appear in attribution reports the same as other campaign types. Credit is distributed across the channels (Search, Display, YouTube, Shopping, Gmail, Discover) according to the attribution model on the conversion action. DDA is strongly recommended for PMax — last-click systematically under-credits Display/YouTube touches, which then bleeds into Smart Bidding signal quality.

When NOT to trust Google's reported conversions

  • Heavy view-through windows on Display/YouTube — view-through conversions in those channels are noisy. Cross-check with GA4 last-touch.
  • Performance Max in mature accounts — PMax sometimes claims credit for branded search that would have converted anyway. Run an incrementality test (geo holdout) at least once a year.
  • Over-modeled conversions in EEA — if your modeled conversion rate exceeds ~30% of total, your Consent Mode setup is likely under-collecting actual events. Audit before celebrating.

AI Max for Search Campaigns: Conversion Handling

AI Max for Search reached general availability April 15, 2026. It's an optimization layer on existing Search campaigns (search term matching, text customization, final URL expansion) — not a new campaign type. It uses the conversion actions already attached to the campaign.

Two attribution-relevant gotchas:

  1. Final URL expansion + tracking templates. AI Max can expand to URLs different from your final URL. If you have a custom tracking template ({lpurl}?utm_source=google&utm_campaign={campaignid}), the expanded URL needs to be reachable too. Mismatched templates cause 404s on the expanded landing pages and drop conversions.
  2. Conversion accuracy is gating. AI Max bidding optimizes against the conversion actions you've marked Primary. Garbage in, garbage out: dedupe, value-track, and verify in Tag Assistant before turning AI Max on for high-spend campaigns.

Existing campaigns using Dynamic Search Ads (DSA), automatically created assets (ACA), or campaign-level broad match are being automatically upgraded to AI Max through September 2026. Audit conversion settings before the upgrade window for those campaigns.

Cross-Device & Cross-Account Tracking

FeatureWhat it doesSetup
Cross-device conversionsAttributes a desktop conversion to a mobile click (or vice versa) when the user is signed into Google on bothAutomatic — included in the "Conversions" column. Requires sufficient signed-in user volume in your account.
Conversion linker (first-party cookies)Stores gclid/gbraid/wbraid in _gcl_aw so the click ID survives multi-page navigationgtag.js: on by default. GTM: add the Conversion Linker tag.
Parallel trackingSends users directly from ad to final URL, runs click tracking in backgroundRequired for Search, Shopping, Display, Video, and Performance Max since 2018. Your tracking template must support 200ms response.
Parallel tracking is mandatory. If your tracking template lives on a slow third-party redirector that takes >200ms, Google will silently drop the click measurement (the user still arrives, but you lose the gclid). Test with the Parallel Tracking checker before going live.

Verification & Debugging

Tools

ToolUse for
Google Tag Assistant (tagassistant.google.com)Live page-by-page inspection. Shows which tags fired, payloads, errors.
GTM Preview/DebugStep through a session and see tag firing order, variable values, trigger evaluations.
Tag Coverage report (GTM)Site-wide check for pages where Google tag failed to fire.
Tag Diagnostics (GTM)Auto-detected issues by severity. Catches missing user_data fields, malformed hashes, etc.
EC for Web Diagnostics (Google Ads)Per-conversion-action match-rate report and field-level errors for Enhanced Conversions.
Browser DevTools -> NetworkFilter by google to see /g/collect, /pagead/conversion, etc. — last resort but always available.

Common failure modes

SymptomLikely causeFix
Conversions show but match rate <30%Wrong field hashing (e.g. trailing whitespace, non-lowercase email)Re-verify normalization. Test one record manually with printf "user@example.com" | sha256sum.
EC for Leads conversions never appearConversion action type is WEBPAGE, not UPLOAD_CLICKSCreate a new UPLOAD_CLICKS conversion action.
gclid not captured server-sideConversion linker disabled, or tracking template strips itRe-enable, or read from _gcl_aw cookie which is set by the linker.
Modeled conversions = 60%+ in EEAConsent Mode v2 not implemented, all users effectively "denied"Wire up a Google-certified CMP, set defaults to denied, update on accept.
Double-counted conversionsSame event sent via gtag + API without shared transaction_id / order_idUse transaction_id (gtag) === order_id (API). Google dedupes on this key.
"Unverified" status in Conversions diagnosticsNew conversion action created <4–6 hours agoWait. Don't trust the first day of data.
Attribution flipped from DDA to last-click after migrationOld action archived, new action defaulted to last-clickEdit each new action -> Attribution model -> Data-driven.

2026: What Changed in the Last 12 Months

  • Universal Analytics fully sunset 2024-07-01. GA4 is now the only Google analytics source for Google Ads conversion imports.
  • Consent Mode v2 enforcement tightened (July 2025). EEA accounts without correct signals had remarketing, optimized targeting, and Customer Match disabled.
  • Privacy Sandbox shut down (October 2025). Topics, Protected Audience (PAAPI), and Attribution Reporting API were retired by Google after low adoption. Third-party cookies in Chrome were not deprecated — Google reversed course in 2024 and again in April 2025. Plan for a 3PC-permitted-but-degraded world, not a cookieless one.
  • Customer Match Data Manager migration. As of April 1, 2026, dormant developer tokens (180+ days no Customer Match traffic) lose OfflineUserDataJobService access. New integrations should target the Data Manager API.
  • AI Max for Search GA (April 15, 2026). DSA, ACA, and campaign-level broad match campaigns are auto-upgrading through September 2026 — re-verify conversion accuracy on those campaigns before the upgrade.
  • Simplified conversion measurement setup UI rolled out December 2024 across most accounts. The new flow is the same conversion actions under the hood — older runbooks still apply, the screenshots just look different.
Official ref: FAQs: third-party cookie deprecation in Chrome — read this if anyone in your org is still planning around an imminent 3PC kill date.

Orbit MCP Integration

If your team uses Claude Code, Cursor, or any other MCP client for development, connect the Orbit MCP server to your AI assistant. The MCP exposes:

  • This setup guide as a reference document via get_guide
  • Live Google Ads diagnostics: audit_conversion_setup, diagnose_conversion_tracking
  • Conversion action management: create_conversion_action, update_conversion_action, get_conversion_tag_snippet
  • GA4-to-Ads import: import_ga4_conversion_to_google_ads, create_native_conversion_from_ga4
  • GTM container inspection and updates: list_gtm_tags, create_gtm_tag, diagnose_gtm_container, create_gtm_version_and_publish

Ask your Orbit contact for MCP credentials, or generate them at orbitllm.com/settings/mcp.

ResourceURL
How Google Ads tracks website conversionshttps://support.google.com/google-ads/answer/7521212
Conversion Linker (Tag Manager)https://support.google.com/tagmanager/answer/7549390
Updates to iOS 14+ campaign measurementhttps://support.google.com/google-ads/answer/10417364
Set up Enhanced Conversions for Web (Google tag)https://support.google.com/google-ads/answer/13258081
Set up Enhanced Conversions for Web (GTM)https://support.google.com/google-ads/answer/13262500
Enhanced Conversions for Web in the Google Ads APIhttps://support.google.com/google-ads/answer/13261987
About Enhanced Conversions for Leadshttps://support.google.com/google-ads/answer/15713840
Configure the Google tag for EC for Leadshttps://support.google.com/google-ads/answer/11021502
EC for Leads (API sample)https://developers.google.com/google-ads/api/samples/upload-enhanced-conversions-for-leads
Manage online click conversions (API)https://developers.google.com/google-ads/api/docs/conversions/upload-online
Manage offline conversions (API)https://developers.google.com/google-ads/api/docs/conversions/upload-offline
Set up offline conversions using GCLIDhttps://support.google.com/google-ads/answer/7012522
Guidelines for importing offline conversionshttps://support.google.com/google-ads/answer/15081888
Set up Consent Mode (developers)https://developers.google.com/tag-platform/security/guides/consent
Updates to Consent Mode for EEA traffichttps://support.google.com/google-ads/answer/13695607
Customer Match (API)https://developers.google.com/google-ads/api/docs/remarketing/audience-segments/customer-match/get-started
About primary and secondary conversion actionshttps://support.google.com/google-ads/answer/11461796
About data-driven attributionhttps://support.google.com/google-ads/answer/6394265
About attribution modelshttps://support.google.com/google-ads/answer/6259715
sGTM with Cloud Runhttps://developers.google.com/tag-platform/tag-manager/server-side/cloud-run-setup-guide
sGTM custom domainhttps://developers.google.com/tag-platform/tag-manager/server-side/custom-domain
Create Google Ads conversions from GA4 key eventshttps://support.google.com/google-ads/answer/13735417
Tag Assistant troubleshootinghttps://support.google.com/tagassistant/answer/10039345
Enhanced Conversions for Web tag diagnosticshttps://support.google.com/google-ads/answer/11956168
About parallel trackinghttps://support.google.com/google-ads/answer/7544674
AI Max for Search Campaignshttps://support.google.com/google-ads/answer/15910187
AI Max for Search Campaigns (API)https://developers.google.com/google-ads/api/docs/campaigns/ai-max-for-search-campaigns/getting-started
Third-party cookie deprecation FAQhttps://support.google.com/google-ads/answer/14762010

Last updated: May 2026 · Maintained by Orbit (orbitllm.com)

Google Ads Attribution Setup Guide — Orbit