Custom Tracking Parameters for Creative Variants: Let the Ad Platform Fill Them In
You launched 34 creatives last month. Three of them carried the campaign.
Can you name which three? Not the ad set. The actual creative: the hook it used, the format it ran in, the placement it won on.
Most teams cannot. Not because they skipped tracking, but because they tagged all 34 ads with the same utm_content=variant_a and moved on. I have done exactly this, and lost a quarter of creative learning to it.
Here is the thing about creative testing: it is not one test with two arms. It is four or five attributes changing at once across dozens of assets. One tracking field cannot hold that. This post is about the fields that can, and the platform macros that fill them in so nobody types a creative ID by hand.
What this post is not about. I am not re-explaining what a custom parameter is or how to register a GA4 custom dimension. Our custom UTM parameter setup guide covers that end to end. I am also not covering A/B experiment arms, naming schemas or significance. That lives in our A/B test variant schema post. This one starts where both of those stop: the paid creative layer, and the macros that automate it.
Why one variant field runs out the moment you test creative
Because a creative variant is not an experiment arm, and the two need different amounts of room.
An experiment arm answers one question: which version won? Two values, control and variant_a, handle it. A clean binary, and utm_content was built for it.
A creative variant answers a different question: what was this asset made of? Take one TikTok ad. It has a hook (“save 5 hours”), a format (UGC talking head), a length (15 seconds), an editor, and a placement it served on. Five attributes. One ad.
Now run 34 of those. You are not comparing A to B. You are trying to learn that UGC hooks beat studio hooks, that 15 seconds beats 30 on Reels but loses on Feed, and that one editor’s cuts outperform another’s. Three separate findings, one campaign.
Cram it into a single field and you get this:
utm_content=variant_a
That value tells you which ad won. It tells you nothing about why. Six weeks later, when someone asks “should we brief more UGC?”, the data cannot answer.
Honest limitation: most teams do not need this. If you launch four creatives a month, a good utm_content value and a spreadsheet is enough, and everything below is overhead you will resent. The multi-axis approach starts paying off north of 20 creatives a month, when nobody can hold the mapping in their head.

What counts as a custom tracking parameter here
A custom tracking parameter is any query parameter in your URL that your analytics platform does not read on its own. GA4 recognises a closed list of utm_ keys. Everything else, including ad_creative=ugc_hook_15s, arrives in the URL and sits there unread until you register it as a custom dimension.
Before you add a single custom parameter, read what a custom parameter actually costs. The short version: you get 50 event-scoped custom dimensions on the standard GA4 tier, registration is not retroactive, and a parameter you add today shows nothing for last month. Those constraints shape every decision below.
For creative tracking, you have two places to put attributes:
- A standard field you are not using.
utm_termsits empty in almost every paid social account. It costs nothing and needs no registration. - A genuinely custom key, like
ad_format=, which costs a custom dimension slot.
I reach for option 1 far more often. Most teams have two or three unused standard fields and never notice.
Honest limitation: repurposing utm_term for creative format works until the day you run paid search in the same property, where utm_term means keyword. Then one dimension holds two incompatible meanings and your reports quietly lie. Decide once, per property, and write it down.
The three axes actually worth tracking on a creative
Track asset identity, message angle, and delivery context. Skip everything else.
I have watched teams encode nine attributes per creative. They stop maintaining it inside a month. Three axes survive contact with a real workflow:
| Axis | What it answers | Example values |
|---|---|---|
| Asset identity | Which specific file ran? | ugc_004, studio_012 |
| Message angle | What did it claim? | save_time, cut_errors, price |
| Delivery context | Where did it serve? | reels, feed, pangle |
Asset identity is the one people skip and regret. Without it you can tell that “save time” beat “cut errors”, but you cannot find the video file that did it. Six weeks later that file is the brief for your next ten creatives, and nobody can locate it.
Message angle is where the real learning lives. It is also the only one of the three a human has to decide, because no platform knows what your ad claimed.
Delivery context is the one you should never type. The platform already knows the placement and will insert it for you, which is the point of the next section.
Honest limitation: these axes assume creative is the variable you are testing. If your real variable is audience, this taxonomy tracks the wrong thing, and you would do better putting audience segments in the ad set name and letting a macro pull that.
Let the platform fill the values: dynamic macros by network
A dynamic macro is a placeholder you write once in the platform’s URL field, which the platform swaps for the real value at the moment of each click. You write {{ad.name}} one time. Meta writes the actual ad name into the URL of every click, forever, with no typing and no typos.
This is the highest-return setup in this post, and almost nobody does it. It also kills the problem that breaks every manual naming convention: a teammate typing variantA instead of variant_a and fragmenting the data into two unrelated rows.
Here is the catch that costs people days. Three platforms, three completely different syntaxes, for the same job.
| Job | Meta | Google Ads | TikTok |
|---|---|---|---|
| Syntax style | Double curly braces | Single curly braces | Double underscores |
| Campaign name | {{campaign.name}} |
(use manual value) | __CAMPAIGN_NAME__ |
| Campaign ID | {{campaign.id}} |
{campaignid} |
__CAMPAIGN_ID__ |
| Ad group / ad set name | {{adset.name}} |
(use manual value) | __AID_NAME__ |
| Ad group / ad set ID | {{adset.id}} |
{adgroupid} |
__AID__ |
| Ad / creative name | {{ad.name}} |
(use manual value) | __CID_NAME__ |
| Ad / creative ID | {{ad.id}} |
{creative} |
__CID__ |
| Placement | {{placement}} |
{placement} |
__PLACEMENT__ |
| Network or surface | {{site_source_name}} |
{network} |
(use __PLACEMENT__) |
A working Meta template, pasted once into the URL Parameters field:
utm_source=facebook&utm_medium=paid_social&utm_campaign={{campaign.name}}&utm_content={{ad.name}}&ad_placement={{placement}}&ad_surface={{site_source_name}}
{{site_source_name}} is underrated. It splits Facebook, Instagram, Audience Network and Messenger apart, which Meta otherwise blends into one facebook source, quietly ruining channel comparisons.
The Google Ads equivalent, in the final URL suffix:
utm_source=google&utm_medium=cpc&utm_content={creative}&ad_network={network}&ad_match={matchtype}
{network} returns a single letter, and the legend matters: g is Google search, s is search partners, d is Display, ytv is YouTube, vp is video partners, gtv is Google TV, x is Performance Max, e is app engagement. That x is how you separate PMax traffic without guessing.
Honest limitation: macros give you IDs and names, never meaning. {creative} returns a numeric ad ID, not “the UGC one with the kitchen hook”. Macros solve the typing and typo problems. They do not solve the taxonomy problem, which is why message angle still has to be decided by a human and baked into the ad name.
Four macro traps that corrupt data silently
Every one of these fails without an error message. That is what makes them expensive.
1. TikTok’s __AID__ and __CID__ do not mean what they look like
In TikTok’s macro vocabulary, __AID__ returns the ad group, not the ad. __CID__ returns the creative, not the campaign. The ad-level identifier is __ADID_V2__, and it is available on Smart+ only.
Read that twice, because the abbreviations point exactly the wrong way. Everyone reads AID as “ad ID” and CID as “campaign ID”. Both readings are wrong.
Here is why it beats an ordinary mistake. The substitution still succeeds. TikTok swaps in a real, valid, plausible numeric ID. Your utm_content column fills with numbers, every dashboard renders, nothing is flagged. You then spend a quarter analysing ad-group performance while believing you are analysing creative performance.
I only caught this by pulling one ID out of a report and searching for it in TikTok Ads Manager. It resolved to an ad group. That five-minute check is worth doing on every new platform before you trust a number.
2. TikTok’s underscores must be doubled
__CAMPAIGN_ID__ works. _CAMPAIGN_ID_ does not. TikTok cannot match a single-underscore token, so it passes the literal text straight through into your URL. Your report then shows a row whose value is the characters _CAMPAIGN_ID_, repeated across every click.
The upside: this one fails loudly and you will spot it the first time you look. It is the only trap on this list that announces itself.
3. Meta’s URL Parameters field is ad-level with no cascade
The URL Parameters field lives on the ad, not the campaign and not the ad set, and settings do not flow downward. Set a perfect template at campaign level and it applies to nothing.
This is why so many accounts have partial tracking: the parameters were set on the four ads live that week, then every ad created afterwards launched bare. Applying a template across an existing account means bulk edit, and auditing it means checking ads, not campaigns.
The tell is a GA4 report where a chunk of paid social traffic has a campaign name and the rest has (not set), from the same account, in the same week.
4. Google’s {keyword} returns blank in the campaign types you now run most
{keyword} returns nothing in Performance Max, Dynamic Search Ads and AI Max when ads match without keywords. Not an error, not a default. Blank. {placement} and {targetid} are unsupported in Demand Gen campaigns entirely.
This matters more every year, because the campaign types where these macros go quiet are the ones Google keeps pushing budget toward. A ValueTrack template written for classic search in 2022 degrades silently as the account shifts to PMax. On the keyword side, {matchtype} now returns a for AI Max keywordless matching, alongside the familiar e, p and b.
Honest limitation across all four: none of these traps show up in a link checker, because the URLs are structurally valid. The only reliable test is clicking a live ad and reading the resulting URL, per platform, before you scale spend. I do this once per platform per quarter and it catches something roughly every other time.
How to parse a multi-axis value back apart in GA4
Put several attributes in one field with a delimiter, then split them back out in Explore. This is how you track three axes without spending three custom dimension slots.
The value looks like this:
utm_content=ugc_004|save_time|15s
Three attributes, one field, one pipe delimiter between them.
Choosing the delimiter is the part that goes wrong. My rules, learned the annoying way:
- Never use a character your values already contain. Underscores are out if your naming uses them, which it does.
- Never use
&or=. They are URL structure and will break the parameter itself. - Avoid spaces. They become
%20and your report rows split into two variants of the same value. - Pipe (
|) or double underscore (__) both work. I use pipe for creative attributes and reserve double underscore for experiment arms, so the two never collide in one field.
To read it back, open Explore, add utm_content as a dimension, and build a custom field that splits on the pipe. Each position becomes its own column: asset in one, angle in the next, length in the third. Now you can pivot angle against placement without registering a custom dimension at all.
The trade-off is worth stating plainly. Delimiter encoding costs zero quota but gives you no native filtering: GA4 sees one opaque string, so you cannot filter on “angle = save_time” in a standard report. A real custom dimension costs a quota slot but is filterable everywhere. I use delimiters while a taxonomy is still changing, then promote the one axis that proves useful to its own registered parameter.
Keeping the delimiter and the field order identical across every link is the part humans fail at. This is exactly what dynamic variables are for: the structure is defined once and populated per link, so position three is always length and never quietly becomes editor.
Honest limitation: GA4 event parameter values truncate at 100 characters. Three attributes fit comfortably. Six do not, especially with descriptive names, and the truncation is silent. If your encoded value is creeping past 60 characters, you have too many axes.
When to skip all of this
Skip it when the creative volume does not justify the maintenance, and skip it when the platform already answers the question.
Four cases where I would not do any of the above:
- You run under 20 creatives a month. The platform’s own reporting already tells you which creative won. A parameter layer buys you nothing except a taxonomy to maintain.
- You only care about which creative won, not why. Meta and TikTok both report creative-level results natively. Custom parameters earn their keep when you compare attributes across campaigns and platforms, which native reporting cannot do.
- Nobody owns the taxonomy. It degrades in about six weeks. Half-tagged data is worse than untagged data, because it looks complete.
- You are testing audiences, not creative. Wrong tool. Put the segment in the ad set name and pull it with a macro.
Where it clearly pays off: multiple platforms, high creative volume, and a real question about attributes rather than assets. “Do UGC hooks beat studio hooks across Meta and TikTok?” is a question no native report will answer, because none spans both platforms with a shared vocabulary.
Frequently Asked Questions
What are custom tracking parameters in advertising?
Custom tracking parameters are query parameters added to an ad’s destination URL that your analytics platform does not read by default. GA4 recognises a closed list of utm_ keys, so a key like ad_format=ugc arrives in the URL but stays invisible until you register it as a custom dimension. In advertising they carry creative attributes the five standard UTM fields have no room for, such as asset ID, message angle or editor.
What is the difference between __AID__ and __CID__ in TikTok ads?
In TikTok’s macro vocabulary, __AID__ returns the ad group identifier and __CID__ returns the creative (ad) identifier. The abbreviations mislead, because most people read them as “ad ID” and “campaign ID”, and both readings are wrong. The ad-level identifier is __ADID_V2__, available on Smart+ only. The mistake is dangerous because the substitution still succeeds and fills your report with valid-looking IDs pointing at the wrong objects.
Can I use dynamic macros inside UTM parameters?
Yes, and this is the recommended setup. You write something like utm_content={{ad.name}} in the platform’s URL field, and the platform replaces the macro with the real value at click time. The macro is just the value side of a normal UTM pair, so the result is an ordinary tagged link. Syntax does not transfer between platforms: Meta uses double curly braces, Google Ads uses single curly braces, TikTok uses double underscores.
Why do my Meta URL parameters only apply to some ads?
Because Meta’s URL Parameters field lives at the ad level and does not cascade from the campaign or ad set. Any ad created after you set the template launches without it unless you apply the parameters again. The usual symptom is a GA4 report where some paid social traffic carries a campaign name and the rest shows (not set) from the same account and week. Fixing an existing account means a bulk edit at ad level.
Does {keyword} work in Performance Max?
No. Google’s {keyword} ValueTrack parameter returns a blank value in Performance Max, Dynamic Search Ads and AI Max whenever ads match without keywords. It does not error or fall back to a default, it comes through empty. {placement} and {targetid} are separately unsupported in Demand Gen, so a ValueTrack template built for classic search degrades quietly as campaign mix shifts.
Should creative variants go in utm_content or a custom parameter?
Start with utm_content if it is free, and move to a custom parameter only when you need to filter on the attribute natively. utm_content needs no registration, works retroactively, and is filterable in GA4 out of the box. A custom parameter costs one of your 50 event-scoped custom dimension slots and shows no data for any period before you registered it. My sequence is to encode attributes in utm_content with a delimiter first, then promote the axis that proves useful.
How many creative attributes can I track in one parameter?
Three comfortably, four at a push. GA4 truncates event parameter values at 100 characters without warning, and descriptive attribute names eat that budget fast. The practical constraint arrives sooner: every attribute is another thing a human has to get right on every ad, and taxonomies with more than four axes stop being maintained within a couple of months.
Start with the axis, then automate the typing
Creative testing generates learning that outlives the campaign. The hook that worked in March is the brief for June, but only if you can still find it.
Three things to do this week:
- Pick your three axes. Asset identity, message angle, delivery context. Write them down so the vocabulary is fixed.
- Set one macro template per platform, in the right field at the right level. Meta at ad level, Google in the final URL suffix, TikTok in the URL parameter field.
- Click one live ad per platform and read the URL in your browser. Confirm every macro resolved to what you expected, especially on TikTok.
That third step is the one people skip, and it catches the __AID__ problem before it costs you a quarter of creative learning.
Once the taxonomy is fixed, nobody should type it again. Lock the structure with UTM rules so every link comes out identical, and let the platform macros fill in the rest.