Dynamic UTM Variables in Programmatic: Why DSP Macros Return IDs, Not Names

Ever opened a GA4 report for a programmatic campaign and found a column of numbers where the campaign names should be?
I have. The first time, I assumed something was broken. Nothing was broken. That is just what programmatic tracking gives you, and nobody warns you before you turn on spend.
I run link tracking for a living at linkutm, and paid social macros are well covered ground. Programmatic is not. So this post is about the part people skip: what dynamic UTM variables actually return when the traffic comes from a demand-side platform, why the values look nothing like your Meta reports, and the one macro in Display & Video 360 whose name lies to you.
If you want the Meta, Google Ads and TikTok syntax instead, I wrote that separately in custom tracking parameters for creative variants. This post does not repeat any of it.
What Is a Dynamic UTM Variable?
A dynamic UTM variable is a placeholder you write once into a platform’s URL field, which the platform replaces with a real value at the moment of each click.
You type the placeholder one time. The ad platform types the actual value a million times, for free, with no typos.
Compare the two approaches:
| Static UTM value | Dynamic UTM variable | |
|---|---|---|
| Who types the value | A person, per link | The ad platform, per click |
| Typo risk | High, and it fragments reports | None |
| Stays correct when the ad is renamed | No | Yes |
| Readable without a lookup | Yes | Depends on the platform |
That last row is the whole reason this post exists. On Meta, a dynamic variable hands you a name you can read. On a DSP, it usually does not.
A quick definition before going further, because the rest of the post leans on it. A DSP, or demand-side platform, is the software that buys ad inventory across many websites and apps through real-time auctions. Display & Video 360 is Google’s. The Trade Desk and Amazon DSP are the other two you are most likely to meet.

Why Dynamic UTM Variables Behave Differently in Programmatic
Here is the thing that catches people. Display & Video 360 has no macro that inserts a campaign name into a landing page URL. It has no macro that inserts any name at all. The entire landing-page vocabulary is identifiers.
Meta gives you {{ad.name}} and {{campaign.name}}. Those return words. A DSP gives you ${CREATIVE_ID} and ${INSERTION_ORDER_ID}. Those return numbers like 88134422.
So a report built on social macros is readable on day one. A report built on DSP macros is a spreadsheet of integers until you do extra work. That is not a bug you can configure away. It is how the platforms were built.
There is a second difference worth knowing. DSP macros are a fourth syntax family. Meta uses double curly braces, Google Ads uses single curly braces, TikTok uses double underscores, and DSPs use a dollar sign with braces: ${MACRO_NAME}. Four platforms, four ways to write the same idea.
Honest limitation: I can only give you verified syntax for Display & Video 360 in this post. The Trade Desk and Amazon DSP each use their own vocabulary, and I could not verify their current macro lists against primary documentation while writing this. Rather than print syntax I have not confirmed, I would rather tell you to pull the macro list from inside the platform itself. Guessing at a macro name costs you a week of blank columns.
The Ad Hierarchy Is Why the Names Get Confusing
This section is the key to everything below, so it is worth 60 seconds.
Programmatic buying nests five levels deep:
- Advertiser. The brand paying.
- Campaign. The top-level container, usually one objective.
- Insertion order. The budget and flight dates. An insertion order is the booking unit that holds a budget for a date range.
- Line item. The actual targeting and bidding instructions. A line item is the level where you set audience, inventory and bid.
- Creative. The individual ad.
Meta’s hierarchy is three levels: campaign, ad set, ad. DSPs add insertion orders in the middle. When you carry a mental model from Meta into a DSP, the extra level is where things go wrong, because the macro names do not map the way you expect.
Which brings me to the single most expensive thing in this post.
What ${CAMPAIGN_ID} Actually Returns in DV360
In Display & Video 360, ${CAMPAIGN_ID} returns the line item ID, not the campaign ID. Google documents this in its own macro reference. The name points at one level of the hierarchy. The value comes from another.
Read that again, because everyone I have shown it to has had to.
Now look at why it is so costly. The substitution does not fail. DV360 swaps in a real, valid, plausible number. Your utm_campaign column fills up. Every dashboard renders. No error appears anywhere. You then spend a quarter analysing what you believe is campaign performance, while you are actually looking at line items.
Line items outnumber campaigns badly. One campaign can hold dozens. So your campaign report quietly becomes a fragmented mess of near-duplicate rows, and the obvious diagnosis is “GA4 is splitting my traffic”, which sends you off debugging the wrong system entirely.
It gets one step worse. There is no macro that puts the real campaign ID into a landing page URL. If you want the campaign identifier in your UTM string, you hardcode it per campaign. That is the one value in this entire setup you cannot automate, and it is the value most people assume is automatic.
This is the same category of trap as TikTok’s __AID__ and __CID__, which point the opposite way from what their abbreviations suggest. I wrote that one up in the creative variants post. The pattern repeats across the industry: macro names are written by engineers describing an internal object, not by marketers describing a report column.
Honest limitation: I am reporting Google’s documented behaviour, and platforms do change. That is exactly why the verification method further down exists. Do not trust this post in six months. Trust the click you tested yesterday.

The DV360 Dynamic UTM Variables Worth Knowing
These are the ones I actually use, pulled from Google’s macro documentation. Everything here returns an identifier unless stated otherwise.
| Macro | Returns | Worth tagging? |
|---|---|---|
${CREATIVE_ID} |
The creative identifier | Yes, your main creative axis |
${LINE_ITEM_ID} |
The line item | Yes |
${CAMPAIGN_ID} |
The line item, despite the name | Avoid, it misleads |
${INSERTION_ORDER_ID} |
The insertion order | Yes, for budget-level reporting |
${ADVERTISER_ID} |
The advertiser | Rarely, it is constant per account |
${UNIVERSAL_SITE_ID} |
The site the ad ran on | Yes, this is placement data |
${EXCHANGE_ID} |
The exchange serving the impression | For supply-path questions |
${BUNDLE_ID} |
The app identifier | Yes, for in-app inventory |
${AUCTION_ID} |
The individual auction | No, cardinality explodes |
${CACHEBUSTER} |
A random number | No, never in a UTM |
Two notes on that table.
${UNIVERSAL_SITE_ID} is the underrated one. Programmatic runs across thousands of sites, and without it your entire DSP spend collapses into one undifferentiated source. It is the closest thing programmatic has to Meta’s placement breakdown.
${AUCTION_ID} is the trap at the other end. A cachebuster is a random number added to a request so a browser or server cannot serve a cached copy. Auction IDs behave similarly for reporting purposes: both are unique per event. Put either in a UTM parameter and you generate a brand new value on every single click. GA4 will bucket the overflow into an (other) row and your report becomes useless. I covered why that ceiling exists in designing a UTM framework that scales, so I will not rebuild the maths here. Just never tag a value that is unique per click.
Honest limitation: this table is the landing-page-useful subset, not the full list. DV360 also exposes consent macros such as ${GDPR} and ${GDPR_CONSENT_xxxxx}, plus geo macros. Those have real uses in ad serving, but they do not belong in campaign attribution, so I left them out.
Encoding, and the Redirect Nobody Mentions
Your click does not go straight to your landing page. DV360 wraps supported third-party tags with impression and click tracking macros, so the click passes through the ad server first and then redirects onward. That detour is where UTM strings get damaged.
DV360 offers the click URL at three different encoding levels: ${CLICK_URL}, ${CLICK_URL_ENC} and ${CLICK_URL_ENC_ENC}. Plain, encoded once, encoded twice.
Picking the wrong one is not a subtle failure. Your ampersands turn into %26, the destination stops treating them as parameter separators, and the whole UTM string arrives as one long value glued into a single parameter. Your report then shows one enormous meaningless row.
The rule I follow: the number of encoding layers must match the number of redirects the URL passes through before it is read. One hop, encode once. Two hops, encode twice. If you are handing your URL to a third-party click tracker that then redirects again, you are usually at two.
Real talk, I do not calculate this from first principles any more. I click the ad and read the address bar. Thirty seconds beats an hour of reasoning about layers.
Honest limitation: this is also the point where a link management layer earns its keep or gets in the way. Every extra redirect in the chain is another encoding decision and another few milliseconds. If your DSP setup already works, adding a hop to make the links prettier is not a trade I would make.

Turning Dynamic UTM Variables Back Into Readable Names
So your report is full of numbers. Here is how you fix the readability problem without pretending the IDs are something they are not.
The ID-to-name join is a lookup table that maps each platform identifier to the human name it represents, applied after the data lands in your reporting tool. You export the ID and name columns from the DSP, then join them onto your analytics export on the ID.
Three ways to do it, in the order I would try them:
- Join in your BI tool. Export line item and creative names from DV360, drop them in a sheet, join on the ID in Looker Studio. Cheapest option, works today, needs a refresh whenever you launch new line items.
- Use a single tagged identifier. Put one stable value in
utm_idand keep the full detail in the DSP, then join on that one field. The utm_id parameter exists for exactly this, and it keeps your other four parameters readable. - Hardcode the names you control. Nothing forces you to make every field dynamic. Campaign name is stable for a campaign’s whole life, so type it once per campaign and let macros handle only the values that genuinely vary per click.
Option three is the one people resist and the one I recommend most. Dynamic is not automatically better. A macro is worth it when the value changes per click and would otherwise be typed by hand repeatedly. Campaign name fails that test.
Honest limitation: every join is a maintenance job. If nobody re-exports the name table, the report silently keeps showing last month’s names against this month’s IDs, which is arguably worse than showing raw numbers. Raw numbers at least look wrong.
How I Verify a DSP Macro Before Scaling Spend
This takes about ten minutes per platform and has caught something roughly every other time I have done it. Do it once per platform per quarter, and always before a budget increase.
- Launch one ad with a tiny budget. One creative, one line item, minimum spend. This is a test, not a campaign.
- Click your own live ad. Not a preview. Previews frequently do not resolve macros at all, which is why previews give false confidence.
- Read the full address bar on the landing page. Copy the entire URL into a text editor. Check that every placeholder has been replaced by a value, that no literal macro text survived, and that your ampersands are still ampersands.
- Resolve one ID back inside the platform. Take a single number from your URL, search it in the DSP interface, and confirm it points to the object you think it does. This is the step that catches
${CAMPAIGN_ID}returning a line item, and it is the step everyone skips.
Step four is the whole point. Steps one to three tell you the plumbing works. Only step four tells you the values mean what you assume.
Honest limitation: this catches macro problems, not taxonomy problems. If your line items are named badly, the macro will faithfully deliver a badly named value. Macros solve typing. They do not solve naming, and no tool does.

When I Skip Dynamic UTM Variables Entirely
Not every campaign earns this setup. I skip macros when:
- The campaign is short and small. A two-week flight with three creatives does not need automation. Type the values.
- The platform is the only reporting surface anyone reads. If nobody opens GA4 for this channel, the join work buys nothing.
- The values would be unique per click. Covered above, but it is the most common self-inflicted wound.
- Nobody owns the lookup table. A dynamic setup with no maintainer degrades into numbers nobody can read. A static setup with sloppy names is at least legible.
That last one is a people problem wearing a technical costume, and it is the most common reason I have seen programmatic tracking projects quietly die.
Frequently Asked Questions
What is a dynamic UTM variable?
A dynamic UTM variable is a placeholder written once into an ad platform’s URL field that the platform replaces with a real value at each click. Meta uses {{ad.name}}, Google Ads uses {keyword}, and DSPs such as Display & Video 360 use ${CREATIVE_ID}. The point is that you type the placeholder once and the platform fills the actual value on every click, which removes both manual typing and typos.
Does ${CAMPAIGN_ID} return the campaign ID in DV360?
No. In Display & Video 360, ${CAMPAIGN_ID} returns the line item ID, which Google documents in its macro reference. The substitution succeeds and returns a real number, so nothing looks broken. Display & Video 360 provides no macro that inserts the actual campaign ID into a landing page URL, so that value has to be hardcoded per campaign.
Why does my programmatic traffic show IDs instead of campaign names in GA4?
Because DSP macros return identifiers, not names. Unlike Meta, Display & Video 360 offers no name macros for landing page URLs, so utm_content and similar fields fill with numbers. Fix it by joining the IDs to a name lookup exported from the DSP, or by hardcoding the values that stay stable for a campaign’s whole life.
Do DSP macros work on YouTube ads?
Generally no. Google documents that macros are not supported in YouTube ads or data-driven creatives, with only limited support in Studio dynamic creatives. Click tracking macros also do not work with creatives hosted by Display & Video 360 itself. Test a live click before assuming your tagging carries across formats.
Are DSP macros the same as Google Ads ValueTrack parameters?
No, they are separate systems with different syntax and different availability. ValueTrack uses single curly braces such as {creative} and belongs to Google Ads. DSP macros use ${MACRO_NAME} and belong to the demand-side platform. A template written for one will not resolve in the other, and mixing them is a common cause of literal placeholder text appearing in reports.
Should I use a macro or hardcode the value?
Hardcode anything that stays constant for the life of the campaign, and use a macro only where the value genuinely changes per click. Campaign name is stable, so type it once. Creative identifiers change constantly across a flight, so automate those. The wrong instinct is treating dynamic as automatically better, which is how people end up with unreadable reports and a lookup table nobody maintains.
Start With One Tested Click
Programmatic tracking is not harder than social tracking. It is just less forgiving, because the values arrive as numbers and wrong numbers look exactly like right numbers.
If you do one thing from this post, do step four of the verification method. Take a single ID out of a live URL and resolve it inside the platform. That one check is what separates a report you can trust from a quarter of confident analysis pointed at the wrong level of the hierarchy.
When you are ready to standardise this across platforms instead of rebuilding it per campaign, dynamic variables in linkutm let you keep one tagging template and fill the values automatically.