UTM parameters are tags you append to a landing page URL so your analytics platform can attribute a visit to a specific ad instead of dumping it into “direct” or “referral.” Each ad platform has one correct field for them: Google Ads uses the Final URL suffix, Meta uses the URL parameters field at the ad level, and TikTok uses Build URL parameters inside Ad details. All three support dynamic tokens that fill in campaign, ad group, and ad names at click time, so you write the string once instead of hand-tagging every ad. Getting those values onto a form submission is a separate job with its own failure mode: Gravity Forms and Ninja Forms both read the query string of the page the form sits on, which works when the ad points straight at that page and quietly returns nothing when it does not.

This guide covers all four pieces: the taxonomy, the three ad platforms, the form capture, and the code that fixes the gap between them.

The five UTM parameters, and what each one is actually for

The UTM parameter convention is old, stable, and universally understood. Anything that can parse a query string can read it: GA4, your CRM, your data warehouse, your form plugin.

Parameter Answers Typical value
utm_source Which platform sent this click? google, facebook, tiktok
utm_medium What kind of traffic is it? cpc, paid_social
utm_campaign Which campaign? q3_2026_wordpress_leads
utm_content Which creative or ad? video_testimonial_30s_v2
utm_term Which keyword, or which ad group? custom+wordpress+development

There is a sixth worth knowing: utm_id. GA4 uses it as the join key when you import cost data, so if you ever plan to compare spend against revenue inside GA4 rather than in a spreadsheet, set it to the platform’s campaign ID from the start. Retrofitting it later means a gap in your history.

Decide your taxonomy before you touch an ad account

Every tracking problem we get called in to fix traces back to the same root cause: three people tagged the same concept three different ways. Facebook, facebook, and fb are three sources in GA4. paid_social and paid-social are two mediums. The reports are not wrong; they are faithfully reporting an inconsistent input.

Four rules, applied everywhere, with no exceptions:

  1. Lowercase everything. UTM values are case-sensitive in most analytics platforms. This single rule prevents the majority of split-row problems.
  2. Underscores, never spaces. A space becomes %20 and your campaign names become unreadable.
  3. One value per concept, written down. Keep the list in a shared document. If it is not on the list, it does not go in an ad.
  4. Never use utm_medium=social for a paid ad. This one deserves its own explanation.

The utm_medium rule that costs agencies real money

GA4’s default channel grouping sorts a session into Paid Social only when the source matches its list of social sites and the medium matches the regular expression ^(.*cp.*|ppc|retargeting|paid.*)$. The value social does not match that pattern. It does match GA4’s Organic Social rule, which evaluates the medium on its own.

So if you tag a Meta ad with utm_source=facebook&utm_medium=social, GA4 files your paid clicks as organic social traffic. The spend is real, the leads are real, and the channel report says you got them for free. Your reported return on ad spend is understated and your paid social channel looks like it is doing nothing.

Values that work: cpc, cpm, ppc, paid_social, paid-social, retargeting. Values that fail: social, meta, display, paid social with a space.

Our house convention: cpc for Google Ads, paid_social for Meta and TikTok. Both match the regex, and the split keeps paid search and paid social legible at a glance in the channel report. Pick one pair, write it down, and never negotiate it again.

How to add UTM parameters to Google Ads

Google Ads is the platform where people most often get told they do not need UTMs at all. That advice is half right, and the half that is wrong is expensive.

Auto-tagging and UTMs are not in conflict

Google Ads appends a gclid (Google Click Identifier) to your landing page URL automatically when auto-tagging is on. That click ID is what powers the GA4 integration, offline conversion import, and Smart Bidding signal. Leave it on. Turning auto-tagging off to “clean up the URL” costs you conversion import and degrades your bidding.

The catch is that gclid is an opaque string only Google products can read. Your CRM cannot parse it. Your data warehouse cannot join on it. If you want a human-readable record of which campaign produced a lead sitting in your form entries, you need UTMs alongside the click ID. The two coexist.

Use the Final URL suffix, not the Final URL

Google gives you two fields under URL options, and the difference matters:

  • Final URL suffix appends parameters to your landing page URL. Google handles the ? or & separator for you. This is where UTMs belong.
  • Tracking template is for routing clicks through a third-party tracking server before the landing page. You only need it if you actually have one.

Set the suffix at the account level and it applies everywhere, including campaigns you have not created yet. The field is available at account, campaign, ad group, ad, dynamic ads target, keyword, and sitelink levels; lower levels override higher ones. Account level is the default answer for most businesses.

Go to Admin, then Account settings, then Tracking template, and enter this in the Final URL suffix field:

utm_source=google&utm_medium=cpc&utm_campaign={campaignid}&utm_id={campaignid}&utm_content={creative}&utm_term={keyword}

Those braces are ValueTrack parameters. Google substitutes real values at click time. {campaignid}, {adgroupid}, {creative}, {keyword}, {matchtype}, and {device} are the common ones. Note that {keyword} comes back empty on Performance Max and Dynamic Search Ads, where {searchterm} is the usable alternative.

If you want readable campaign names rather than IDs in your reports, define a custom parameter at the campaign level (for example {_cn}=raleigh_wordpress_q3) and reference it as utm_campaign={_cn}. IDs are stable and never break; names are readable and break the moment someone renames a campaign. Using the ID for utm_id and the custom parameter for utm_campaign gives you both.

The one mistake to avoid

Do not put UTMs in the Final URL and the Final URL suffix. You will get the same key twice in the resulting URL, like utm_campaign=brand&utm_campaign=brand_exact. Most tools read the first value. Some read the last. Some read neither. Pick the suffix and keep the Final URL clean.

Before you spend anything, use the Test button next to the tracking template. Google simulates a click and tells you whether the landing page resolves. If your site runs security plugins, aggressive redirects, or a firewall that strips unknown query parameters, this is where you find out.

How to add UTM parameters to Meta ads

Meta’s tracking lives in the URL parameters field at the ad level, inside the Tracking section. Not the campaign, not the ad set, and not the website URL field.

Enter the query string without a leading question mark. Meta adds the separator itself:

utm_source={{site_source_name}}&utm_medium=paid_social&utm_campaign={{campaign.name}}&utm_id={{campaign.id}}&utm_term={{adset.name}}&utm_content={{ad.name}}

Meta’s documented dynamic parameters:

Token Resolves to
{{campaign.name}} / {{campaign.id}} Campaign name or ID
{{adset.name}} / {{adset.id}} Ad set name or ID
{{ad.name}} / {{ad.id}} Ad name or ID
{{site_source_name}} Platform: fb, ig, an, or msg
{{placement}} Where the ad ran (feed, story, and so on)

Three things that catch people out:

Name-based tokens freeze at publish time. Meta resolves {{campaign.name}} to the name the campaign had when it was published. Rename the campaign later and the parameter keeps reporting the original. This is not a bug you can work around; it is a reason to get naming right before you hit publish, and a reason to carry the ID in utm_id as the stable join key.

The URL parameters field survives duplication. Hardcoding UTMs into the destination URL means every duplicated ad carries a stale string. The dedicated field is the one that behaves.

{{placement}} and {{site_source_name}} can return empty in some Advantage+ configurations, which produces values like utm_source= with nothing after it. If you run Advantage+ Shopping and see empty sources, swap to a static utm_source=facebook and get your placement breakdown from Ads Manager’s own breakdown view instead. A populated field beats an elegant one.

How to add UTM parameters to TikTok ads

TikTok gives you two paths, and the convenient one has a cost.

URL Auto-attach

Enter a destination URL in Ad details and TikTok appends UTM parameters automatically, filling in source (TikTok), medium (“Paid”), plus campaign ID and name. You can toggle it under Edit, below the Preview button.

It is genuinely convenient, and it will not match the taxonomy you use on Google and Meta. You get Paid where your convention says paid_social, and TikTok where your convention says tiktok. If TikTok is your only paid channel, take the convenience. If you are running cross-channel and want one legible report, turn Auto-attach off and tag manually.

Build URL parameters

Enter your Destination URL, click Edit under Preview, then Build URL parameters. Enter each UTM in its field, or paste a full string into the destination URL and let Autodetect find the parameters.

TikTok’s macros use double underscores on both ends. From TikTok’s own documentation:

Macro Expands to
__CAMPAIGN_NAME__ Campaign name
__CAMPAIGN_ID__ Campaign ID
__AID_NAME__ Ad group name
__AID__ Ad group ID
__CID_NAME__ Creative name
__CID__ Creative ID
__PLACEMENT__ Placement type (TikTok, TikTok Pangle)
__ADID_V2_NAME__ Ad name, upgraded Smart+ campaigns only
__ADID_V2__ Ad ID, upgraded Smart+ campaigns only

A working string, aligned to the house convention:

utm_source=tiktok&utm_medium=paid_social&utm_campaign=__CAMPAIGN_NAME__&utm_id=__CAMPAIGN_ID__&utm_term=__AID_NAME__&utm_content=__CID_NAME__

Two TikTok traps worth naming out loud

The macro names do not mean what they look like. TikTok’s hierarchy is Campaign, then Ad group, then Ad. But __AID__ is the ad group ID, not the ad ID, and __CID__ is the creative ID, not the campaign ID. Reach for the one that reads correctly and you will tag the wrong level. The substitution succeeds, the report populates, and nothing flags the error. Read the table, not the abbreviation.

Single underscores fail silently. __CAMPAIGN_ID__ works. _CAMPAIGN_ID_ does not. TikTok cannot match the token, so it passes the literal text through and your analytics receives utm_id=_CAMPAIGN_ID_ as a value. No error, no warning. If you see raw macro text in a landing page URL, this is almost always the cause, and it almost always comes from hand-editing rather than copying the template.

TikTok also appends its own ttclid click identifier, the same way Google appends gclid and Meta appends fbclid. Leave all three alone.

Keeping one taxonomy across all three platforms

Here is what the house convention looks like applied consistently. This is the artifact worth keeping in a shared doc:

Google Ads Meta TikTok
Where Final URL suffix, account level URL parameters, ad level Build URL parameters, ad level
utm_source google {{site_source_name}} tiktok
utm_medium cpc paid_social paid_social
utm_campaign {_cn} or {campaignid} {{campaign.name}} __CAMPAIGN_NAME__
utm_id {campaignid} {{campaign.id}} __CAMPAIGN_ID__
utm_term {keyword} {{adset.name}} __AID_NAME__
utm_content {creative} {{ad.name}} __CID_NAME__
Click ID gclid (automatic) fbclid (automatic) ttclid (automatic)

Because Meta and TikTok pull names directly from what you typed in the ad account, your ad platform naming is your analytics taxonomy. “Copy of Copy of Ad Set 2” is not a naming convention, and it will be sitting in your GA4 reports next quarter.

Getting the query string into a WordPress form

Everything above gets the data to the landing page. Now it has to survive into the lead record, because the question that actually gets asked in the Monday meeting is not “which campaign drove sessions,” it is “which campaign drove that booked call.”

Both Gravity Forms and Ninja Forms can do this natively. Both share the same limitation, so it is worth stating before the instructions rather than after.

The limitation you need to understand first

Native query string population reads the URL of the page the form is on. Nothing else.

If your ad points at /services/web-development/?utm_source=google&utm_medium=cpc and your form lives on that page, this works perfectly. If the visitor lands there, reads for a minute, then clicks through to /schedule-consultation/, the parameters are gone. The internal link does not carry them. The form loads on a clean URL, finds nothing in the query string, and writes empty hidden fields.

This is the number one reason a UTM setup that “should work” produces blank entries. Set up the native method first because it is the base layer, then add the cookie layer below to close the gap.

Gravity Forms

For each parameter you want to capture:

  1. Add a Hidden field to the form.
  2. Label it something you will recognize in the entry, like “UTM Source.”
  3. Open the Advanced tab and check Allow field to be populated dynamically.
  4. In the Parameter Name field that appears, enter utm_source.
  5. Repeat for utm_medium, utm_campaign, utm_content, utm_term, and gclid.

Gravity Forms now reads the query string on load and fills the matching fields. No code required.

Three warnings from Gravity Forms’ own documentation that are easy to skip past:

  • Avoid WordPress reserved terms as parameter names. The most dangerous one is name, which produces a 404 on the form page. The UTM parameters themselves are all safe, but if you extend this pattern to custom parameters, check the reserved list first.
  • Aggressive caching breaks this. Gravity Forms specifically calls out hosts that cache query string parameters. If your fields are empty and everything looks correct, cache is the first suspect. Ours included.
  • Do not use the {all_fields} merge tag in customer-facing notifications. Hidden fields are included in it. Your prospect will receive an email containing your internal campaign naming.

Ninja Forms

Ninja Forms uses a merge tag rather than a checkbox:

  1. Add a Hidden field to the form.
  2. Open the field settings and find the Default Value field.
  3. Click the merge tag selector icon to the right of the field and choose the querystring merge tag. It inserts {querystring:YOUR_KEY}.
  4. Replace YOUR_KEY with the parameter name, so the Default Value reads {querystring:utm_source}.
  5. Repeat for each parameter.

The field label is only visible inside the Ninja Forms builder, so name it for your own sanity.

While you are in the field settings, set the field key under Advanced to match the parameter name (utm_source and so on). The native merge tag does not need it, but the cookie fallback below does.

Closing the cross-page gap

Three moving parts: a script that captures parameters on arrival and stores them, a filter that reads them back at form render, and a decision about caching.

1. Capture on arrival

This runs on every page load. If the URL carries tracking parameters, it writes two first-party cookies: a last-touch cookie that overwrites every time, and a first-touch cookie written once and never touched again. First-touch matters more than most people expect, because the ad that introduced someone to you is rarely the click they converted on.

(function () {
var KEYS = [
'utm_source', 'utm_medium', 'utm_campaign',
'utm_content', 'utm_term', 'utm_id',
'gclid', 'gbraid', 'wbraid', 'fbclid', 'ttclid'
];

var params = new URLSearchParams(window.location.search);
var found = {};
var hasAny = false;

KEYS.forEach(function (key) {
var value = params.get(key);
if (value) {
found[key] = value.slice(0, 200);
hasAny = true;
}
});

if (!hasAny) {
return;
}

var payload = encodeURIComponent(JSON.stringify(found));
var maxAge = 60 * 60 * 24 * 90; // 90 days
var secure = window.location.protocol === 'https:' ? '; Secure' : '';
var base = '; Max-Age=' + maxAge + '; Path=/; SameSite=Lax' + secure;

// Last touch: always overwrite.
document.cookie = 'coa_attr_last=' + payload + base;

// First touch: write once, never overwrite.
if (document.cookie.indexOf('coa_attr_first=') === -1) {
document.cookie = 'coa_attr_first=' + payload + base;
}
})();

The slice(0, 200) is a cheap guard against someone stuffing a novel into your query string and into your cookie header. gbraid and wbraid are Google’s click identifiers for iOS traffic where gclid is unavailable, so capture them alongside it.

2. Read it back at render: Gravity Forms

Gravity Forms exposes a filter named for each parameter. The query string wins when it exists, and the cookie fills in when it does not:

<?php
add_filter( 'gform_field_value_utm_source', 'coa_attr_value' );
add_filter( 'gform_field_value_utm_medium', 'coa_attr_value' );
add_filter( 'gform_field_value_utm_campaign', 'coa_attr_value' );
add_filter( 'gform_field_value_utm_content', 'coa_attr_value' );
add_filter( 'gform_field_value_utm_term', 'coa_attr_value' );
add_filter( 'gform_field_value_gclid', 'coa_attr_value' );

function coa_attr_value( $value ) {
$key = str_replace( 'gform_field_value_', '', current_filter() );

// Query string on this page always wins.
if ( ! empty( $_GET[ $key ] ) ) {
return sanitize_text_field( wp_unslash( $_GET[ $key ] ) );
}

if ( empty( $_COOKIE['coa_attr_last'] ) ) {
return $value;
}

$data = json_decode( wp_unslash( $_COOKIE['coa_attr_last'] ), true );

if ( ! is_array( $data ) || ! isset( $data[ $key ] ) ) {
return $value;
}

return sanitize_text_field( $data[ $key ] );
}

PHP decodes cookie values automatically, so there is no second urldecode here. Adding one would break the JSON.

3. Read it back at render: Ninja Forms

Ninja Forms uses a single filter across all fields, so the callback checks the field key and the field type before doing anything:

<?php
add_filter( 'ninja_forms_render_default_value', 'coa_nf_attr_value', 10, 3 );

function coa_nf_attr_value( $default_value, $field_type, $field_settings ) {
if ( 'hidden' !== $field_type ) {
return $default_value;
}

$allowed = array(
'utm_source', 'utm_medium', 'utm_campaign',
'utm_content', 'utm_term', 'gclid',
);

$key = isset( $field_settings['key'] ) ? $field_settings['key'] : '';

if ( ! in_array( $key, $allowed, true ) ) {
return $default_value;
}

if ( ! empty( $_GET[ $key ] ) ) {
return sanitize_text_field( wp_unslash( $_GET[ $key ] ) );
}

if ( empty( $_COOKIE['coa_attr_last'] ) ) {
return $default_value;
}

$data = json_decode( wp_unslash( $_COOKIE['coa_attr_last'] ), true );

if ( ! is_array( $data ) || ! isset( $data[ $key ] ) ) {
return $default_value;
}

return sanitize_text_field( $data[ $key ] );
}

This is why the field key had to be set to utm_source earlier. The {querystring:utm_source} merge tag can stay in the Default Value as a same-page fallback; this filter takes precedence at render.

4. The caching decision

Both filters run when WordPress renders the page. If a full-page cache serves a stored copy of that HTML, the filters never run, and whatever values were baked in when the cache was built get served to everyone. That is worse than empty fields, because it looks like it is working.

Two ways out, and the right one depends on the site:

  • Exclude form pages from full-page caching. In WP Rocket, add them under Never Cache URLs. Clean and reliable. The cost is that your highest-intent pages lose the cache benefit, which is not nothing on a site that sells page speed.
  • Populate client-side instead. Read the cookie in JavaScript after the form renders and set the hidden input values directly, targeting them by field ID. This works with caching intact but adds a dependency on the form markup, which means it needs a regression check whenever the plugin updates.

We default to the first on lead-generation pages, since a single misattributed lead costs more than 200ms on a contact page. This is exactly the kind of tradeoff that gets decided differently when the same team owns both the paid advertising and the WordPress build, rather than one agency filing a ticket with another.

Before you spend a dollar: the QA pass

Ten minutes here saves a month of unusable data.

  1. Click a real ad in preview. Not a constructed URL. Inspect the address bar after the page loads and confirm your UTMs are present alongside the platform’s click ID.
  2. Look for raw tokens. If you see {{campaign.name}} or _CAMPAIGN_ID_ as literal text, the token did not resolve. On TikTok, check both ends for double underscores.
  3. Check GA4 Realtime. Within 30 minutes, Reports, then Realtime, then Source/Medium. You should see google / cpc or facebook / paid_social. Seeing (direct) / (none) means the parameters never arrived. Seeing facebook / social means your medium is wrong.
  4. Confirm nothing strips your parameters. Security plugins, firewalls, and redirect rules sometimes drop unknown query parameters. Google’s URL test button catches this on Search; on social you have to click through and look.
  5. Submit the form yourself. Land on a tagged URL, click through to a different page, then submit. Open the entry. If the hidden fields are populated, your cookie layer works. If they are empty, it does not, and better you find that than a client.
  6. Submit again from a clean URL. The fields should be empty or hold your fallback. If they hold your test campaign’s values, you have a caching problem.

What to do with the data once you have it

Hidden fields on an entry are a means, not an end. Three things they unlock:

Route the lead by source. Conditional notification rules can flag paid leads for faster follow-up. Speed to lead is worth more on paid traffic than anywhere else, because you are paying for the click either way.

Close the loop with offline conversions. Capture gclid and you can upload the conversion back to Google Ads when the deal actually closes, weeks later. That teaches Smart Bidding what a real customer looks like rather than what a form fill looks like. This is the highest-leverage thing on this list and the one most often skipped.

Report on cost per booked call, not cost per form fill. Once source data is on the lead record, you can join it to whatever happened next in your CRM. That is the number that decides budget, and once you have it, conversion rate optimization stops being guesswork and starts being arithmetic.

None of this works without the taxonomy at the top. Every one of these depends on utm_source meaning the same thing today that it meant last quarter.

Going beyond UTMs: custom query parameters

The five UTM parameters cover where a click came from. They were never meant to carry everything. When you need to track something the UTM vocabulary has no word for, a custom query parameter is the tool: any key/value pair you invent, appended to the URL the same way and read back the same way.

The rule that keeps this clean: UTMs classify the traffic source, custom parameters carry everything else. Do not overload utm_campaign with an A/B variant or a coupon code just because it is the field you already know. Give each idea its own key, or your reports become unreadable six weeks later.

The most common use is marking which version of a page a visitor saw. Testing two landing pages behind the same ad:

https://cityofoaksmarketing.com/offer/?utm_source=google&utm_medium=cpc&utm_campaign=q3_leads&variant=hero_a

Run a second ad with variant=hero_b and every system that reads the query string can split results by variant: your analytics, your form entries, your CRM. The variant key is not special; it is a name you chose, and the mechanics are identical to a UTM. Other cases that earn their own parameter: coupon codes (promo=summer15), audience segments (audience=retargeting_30d), affiliate IDs (partner=acme), and email or SMS message variants (msg=subject_b).

One caveat on A/B testing specifically: if both variants live at the same URL and a testing tool swaps the content, the parameter is redundant because the tool tracks the split itself. The custom-parameter approach earns its place when the variants are genuinely different URLs, or when you want the variant recorded on the lead itself rather than only inside the testing tool’s dashboard. That second case is the one most teams miss.

Capturing them is the same job you just did

Everything earlier in this article already handles custom parameters. The cookie-capture script keys off a list you control, so add yours to it:

var KEYS = [
'utm_source', 'utm_medium', 'utm_campaign',
'utm_content', 'utm_term', 'utm_id',
'gclid', 'gbraid', 'wbraid', 'fbclid', 'ttclid',
'variant', 'promo', 'audience', 'partner', 'msg' // custom
];

In Gravity Forms, add a hidden field, check Allow field to be populated dynamically, and set the Parameter Name to variant. One thing to check first: your parameter name must not collide with a WordPress reserved term. name, page, and author will break the page or get swallowed; variant, promo, audience, and partner are all safe. When in doubt, prefix your own, like coa_variant. In Ninja Forms, add a hidden field, set its Default Value to {querystring:variant}, and add the key to the $allowed list in the filter shown earlier so the cookie fallback covers it too.

One difference from UTMs is worth flagging: GA4 recognizes the five UTM parameters automatically, but it has no idea a parameter you invented means anything. To report on variant inside GA4, you need to register it as a custom dimension through Google Tag Manager. You do not need any of that to get the value onto your form entries, which runs entirely through the query string and the code above, independent of GA4. The GA4 setup is a separate topic we cover in tracking custom parameters and A/B tests in GA4.

Frequently Asked Questions

Do UTM parameters hurt SEO?

Not if your site handles them correctly. Query parameters create URL variants that search engines can discover and crawl, which is why a self-referencing canonical tag on every page matters: it tells Google which version is authoritative. What does cause problems is putting UTM parameters on internal links, which fragments your own analytics and generates crawlable variants for no benefit. Use UTMs on inbound links from ads, email, and external campaigns only. Never on a link from one page of your site to another.

Do I still need UTM parameters if Google Ads auto-tagging is on?

For GA4 alone, no. The gclid handles attribution when auto-tagging is on and the accounts are linked. You need UTMs when anything other than a Google product has to read the data: your CRM, your form entries, a third-party attribution tool, or a data warehouse. You also need them for a consistent naming convention across Google, Meta, and TikTok. Keep auto-tagging on and add UTMs in the Final URL suffix. They coexist without conflict.

Why is my Meta traffic showing up as organic social in GA4?

Almost certainly because utm_medium=social. GA4 assigns Paid Social only when the medium matches the pattern ^(.*cp.*|ppc|retargeting|paid.*)$, and social does not match it. It does match the Organic Social rule, which evaluates the medium on its own, so your paid clicks get filed as free traffic. Change the medium to cpc or paid_social. Note that this does not fix historical data, only traffic from the change forward.

Why are my Gravity Forms hidden fields empty?

The most common cause by far is that the visitor landed on one page and submitted the form on another. Dynamic population reads the query string of the page the form is on, and internal links do not carry UTM parameters, so by the time the form loads there is nothing to read. The fix is a cookie layer that stores the values on arrival and reads them back at render. The second most common cause is page caching serving a stored copy of the HTML, which means the population filter never runs.

Does Ninja Forms capture UTM parameters automatically?

No. You add a hidden field for each parameter and set its Default Value to the querystring merge tag, so {querystring:utm_source} for the source. Ninja Forms then reads that key from the current page’s URL on load. Like Gravity Forms, it only sees the page the form is on, so it needs the same cookie layer to survive a visitor navigating between the landing page and the form.

What is the difference between the Final URL suffix and the tracking template in Google Ads?

The Final URL suffix appends parameters to the end of your landing page URL, and Google handles the separator. The tracking template routes the click through a third-party tracking server first, using {lpurl} as a placeholder for the landing page. If you do not use a third-party click tracker, you do not need a tracking template. UTM parameters belong in the Final URL suffix.

Should utm_medium be cpc or paid_social?

Both work. GA4’s Paid Social pattern matches anything containing “cp” or starting with “paid,” so cpc, cpm, ppc, paid_social, and retargeting all classify correctly. What breaks things is inconsistency: paid_social and paid-social are two separate rows in every report. Pick one value per concept and use it everywhere. Our convention is cpc for Google Ads and paid_social for Meta and TikTok, which keeps paid search and paid social legible at a glance.

How long should I store first-touch attribution data?

Ninety days covers most B2B buying cycles and keeps you inside reasonable expectations for first-party cookie retention. Longer windows capture more of a slow sales cycle but decay in accuracy as people switch devices and clear cookies. Whatever you choose, disclose it in your privacy policy and honor your consent settings. A cookie that records marketing attribution is a marketing cookie, and it should not be written before consent in jurisdictions where consent is required.

Can I add my own custom query parameters beyond UTMs?

Yes. A custom query parameter is any key/value pair you invent, appended and read back exactly like a UTM. Use them for anything the UTM vocabulary has no word for: an A/B variant, a coupon code, an audience segment, or a partner ID. The rule that keeps reports clean is to give each idea its own key rather than overloading a UTM field. Capturing them into a form is the same process as UTMs: add the key to your hidden fields and to the capture script. The one difference is analytics. GA4 recognizes UTMs automatically but not custom parameters, so reporting on a custom key inside GA4 takes an extra setup step through Google Tag Manager, though you do not need that step to get the value onto your form entries.

Getting this right is a build problem, not a reporting problem

Most tracking failures are not analytics failures. They are naming decisions nobody wrote down, a hidden field on the wrong page, and a cache serving yesterday’s HTML. The reports are downstream of all of it, faithfully reporting whatever the plumbing hands them.

That is why we configure conversion tracking before anything launches on every engagement, and why the same team writes the marketing strategy and the code that captures it. When our client TBG Homes committed to that approach, AI-ready architecture and content delivered 200% organic traffic growth in five months, and every bit of it was attributable because the measurement went in first.

If you want to go deeper on the strategy side of paid search, our guide to building an intent-first SEO and Google Ads strategy covers the layer above this one: deciding which clicks are worth tagging in the first place.

Ready for marketing you can measure?

Tell us about your goals and we’ll show you exactly how we’d approach them: which channels, what it costs, and how we’ll prove it’s working. No obligation, no hard sell. If we’re not the right fit, we’ll tell you that too.

Book a Discovery Call →

or call 919-200-0201