Attribution guide · All ad platforms · ~5 minute read

UTM tracking best practices for Shopify, Meta, Google + ad platforms

Paid traffic without UTM tags lands as Direct/None and your ROAS reads as junk. This is a fixable 60-second job per ad — copy-paste a template per platform, layer on the ax_* ID namespace so renames don’t break attribution, and let the in-app URL Builder do the rest.

Why UTM tags matter

Every paid click that hits your storefront without UTM parameters lands as Direct / (none) in your analytics — indistinguishable from someone who typed your domain straight into the address bar. That is the single biggest reason DTC operators look at Meta Ads Manager and Admaxxer side-by-side and see different revenue numbers: Meta knows the click came from a Reels ad, but the moment that visitor lands on https://yourstore.com/products/abc with no tag, every downstream attribution layer (your pixel, GA4, Shopify’s built-in source report, every revenue dashboard) loses the trail.

Once that signal is lost it cannot be reconstructed retroactively. CAPI / enhanced conversions can patch part of the gap server-side, but match rate ceilings out around 70–85% in practice, and even then the link back to the specific campaign / ad set / ad is only as good as the IDs you originally sent. UTM tags are the cheapest, most durable attribution signal you have — they survive iOS 26’s ITP, Safari Private Mode, ad-blocker noise, and the slow erosion of the fbclid / gclid click identifiers.

The good news: this is a 60-second job per ad. Most operators who fix UTMs see their attributed-revenue line jump 15–40% the same week — not because they’re selling more, but because revenue that was already happening finally gets attributed to the campaign that drove it. The Admaxxer URL Builder + Coverage tile exists specifically to make this a one-evening project instead of a six-month change-management exercise.

Campaign-level attribution & the {{campaign.id}} rule

Channel-level attribution ("how much did Meta drive?") only needs utm_source + utm_medium. But to drill into which campaign drove the revenue, Admaxxer has to line up the visit’s campaign value (your pixel’s utm_campaign, and Shopify’s last-visit campaign) with the campaign name the ad platform reports. When those two strings match, your own pixel and Shopify revenue fill in next to the platform’s number and you can compare them honestly — the same side-by-side lens you’d expect from a tool like Triple Whale.

The catch: real-world UTMs are messy. The visit might carry the campaign ID instead of the name, the name with a house prefix stripped off, a lowercased copy, an empty value, or a macro that was never filled in. None of those equal the canonical campaign name, so a strict match finds nothing — the campaign shows $0 on your pixel and Shopify columns and only the platform’s own (often inflated) self-reported number is left to fill the row. That single gap is the most common reason campaign-level numbers look worse than they should.

The evergreen fix is one rule: tag every ad with the campaign ID, not just the campaign name. The ID (Meta’s {{campaign.id}}, Google’s {campaignid}) is immutable — it never changes when you rename the campaign and it can’t be re-cased or prefix-stripped. The name ({{campaign.name}}) snapshots at publish time and drifts the moment anyone edits it. The URL Builder pairs both on every URL it generates: utm_campaign={{campaign.name}} for every other tool that reads names, plus ax_campaign_id={{campaign.id}} as the stable key Admaxxer matches on first.

The common ways campaign matching breaks (and how Admaxxer recovers each)

Every example below uses one example campaign: ACME - CBO Broad - USA (Meta campaign ID 120210000000000001). In each case the ad is spending — the only thing broken is the join.

Symptom What the ad sends Real campaign name Why it breaks & how Admaxxer recovers it
Campaign ID lands in utm_campaign instead of the name utm_campaign=120210000000000001 ACME - CBO Broad - USA A raw numeric ID never equals the human-readable campaign name, so an exact name-to-name join finds nothing and the campaign reads $0 on your pixel + Shopify columns. Recovery: if you also send ax_campaign_id={{campaign.id}} (the URL Builder always does), Admaxxer matches on the immutable ID first — the strongest, rename-proof signal — and the row fills correctly regardless of what utm_campaign holds.
An agency / naming-convention prefix is missing utm_campaign=CBO Broad - USA ACME - CBO Broad - USA The visit carries the campaign name with a leading prefix stripped (here the ACME - house prefix). CBO Broad - USA is not character-for-character equal to ACME - CBO Broad - USA, so a strict match fails and the campaign shows no pixel revenue. Recovery: Admaxxer normalizes both sides (lowercase, trim, collapse spacing) and then matches a unique suffix — when exactly one real campaign ends with CBO Broad - USA, the prefix-stripped visit resolves to it. The ID match is still preferred when present.
The macro was never substituted (wrong macro name) utm_campaign={{campaign_name}} ACME - CBO Broad - USA Meta’s macro is {{campaign.name}} (with a DOT). Typing {{campaign_name}} (with an UNDERSCORE) is a common mistake — Meta does not recognize it, so the literal text {{campaign_name}} is sent on every click and every campaign collapses into one un-matchable row. Recovery: Admaxxer detects the un-substituted template token at ingest, strips it to empty so it can’t masquerade as a real campaign, and surfaces it as a fixable warning on the Coverage drill-down. The fix is one character — switch {{campaign_name}} to {{campaign.name}} — and the URL Builder ships the correct macro so you never hit this.
Lowercased, re-cased, or empty campaign value utm_campaign=acme - cbo broad - usa (or blank) ACME - CBO Broad - USA Some platforms lowercase or re-case the value, or the field is left empty entirely. acme - cbo broad - usa is a different string from ACME - CBO Broad - USA to a case-sensitive join, and a blank value matches nothing at all. Recovery: normalized matching makes casing and stray whitespace irrelevant, so re-cased names still resolve. A truly empty utm_campaign can only be recovered by the ID — which is exactly why pairing ax_campaign_id={{campaign.id}} onto every ad is the durable fix.
The AD name lands in utm_campaign (not the campaign name) utm_campaign=Spring Promo A ACME - CBO Broad - USA (ad name: “Spring Promo A”) A very common Meta setup uses the ad name as utm_campaign (the creative name is what the media buyer thinks of as “the campaign”). Spring Promo A is an AD inside ACME - CBO Broad - USA, so a campaign-name join finds nothing and the spend reads $0 even though the click was perfectly tagged. Recovery: Admaxxer also matches the value against your ad names — when a utm_campaign value uniquely equals one real ad, the visit resolves up to that ad’s parent campaign and ad set. Forgiveness, not a license: the bulletproof setup still sends ax_campaign_id={{campaign.id}} + ax_ad_id={{ad.id}} so the join uses immutable IDs instead of guessing from a name.
Campaign name is in utm_source, ad name is in utm_campaign utm_source=acme - cbo broad - usa · utm_campaign=Spring Promo B ACME - CBO Broad - USA (ad name: “Spring Promo B”) Another widespread Meta convention lowercases the campaign name into utm_source and puts the ad name in utm_campaign. A strict reader sees an unknown source AND an unmatched campaign — the channel mis-buckets and the campaign row reads $0. Recovery: Admaxxer recognizes this layout, still classifies the click as paid Meta (the channel is decided by the platform-issued ax_source / known-source list, not the free-text value), then resolves the campaign from utm_source and the ad from utm_campaign. Best-effort, fires only on an unambiguous match — ax_campaign_id / ax_ad_id remove the guesswork.
Spaces arrive as plus signs and never get decoded back utm_campaign=Gental+Detox+Over+40 Gentle Detox Over 40 Some platforms encode spaces in a name as + (form-encoding) rather than %20. A naive reader keeps the literal pluses, so Gental+Detox+Over+40 never equals Gentle Detox Over 40 and the row stays $0. (The typo in the example — “Gental” — is also why ID tags beat names: a fat-fingered creative name can’t break an ID.) Recovery: Admaxxer plus-decodes the value (+ → space) before matching, so plus-encoded names resolve. As always a recovery net under correct tagging — it can’t rescue a value that is also misspelled, one more reason to pair every ad with ax_campaign_id={{campaign.id}}.

How Admaxxer’s resolver decides (priority order)

  1. Exact campaign ID. If the visit carries the immutable campaign ID (via ax_campaign_id), match on it. Rename-proof, case-proof, prefix-proof — the gold standard.
  2. Normalized exact name. Lowercase, trim, and collapse spacing on both sides, then match. This makes casing, stray spaces, and dash-spacing differences irrelevant.
  3. Unique normalized suffix. When a house prefix is stripped, resolve the visit to the one real campaign whose name ends with that value — only when the match is unambiguous (exactly one candidate).
  4. Unique ad-name match. If the value isn’t a campaign but uniquely equals one of your ad names (a very common Meta habit), resolve the visit up to that ad’s parent campaign and ad set so the revenue still lands on the right rows.
  5. Split-field convention. When the campaign name is in utm_source and the ad name is in utm_campaign, recognize the pattern and resolve each from the right field — the channel is still classified as paid Meta from the platform-issued source, never from the free-text value.
  6. Plus-decode. Values whose spaces arrived as + signs (Gentle+Detox+Over+40) are decoded back to spaces before any of the matches above run, so form-encoded names aren’t left stranded.
  7. Fall through to an honest “Untagged / unresolved” row. Anything still unresolved is collected into a single Untagged / unresolved clicks row, shown exactly as it arrived. Admaxxer never fabricates a match or silently merges two campaigns it isn’t sure about — and because that row is always present, the campaign rows plus the untagged row add up to the channel total. Nothing is dropped to make the numbers look tidier.

We recover the common tagging mistakes automatically — but correct tags are always better. Steps 2–6 are best-effort recovery for messy names: they can’t rescue a truly empty value, they can’t be 100% unambiguous on every name, and they can’t un-typo a misspelled creative. Shipping ax_campaign_id={{campaign.id}} + ax_ad_id={{ad.id}} on every ad makes step 1 win — that’s the durable fix, and the URL Builder does it for you.

When a spending campaign still reads $0 — the “why $0?” chip

Every campaign of your connected ad account stays visible on the Sources & Attribution table — a row never disappears just because its tracking is imperfect. When a campaign is spending but shows $0 in the pixel or store-journey column, the row carries a why $0? chip that names the most likely reason instead of leaving you guessing:

The Admaxxer URL Builder (30-second walkthrough)

Open /integrations and click URL Builder in the top-right of any connected ad-platform card. Pick a platform (Meta, Google, TikTok, Klaviyo, Pinterest, Snapchat, Reddit, or Amazon), paste your destination URL (the Shopify product / collection / landing page you’re sending the ad to), confirm the recommended template (it’s pre-filled per platform — you can edit it), and copy the resulting URL parameters. Paste them into the platform’s URL Parameters / Tracking Template field. Done. The Builder also surfaces an Auto-apply button on platforms where Admaxxer can write the template account-wide (Google Ads only today — Meta requires per-ad re-review, so we surface it as an opt-in batch instead of a silent rewrite).

Use the in-app URL Builder

The Admaxxer URL Builder at /url-builder is a public, no-signup tool that takes the friction out of UTM tagging. Hand-crafting URLs across eight ad platforms means memorizing eight different macro syntaxes — Meta’s double-curly {{campaign.id}}, Google’s single-curly {lpurl}, TikTok’s double-underscore __CAMPAIGN_ID__, Klaviyo’s template tags {% raw %}{{ campaign.id }}{% endraw %}. Get one wrong and the macro renders literally instead of being substituted, and every dashboard reads utm_campaign={{campaign.name}} as a single string row instead of unique campaign names. The URL Builder pre-fills platform-correct macros so you cannot ship a typo.

Beyond preventing typos, the Builder auto-applies UTM hygiene rules every operator eventually learns the hard way: lowercase canonicalization on the source field (so utm_source=Facebook doesn’t fragment from utm_source=facebook), no trailing spaces, no double-encoding, and the canonical GA4-recognized medium values (cpc, paid-social, email). It pairs every utm_* with Admaxxer’s ax_* ID namespace so renaming a campaign mid-flight doesn’t fragment your data. Output is a copyable URL or a re-usable template you paste into your platform’s tracking-template field.

How to access

From /marketing-acquisition
The yellow UTM Coverage banner above the source/medium grid surfaces an Open URL Builder button when coverage drops. Click-through pre-fills the offending landing page so you can fix the underlying ad in one shot.
Direct URL: https://admaxxer.com/url-builder
Bookmarkable, no login required. Power users keep this pinned in their browser’s bookmarks bar and deep-link with query parameters (e.g. ?platform=meta) to skip straight to the Meta template.
Deep-link from a fix CTA: /url-builder?landing=/products/widget&platform=meta
When you click Fix on an untagged landing page in the Coverage drill-down, Admaxxer hands off to the Builder with both the destination URL and the platform pre-filled — you confirm and copy. The full fix takes under 30 seconds end-to-end.

End-to-end example: tagging a Meta campaign

Walk through every field with concrete values:

  1. Open /url-builder.
  2. Step 1. Pick Meta from the eight platform cards.
  3. Step 2. Paste the destination URL: https://example-store.com/products/winter-jacket.
  4. Step 3. Confirm the template — Admaxxer pre-fills:
    • utm_source=facebook
    • utm_medium=paid-social
    • utm_campaign={{campaign.name}}
    • utm_content={{adset.name}}
    • utm_term={{ad.name}}
    • ax_source=meta
    • ax_campaign_id={{campaign.id}}
    • ax_adset_id={{adset.id}}
    • ax_ad_id={{ad.id}}
  5. Step 4. Copy the resulting full URL:
    https://example-store.com/products/winter-jacket?utm_source=facebook&utm_medium=paid-social&utm_campaign={{campaign.name}}&utm_content={{adset.name}}&utm_term={{ad.name}}&ax_source=meta&ax_campaign_id={{campaign.id}}&ax_adset_id={{adset.id}}&ax_ad_id={{ad.id}}
  6. Step 5. Paste into Meta Ads Manager › Ad level › URL parameters field (NOT the destination/website URL). Meta concatenates the query string onto your destination automatically — paste the parameters only (everything after ?), not the full URL. Edits re-process in roughly an hour and aren’t retroactive; engagement (likes/comments) stays on the un-tagged post, so social proof doesn’t carry over when you change the URL.

End-to-end example: tagging a Google campaign

  1. Open /url-builder.
  2. Step 1. Pick Google from the eight platform cards.
  3. Step 2. Paste destination: https://example-store.com/products/winter-jacket.
  4. Step 3. Confirm template:
    • utm_source=google
    • utm_medium=cpc
    • utm_campaign={campaignid}
    • utm_content={creative}
    • utm_term={keyword}
    • ax_source=google
    • ax_campaign_id={campaignid}
    • ax_adset_id={adgroupid}
    • ax_ad_id={creative}
  5. Step 4. Copy the resulting URL.
  6. Step 5. Paste into Google Ads › Settings › Account-level URL options › Tracking template. Tracking-template syntax: {lpurl}?utm_source=google&utm_medium=cpc&...&ax_campaign_id={campaignid} — the {lpurl} macro is Google’s standard placeholder for the final URL.

Pair with Google auto-tagging — ship both, not one or the other. Auto-tagging adds gclid for Google’s own attribution graph; manual UTMs survive UTM-blocking extensions, third-party tracker rewrites, and power Admaxxer’s source/medium grid on /marketing-acquisition. Without manual UTMs, your Google paid clicks land in (direct) or as raw gclid rows. Without auto-tagging, you lose Google’s smart-bidding signal. The right answer is always both.

End-to-end example: tagging a Klaviyo campaign

  1. Open /url-builder.
  2. Step 1. Pick Klaviyo from the eight platform cards.
  3. Step 2. Paste destination: https://example-store.com/collections/all.
  4. Step 3. Confirm template:
    • utm_source=klaviyo
    • utm_medium=email
    • utm_campaign={% raw %}{{ campaign.id }}{% endraw %}
    • utm_content={% raw %}{{ message.id }}{% endraw %}
  5. Step 4. Copy the resulting URL.
  6. Step 5. Paste into Klaviyo › Account › Settings › UTM Tracking (account-wide) or per-campaign in the campaign editor. Note Klaviyo’s whitespace-around-braces convention ({% raw %}{{ campaign.id }}{% endraw %} with spaces, not {% raw %}{{campaign.id}}{% endraw %}) — Admaxxer’s URL Builder auto-handles this so you don’t have to remember which platform uses which spacing.

URL Builder FAQ

What’s the difference between utm_* and ax_*?
utm_* is the standard analytics convention every tool understands (GA4, Shopify Analytics, Triple Whale, Northbeam, Hyros, every dashboard ever shipped). ax_* is Admaxxer’s ID-stable namespace that pairs with the names: utm_campaign={{campaign.name}} (which breaks on rename) gets paired with ax_campaign_id={{campaign.id}} (which survives renames). Admaxxer’s match-rate join uses the ID first and falls back to the name only when the ID is absent. Other attribution tools that only join on utm_campaign fragment one campaign into two the moment you rename it mid-flight — Admaxxer doesn’t.
Does Google auto-tagging replace UTMs?
No. Auto-tagging adds gclid to every paid click for Google’s own attribution graph and smart-bidding optimizer, but Admaxxer’s source/medium grid on /marketing-acquisition keys off utm_source, utm_medium, and utm_campaign. Without manual UTMs, your Google paid clicks land in the (direct) bucket or as raw gclid rows nobody can read. Best practice: ship both. Auto-tagging stays on for Google’s optimizer; manual UTMs (via the URL Builder) feed Admaxxer’s grid. They don’t conflict — Google appends gclid after your tracking template, so both end up on the URL.
What if I rename my Meta campaign mid-flight?
If you only used utm_campaign={{campaign.name}}, Meta’s macro snapshots at publish time and Admaxxer ends up seeing two campaigns — one with the old name on day-1 clicks and one with the new name on day-3 onwards. The URL Builder always pairs utm_campaign with ax_campaign_id={{campaign.id}}, which is immutable across renames. Admaxxer’s match-rate join falls back ID → name and never fragments. Renames happen all the time — "Q4 BFCM" becomes "Q4 BFCM — Final Push" 48 hours into the flight — and the ax_* namespace is the cheapest insurance against that pattern eating your dashboard.
Where do I paste the URL Meta gives me?
In Meta Ads Manager, drill into the specific Ad (not the campaign or ad set) › Tracking section › URL parameters field. Paste the QUERY STRING ONLY — the part after the ? — not the full URL. Meta concatenates it onto your destination URL automatically. Pasting the full URL with the leading ? doubled is one of the more common UTM bugs: you end up with https://store.com/?utm_source=meta?utm_source=meta... and the second copy wins, breaking attribution silently.
Where do I paste it for Google Ads?
Two options. Option A (recommended): account-level Tracking template at Settings › Account-level URL options › Tracking template. Syntax: {lpurl}?utm_source=google&...&ax_campaign_id={campaignid}. The {lpurl} macro inserts the Final URL of each ad at click time, so one template covers every ad in your account. Option B: per-ad Final URL with parameters baked in. The Admaxxer URL Builder generates both; pick the one that fits your account structure. For most DTC brands the account-level tracking template is the right answer because it survives ad creates, edits, and bulk-uploads without manual maintenance.
What about TikTok / Pinterest / Snapchat / Reddit / Amazon?
The URL Builder ships templates for all eight platforms. Same flow every time: pick the platform, paste the destination URL, copy the output, paste into the platform’s native tracking field. The platform-specific quirks are handled automatically — TikTok’s __CAMPAIGN_ID__ double-underscore macros, Reddit’s uppercase {{CAMPAIGN_ID}}, Amazon’s no-macro pattern (Amazon Sponsored Brands doesn’t expose macros, so the URL Builder generates a static template per campaign you launch). Snapchat’s {{adsquad.name}} instead of {{adset.name}} is also pre-handled. You don’t need to memorize any of this.
Can I save my templates?
Saved templates are a planned future feature. For v1 the URL Builder is intentionally stateless — pick a platform, build a URL, paste it. Browser bookmarklets work for power users (https://admaxxer.com/url-builder?platform=meta is bookmarkable and pre-selects Meta on load). The deep-link query parameters (?platform=, ?landing=) are the canonical way to chain the Builder into your own workflows today.

Recommended templates per platform

Each row below is copy-paste ready. The utm_* half is the standard analytics-industry contract every dashboard understands; the ax_* half is Admaxxer’s ID namespace that survives campaign renames (see the next section). All eight platforms render correctly in Admaxxer dashboards, GA4, and any other UTM-aware downstream.

Platform Recommended URL parameters Notes
Meta Ads (Facebook + Instagram) ?utm_source=facebook&utm_medium=paid-social&utm_campaign={{campaign.name}}&utm_content={{adset.name}}&utm_term={{ad.name}}&ax_source=meta&ax_campaign_id={{campaign.id}}&ax_adset_id={{adset.id}}&ax_ad_id={{ad.id}}&ax_placement={{placement}} Paste into Ads Manager › Ad › Tracking › URL parameters. The {{campaign.name}} macros snapshot at publish time — that’s why you also want ax_campaign_id={{campaign.id}}, which is immutable. ax_source=meta is the vendor-soft-override for utm_source — if a typo splits Facebook vs facebook in utm_source, the override keeps the source canonical.
Google Ads ?utm_source=google&utm_medium=cpc&utm_campaign={campaignid}&utm_content={adgroupid}&utm_term={keyword}&ax_source=google&ax_ad_id={creative} Set as Tracking template at the account level under Settings › Account settings › Tracking. Admaxxer can write this for you account-wide via the URL Builder’s Auto-apply button (no ad re-review required on Google). Brackets are single-curly on Google — not the double-curly Meta uses.
TikTok Ads ?utm_source=tiktok&utm_medium=paid-social&utm_campaign=__CAMPAIGN_NAME__&utm_content=__AID_NAME__&ax_campaign_id=__CAMPAIGN_ID__&ax_adset_id=__AID__&ax_ad_id=__CID__ Paste into Ad › URL › Tracking URL. TikTok uses double-underscore macros (__CID__, etc.) — not curly-brace style.
Klaviyo (email + SMS) ?utm_source=klaviyo&utm_medium=email&utm_campaign={% raw %}{{ campaign.name|urlencode }}{% endraw %}&utm_content={% raw %}{{ campaign.id }}{% endraw %}&ax_campaign_id={% raw %}{{ campaign.id }}{% endraw %}&ax_send_id={% raw %}{{ message.id }}{% endraw %} Set as the global UTM tracking template at Account › Settings › UTM Tracking or on a per-campaign basis. Klaviyo will automatically URL-encode names that contain spaces.
Pinterest Ads ?utm_source=pinterest&utm_medium=paid-social&utm_campaign={campaignname}&utm_content={adgroupname}&utm_term={creativeid}&ax_campaign_id={campaignid}&ax_adset_id={adgroupid}&ax_ad_id={creativeid} Paste into Ad › Add a destination URL › UTM parameters. Pinterest macros are single-curly with no dot separator.
Snapchat Ads ?utm_source=snapchat&utm_medium=paid-social&utm_campaign={{campaign.name}}&utm_content={{adsquad.name}}&utm_term={{ad.name}}&ax_campaign_id={{campaign.id}}&ax_adset_id={{adsquad.id}}&ax_ad_id={{ad.id}} Paste into Ad › Tracking › URL parameters. Snap calls ad sets ad squads — that’s why the macro is {{adsquad.name}} not {{adset.name}}.
Reddit Ads ?utm_source=reddit&utm_medium=paid-social&utm_campaign={{CAMPAIGN_NAME}}&utm_content={{AD_GROUP_NAME}}&utm_term={{AD_NAME}}&ax_campaign_id={{CAMPAIGN_ID}}&ax_adset_id={{AD_GROUP_ID}}&ax_ad_id={{AD_ID}} Paste into Ad › Tracking & conversion › Tracking parameters. Reddit uses uppercase macro names with double-curly braces.
Amazon Ads (Sponsored Brands › off-Amazon LP) ?utm_source=amazon&utm_medium=paid-search&utm_campaign={CAMPAIGN_NAME}&utm_content={AD_GROUP_NAME}&ax_campaign_id={CAMPAIGN_ID}&ax_adset_id={AD_GROUP_ID} Only relevant for Sponsored Brands & DSP placements that link off-Amazon to your Shopify store. On-Amazon clicks stay on Amazon and are attributed via Amazon Ads’ native API instead.

The ax_* namespace — survives campaign renames

Every ad platform offers two flavors of macro: a name macro (e.g. Meta’s {{campaign.name}}) and an ID macro (e.g. Meta’s {{campaign.id}}). The name macro is what every dashboard reads as utm_campaign — it’s human-readable and survives moves between accounts. But it snapshots at publish time on Meta: if you rename "Q4 Black Friday" to "Q4 BFCM — Final Push" three days into the flight, every subsequent click still carries the original utm_campaign=Q4+Black+Friday value, and your dashboards split one campaign into two.

The fix is to send both. Admaxxer’s URL Builder always pairs utm_campaign={{campaign.name}} with ax_campaign_id={{campaign.id}}, and joins on the immutable ID first when it groups clicks for attribution — falling back to utm_campaign only when the ID is absent. Renames, mid-flight name tweaks, and accidental copy-paste typos no longer fragment one campaign into three. The ax_* namespace is a clean, dedicated prefix (no collision with utm_*, no risk of other tools rewriting it) and it carries the same data in a stable shape across every platform: ax_campaign_id, ax_adset_id, ax_ad_id, plus platform-specific extras like ax_placement on Meta and ax_send_id on Klaviyo.

ax_source — vendor-soft-override for utm_source

ax_source is a stable, system-correct override for utm_source. Where utm_source is whatever the merchant typed (or whatever Meta’s preview substituted), ax_source is a canonical platform identifier the URL Builder injects every time: ax_source=meta, ax_source=google, ax_source=tiktok, ax_source=klaviyo, etc. Admaxxer’s pipes resolve the source as coalesce(nullIf(ax_source,''), utm_source) — when ax_source is set the override wins, and the merchant’s utm_source field still flows through every other tool unchanged.

Why it matters. Source/medium fragmentation is the #2 reason DTC dashboards under-report paid revenue (right after missing UTMs entirely). A team member tags one campaign utm_source=Facebook and another tags utm_source=facebook, and the source/medium grid splits one platform into two rows. A stale value (utm_source=fb from a 2024 template that was never updated) creates a third row. The Klaviyo team types utm_source=klavyio on Tuesday and the typo silently lives in your dashboard for two months. ax_source defends against every flavor of this bug at the source: the URL Builder ships the canonical platform ID, the server sanitizes it (lowercase, max 64 chars, [a-z0-9_-]+ only), and the channel pipes prefer it over utm_source via coalesce.

How it’s different from Triple Whale’s tw_source. Triple Whale uses a vendor-locked tw_source parameter that ONLY their pixel and dashboards understand — switch to a different attribution tool and the parameter is dead weight on your URLs. ax_source coexists with the standard utm_source — every URL the Builder generates carries both. GA4 reads utm_source, Plausible reads utm_source, every legacy tool reads utm_source, and Admaxxer reads ax_source when present (falling back to utm_source when not). Portable across every tool, no vendor lock-in, no rewriting your tracking templates if you ever migrate.

Example URL. A Meta ad URL that pairs utm_source with ax_source + the rest of the ID namespace looks like:

https://shop.com/p/X?utm_source=meta&utm_medium=paid-social&utm_campaign={{campaign.name}}&ax_source=meta&ax_campaign_id={{campaign.id}}&ax_adset_id={{adset.id}}&ax_ad_id={{ad.id}}

The Builder injects ax_source automatically for every supported platform — you don’t need to remember the canonical ID per platform. Server-side, anything that doesn’t match the sanitizer’s allowlist (uppercase letters, spaces, unicode, separator characters) is dropped to empty so the coalesce falls through to utm_source cleanly — never a partially-malformed override row.

Coverage tile on /marketing-acquisition

Open /marketing-acquisition and look for the UTM Coverage tile in the top section. It shows a live percentage — "What share of your paid landing-page sessions in the last 7 days arrived with usable UTM parameters?" — with a 90-day sparkline so you can see whether your tagging discipline is improving or drifting. Drop below 60% and a banner pops above the tile listing the top untagged paid landing pages with a one-click link into the URL Builder pre-filled with that page’s URL. The tile is gradient-ringed (green >90%, amber 60–90%, red <60%) so the answer is glanceable from across the room.

Match-rate diagnostic on /integrations

The Connections page now surfaces a per-campaign UTM Match Rate diagnostic for every connected ad platform. For each campaign the diagnostic shows what fraction of its impressions resulted in tagged sessions on your site — and proposes a one-click suggested fix when it spots a typo (utm_souce), a missing pair (utm_source without utm_medium), or a stale name macro that doesn’t match the current campaign name. Clicking Fix it opens the URL Builder pre-filled with the corrected template and the affected ad pre-selected. On Google Ads the fix is one-click apply; on Meta the fix is a confirmation per ad (because Meta treats URL-parameter changes as an edit that triggers re-review).

Auto-apply (where safe)

Google Ads exposes a single account-level Tracking Template that applies to every click instantly with no re-review — clicks start carrying the new parameters within minutes. The Admaxxer URL Builder writes this for you with one click; it overwrites any existing template, so we show you the prior value first and let you cancel if you have an existing third-party tracker (e.g. Salesforce, HubSpot) you don’t want to disturb.

Meta Ads deliberately treats URL-parameter changes as an ad edit that triggers re-review (~1 hour per ad, paused during review per Meta’s policy — that’s why we surface it as an opt-in batch and confirm per ad before pushing). This is the AD ACCOUNT SAFETY rule in action: silently editing dozens of live ads in one batch would risk a temporary spend pause and a bad first impression with Meta’s ad-quality scoring. The opt-in batch lets you stage the change, pick the lowest-spend window, and approve per-ad. Other platforms (TikTok, Klaviyo, Pinterest, Snapchat, Reddit, Amazon) require manual paste — the URL Builder generates the template; you copy it once per ad. Admaxxer never writes to those platforms automatically because their APIs either don’t expose the field, or expose it with no safe rollback path.

Common UTM mistakes (FAQ)

Typos like utm_souce or utm_camapign
Most common UTM bug: an extra letter, a missing letter, or transposed letters. Every dashboard reads utm_souce as a custom parameter and ignores it for source attribution — so the click lands as Direct/None silently. Admaxxer’s ingest-side UTM lint flags any parameter that’s within edit-distance 2 of a canonical UTM key and surfaces it on the Coverage drill-down as a one-click fix. The URL Builder never produces typos because the parameter names are pre-filled from a constant map.
Spaces in campaign names (Summer Sale vs summer-sale)
A space in utm_campaign=Summer Sale URL-encodes to %20 on most platforms but to + on others — resulting in two separate buckets in your dashboard for the same campaign. The fix is to standardize on hyphenated lowercase (summer-sale, black-friday-2026) for every campaign name you create. The URL Builder previews the encoded form so you see the bug before you ship it.
Inconsistent casing (Facebook vs facebook vs FB)
UTM parameter values are case-sensitive in every analytics tool that’s ever shipped. utm_source=Facebook and utm_source=facebook are two distinct rows. Standardize on lowercase, no abbreviations: facebook (not FB or Facebook), tiktok (not TikTok), google (not Google or GoogleAds). The URL Builder enforces this for the source field; for campaign names you control, pick a convention and document it.
Reserved-word collision (utm_source=cpc, utm_medium=facebook)
Operators sometimes flip utm_source and utm_medium — e.g. they put utm_source=cpc + utm_medium=google when the right shape is utm_source=google + utm_medium=cpc. Source is where the click came from; medium is how it got there. GA4 in particular treats cpc/paid-social/email/affiliate/organic/display as canonical mediums and bins anything else as Other. The URL Builder will warn you if you set medium to a value that GA4 won’t recognize.
Missing-source pairs
If you send utm_source alone (no utm_medium) or utm_medium alone (no utm_source), GA4 silently drops both and the click lands as Direct/None. The pair must be present together. Admaxxer’s ingest-side lint flags any session that arrived with one but not the other and surfaces the offending campaign on the Match-rate diagnostic so you can fix the underlying ad.
"Paid social" with a space vs paid-social with a hyphen
GA4 recognizes paid-social with a hyphen, cpc, cpm, email, affiliate, organic, display, and referral as canonical paid mediums. paid social (with a space) gets URL-encoded inconsistently across platforms and may bin as Other. Always use the hyphenated form.

iOS 26 / Safari Private Mode — UTMs survive when click IDs don’t

Apple’s Intelligent Tracking Prevention has been steadily eroding the click identifiers that ad platforms rely on for attribution: Meta’s fbclid is stripped after 7 days on Safari and immediately in Private Mode; Google’s gclid follows similar rules; even GA4’s _ga cookie has a 7-day cap on Safari since iOS 14. UTM parameters are different — they live in the URL itself and are read once at landing-page time, then persisted server-side by your pixel or analytics tool. Safari has no mechanism to strip URL parameters in flight, and Private Mode doesn’t affect URL handling at all. As long as your pixel reads them before the cookie barrier kicks in (the Admaxxer pixel does this on the first page_viewed event), the attribution chain stays intact.

The takeaway: as the click-ID layer becomes less reliable, UTM hygiene becomes more important — not less. Pair UTMs with server-side CAPI (Meta) / enhanced conversions (Google) for the ~5% of sessions that arrive in Private Mode with no client-side persistence at all. The combination — UTMs in the URL plus server-side conversion API — is what keeps DTC dashboards honest as the privacy layer continues to tighten.