Operations & Data · Draft

UTM & Attribution Spec — Per-Vendor, GA4 Match Strategy

Created 11 Jun 2026·Updated 11 Jun 2026

Latest change: Publish Dossier site and full doc pack to GitHub

Draft document — deep-dive spec incomplete; content will be updated before and during build. Do not treat as signed-off implementation detail. Pack overview

Goal: Near 100% match between GA4 (source of truth) and ad platform reporting at campaign level (then ad/creative).

Two tracks:

Track Vendors URL parameters
A — Auto-tagging Google Ads (all products) No utm_*gclid + GA4 ↔ Ads link only
B — UTM macros Meta, TikTok, DV360 Full utm_* schema with dynamic macros

CRM SKU (Google only): add kcid / kpv custom parameters (no utm_ prefix) on top of auto-tagging — for form capture and offline match. Paste instructions in implementation guide.

Related: Data & tracking · GA4 source of truth · Campaign execution · Reporting


Principles

# Rule
1 Google: no utm_* when auto-tagging is on — avoids collision with Google's auto-tagging and GA4 Ads import
2 Social: campaign ID in utm_campaign — macro-first ({{campaign.id}}, __CAMPAIGN_ID__); system-inject at mutate if macros unavailable
3 Campaign ID first for joins — platform campaign_id, then click ID, then normalized name
4 CRM on Google — capture gclid + custom params (kcid, kpv) only — never utm_* on Google Ads URLs or CRM fields for Google-sourced leads
5 CRM on Meta/TikTok — capture fbclid/ttclid + utm_* from landing URL (macros resolve to platform IDs)
6 QC on every mutate — Google: auto-tagging + no forbidden utm_; Social: validated UTM template

Canonical contracts

Track A — Google Ads (no utm_*)

Mechanism Purpose
Auto-tagging (gclid, gbraid, wbraid) Primary click ↔ campaign join
GA4 ↔ Google Ads link googleAdsCampaignId, cost, campaign name in GA4
Kobi Google Ads BQ export Warehouse join on campaign.id
Custom params (kcid, kpv) — CRM SKU only Internal plan/manifest key on landing URL + forms — not utm_ prefix

GA4 knows Google traffic from Google Ads data source + session gclid — not from utm_*.

Track B — Meta / TikTok / DV360 (utm_* schema)

Parameter Semantics GA4 join priority
utm_source Vendor channel slug (fixed enum) Channel validation
utm_medium paid_social, display, video Channel validation
utm_campaign Platform campaign ID — macro or injected Join key #1
utm_content Platform ad / creative ID Join key #2
utm_term Ad set / keyword / placement — optional Drill-down
Vendor utm_source utm_medium
Meta (FB / IG) {{site_source_name}} or facebook / instagram paid_social
TikTok tiktok paid_social
DV360 dv360 display

GA4 custom dimensions (all tenants)

Dimension Google source Social source
platform_campaign_id Ads link / BQ / gclid resolution utm_campaign
platform_ad_id BQ ad.id via click join utm_content
kobi_campaign_key kcid custom param (CRM) or manifest kcid or utm_* + manifest
kobi_plan_version kpv custom param kpv or event scope
kobi_tenant_id Registry Registry

Execution decision tree

Google AdsYesNoMeta / TikTok / DV360YesNoApply URL tracking atexecutionPlatform?Google: auto-tagging only no utm_*CRM SKU?Append kcid kpv viafinal_url_suffix +ValueTrack customparamsQC: auto_tagging on; noutm_ in templateSocial: macros supported?URL tags / template withutm_* macrosInject utm_* from APIresponse IDsStore IDs inexecution_manifestQC: utm_campaign =platform ID macro ornumeric

Social: never static human slug in utm_campaign. Google: never any utm_* parameter on ads URLs.


Per-vendor specification

Item Detail
Primary attribution Auto-tagging (gclid) + GA4 ↔ Google Ads link + Kobi Ads BQ
utm_* on ads Forbidden — auto-tagging is active; UTMs add noise and can conflict with GA4 Google Ads integration
Default URL Final URL = landing page only (no tracking template with utm_)
CRM SKU add-on Custom parameters only — see below

All Google products (Search, Display, Shopping, PMax, YouTube, Demand Gen): same rule — no utm_*.

Ad type Join path
Search / Display / Shopping gclid + GA4 Ads link + BQ campaign.id
Performance Max gclid + BQ campaign ID; asset group in manifest for ops — not via UTM
YouTube Same auto-tagging

CRM SKU — custom parameters (paste on forms + optional URL suffix)

Use non-utm_ query params so auto-tagging stays clean:

Param Set by Value CRM / form field
kcid Execution logical_campaign_key (UUID) Hidden input — paste from guide
kpv Execution plan_version integer Hidden input

Optional final_url_suffix (CRM SKU only) — campaign level:

kcid={_kcid}&kpv={_kpv}

Define custom parameters on campaign / ad group in Google Ads API (custom_parameters or url_custom_parameters):

Custom param Value at mutate
_kcid logical_campaign_key from plan
_kpv plan_version

Implementation guide §: “Add hidden fields gclid (auto from URL), kcid, kpvdo not add utm_* fields for Google traffic.”

API surfaces (execution):

Object Field Action
Customer auto_tagging_enabled true — required
Customer / Campaign tracking_url_template Empty or {lpurl} only — no utm_
Campaign final_url_suffix kcid/kpv suffix if CRM SKU
Campaign url_custom_parameters _kcid, _kpv if CRM SKU

Onboarding: enable auto_tagging_enabled ✅; verify GA4 ↔ Ads link when client grants Admin (cross-check).


Meta (Facebook / Instagram)

Item Detail
Primary attribution UTM + fbclid — no native GA4 auto-link like Google
Macro support Dynamic URL parameters on ad / ad set
Apply level Ad set URL tags or ad website_url parameters

Recommended URL tags (ad set or account):

utm_source={{site_source_name}}&utm_medium=paid_social&utm_campaign={{campaign.id}}&utm_content={{ad.id}}&utm_term={{adset.id}}
Meta macro Maps to Notes
{{campaign.id}} utm_campaign Required
{{ad.id}} utm_content Ad-level
{{adset.id}} utm_term or secondary content Ad set drill-down
{{site_source_name}} utm_source facebook vs instagram automatically
{{campaign.name}} Do not use for join Diagnostic only if needed in unused param

Fallback (no macros — rare):

After POST ad create, set creative.link_data.link / asset_feed_spec URLs with:

utm_campaign={campaign_id}&utm_content={ad_id} from API response.

Advantage+ / dynamic creative: macros still apply on served ad; verify in Ads Manager preview. If URL frozen at creative bundle level → system regenerate on each creative variant ID.


TikTok Ads

Item Detail
Primary attribution UTM + ttclid
Macro support ✅ TikTok URL parameters
Apply level Ad / ad group URL field

Recommended URL suffix:

utm_source=tiktok&utm_medium=paid_social&utm_campaign=__CAMPAIGN_ID__&utm_content=__CID__&utm_term=__AID__
TikTok macro Maps to
__CAMPAIGN_ID__ utm_campaign
__CID__ utm_content (creative)
__AID__ utm_term (ad group)
__CAMPAIGN_NAME__ Do not use for join

Fallback: post-create inject from campaign_id, adgroup_id, ad_id fields on ad object.


DV360 (deferred SKU)

Item Detail
Macros ✅ Campaign Manager / DV360 placement macros (%ebuy!, %eaid!, etc.) — pin at implementation
GA4 UTMs on all click-through; Floodlight optional — GA4 SoT per dv360.md
Join Campaign / insertion order ID → utm_campaign via macro or system suffix

Execution service responsibilities

At apply(plan_version) (campaign execution):

Step Owner
1. Resolve logical_campaign_key → platform create mutate Execution connector
2. Google: ensure auto-tagging; apply kcid/kpv suffix if CRM SKU — no utm_* Execution connector
2b. Social: apply URL tags / templates with utm_ macros* Execution connector
3. Social only: if macro unsupported, patch final URL with IDs from mutate response Execution connector
4. Persist platform_campaign_id, tracking_mode, template hash on execution_manifest Registry
5. Run tracking QC (deterministic) — Google: no utm_; Social: valid macros QC
6. Emit tracking.applied audit event Orchestrator

execution_manifest tracking slice (illustrative)

Google (CRM SKU):

{
  "logical_campaign_key": "health_search_brand_v3",
  "platform": "google_ads",
  "platform_campaign_id": "12345678901",
  "tracking_mode": "auto_tagging",
  "final_url_suffix": "kcid={_kcid}&kpv={_kpv}",
  "custom_parameters": { "_kcid": "uuid", "_kpv": "3" },
  "utm_forbidden": true
}

Meta:

{
  "platform": "meta",
  "platform_campaign_id": "120210...",
  "tracking_mode": "macro",
  "url_tags": "utm_source={{site_source_name}}&utm_medium=paid_social&utm_campaign={{campaign.id}}&utm_content={{ad.id}}"
}

GA4 ↔ platform match strategy (near 100%)

Join hierarchy (use in order — stop at first hit)

Priority Key Google Ads Meta TikTok
1 Platform campaign ID googleAdsCampaignId (GA4 Ads link) / BQ campaign.id utm_campaign = {{campaign.id}} utm_campaign = __CAMPAIGN_ID__
2 Click ID gclid ↔ click report fbclid ttclid
3 Linked / warehouse Kobi Google Ads BQ join
4 Platform campaign name Normalized session_campaign = ads campaign.name Same Same
5 Ad / creative ID BQ ad.id via gclid utm_contentad.id utm_content ↔ creative
6 Internal key kcid custom param (CRM) kcid or manifest via UTM IDs Same

Google does not use utm_campaign in join logic.

Normalization for name fallback: lowercase, trim, collapse spaces, strip emoji, remove | Kobi suffixes from auto-naming.

Target match rates (rolling 7d, campaign grain)

Pair Target Alert
GA4 sessions ↔ Google Ads (linked + gclid, no UTM) ≥ 98% < 95%
GA4 UTM-attributed conversions ↔ Meta Ads ≥ 95% < 90%
GA4 ↔ TikTok Ads ≥ 95% < 90%
CRM leads with click ID ↔ GA4 generate_lead ≥ 90% < 80%

Unmatched volume → attribution_gap reason codes: missing_gclid, ads_link_broken, missing_utm (social only), name_mismatch, pmax_granularity, consent_denied, redirect_strip.

Reporting join flow

campaign_id firstGA4 Analytics Data APIGoogle Ads BQMeta Insights APITikTok APIAttribution join serviceDashboard

Optimization rule: bid and budget decisions use GA4 metrics joined on campaign ID; platform UI conversions are diagnostic only (ga4-source-of-truth.md).


QC checklist (deterministic — not LLM)

Check Fail action
auto_tagging_enabled = true Block campaign.live
No utm_ substring in tracking template, final URL suffix, or ad URLs Block
CRM SKU: kcid/kpv present in suffix or custom params Warn if CRM SKU
GA4 ↔ Ads link active (if GA4 Admin granted) Warn
Test click: gclid on landing URL Block

Meta / TikTok / DV360

Check Fail action
utm_source + utm_medium + utm_campaign present Block
utm_campaign = macro or numeric platform campaign ID Block
Not human slug alone in utm_campaign Block
Redirect preserves utm_* and click IDs (landing probe) HITL
GA4 DebugView: utm_campaign matches platform ID after test click Warn

CRM & offline alignment

Google-sourced leads (CRM SKU)

Form / CRM field Source Notes
gclid Auto on landing URL from Google click Required for offline import
kcid Custom param kcid on URL Paste hidden field — implementation guide
kpv Custom param kpv on URL Plan version
utm_* Do not capture for Google traffic Auto-tagging handles attribution

Meta / TikTok-sourced leads

Field Source
fbclid / ttclid Landing URL
utm_campaign Platform campaign ID (from macros)
utm_content Ad ID
kcid / kpv Optional custom params (same as Google)

Offline upload: click ID first, then hashed PII. Google offline conversions use gclid only — not utm_*.


Implementation guide deliverable (onboarding)

Section Content
Google Auto-tagging explained; no UTM; CRM forms: paste gclid + kcid + kpv hidden fields
Meta / TikTok Full utm_ macro* templates; Kobi execution applies automatically
GA4 Custom dimensions; Google via Ads link; social via utm_campaign
Test Google: verify gclid + linked campaign in GA4; Social: verify utm_campaign = platform ID
Do not Add utm_* to Google Ads; override social URL tags without ops approval