Enhanced Conversions for Google Ads: Setup Without the Pitfalls
First-party data hashing, the GTM configuration, and the three mistakes that make enhanced conversions silently underperform.
Enhanced conversions is one of the few tagging changes that costs an afternoon and reliably pays back. It's also one of the easiest to configure in a way that looks correct in every interface while doing almost nothing. The tag fires. GTM Preview shows green. Google Ads reports conversions. And the match rate sits near zero because a phone number arrived with a space in it, or the email field was read after the browser had already navigated away.
What follows is the setup that works, the three failure modes that account for most of the silent underperformance we see in audits, and how to verify it's actually running before telling anyone it's live.
What Enhanced Conversions Actually Send
When a conversion happens, the Google tag collects first-party identifiers you already hold about that person: email address, phone number, and optionally name and postal address. It hashes them with SHA-256 in the browser before anything leaves the page, then sends the hashes alongside the normal conversion ping. Google compares those hashes against hashed identifiers from signed-in Google accounts. When there's a match, the conversion can be attributed to an ad interaction that the cookie no longer remembers.
That last part is the whole point. Safari's ITP caps script-written first-party cookies at seven days. Third-party cookies are gone or going in every major browser. Somebody clicks your ad on their phone on Tuesday and buys on a laptop on Sunday, and the click-to-conversion link simply doesn't exist. Enhanced conversions rebuilds it from an identifier the user gave you directly.
Worth being precise about what it isn't. It doesn't invent conversions. The orders were always happening and always in your backend. What changes is how many of them Google can attribute back to a click, which changes what Smart Bidding believes about which keywords and audiences are worth money. The reported conversion count going up is a symptom. The bidding getting better is the outcome.
Prerequisites You Cannot Skip
Two things happen in the Google Ads interface before GTM matters at all, and skipping either means everything downstream is decorative.
First, accept the customer data terms. In Google Ads, go to Tools, then Data manager, or reach it through Goals, then Conversions, then Settings, and open the customer data terms panel. Until somebody with admin rights accepts them, enhanced conversion data is discarded on arrival. No warning appears in GTM.
Second, turn enhanced conversions on for the specific conversion action, not for the account. Goals, then Conversions, then Summary, click into the conversion action, open Settings, and expand Enhanced conversions. Tick it on and choose Google Tag Manager as the setup method. Choosing "Google tag" or "API" instead will look fine and quietly ignore what GTM sends.
You also need a lawful basis and a privacy policy that says you share hashed customer data with Google for measurement. Enhanced conversions is a personal data transfer, and it belongs in your privacy notice before it goes live.
Building the User-Provided Data Variable
In GTM, go to Variables, then New, and pick User-Provided Data as the variable type. You get two modes.
Automatic collection asks Google's tag to scan the rendered page for anything that looks like an email or phone field. It's a reasonable fallback for a site you can't change. It's also guessing, and it fails exactly where it matters most, which is a confirmation page that no longer contains the form.
Manual configuration is the one to use. You map each field to a dataLayer variable or a CSS selector. Point Email and Phone Number at dataLayer variables, and fill in the address fields if you have them reliably. More identifiers means a higher chance of a match, but a wrong identifier is worse than a missing one, so only send what you trust.
The cleanest source is a server-rendered dataLayer push on the confirmation page, written by whoever renders the order confirmation. They already have the customer record in hand:
<script>
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
event: 'purchase',
ecommerce: {
transaction_id: 'A-10482',
value: 149.00,
currency: 'GBP'
},
user_data: {
email: 'buyer@example.com',
phone_number: '+447700900123',
address: {
first_name: 'Priya',
last_name: 'Nair',
street: '14 Bath Road',
city: 'Bristol',
postal_code: 'BS1 4TR',
country: 'GB'
}
}
});
</script>
Send raw values. The Google tag normalizes and hashes them for you, and it does the normalization correctly, which is more than most hand-rolled implementations manage. Only pre-hash if you have a hard requirement to keep plaintext out of the page, and if you do, you must normalize to Google's rules first and use the hashed field names, because a hash of " Buyer@Example.com " matches nothing.
Finally, open your Google Ads Conversion Tracking tag, tick "Include user-provided data from your website", and select the variable you just built. If you're on the newer Google tag setup, the user data goes in the tag configuration instead.
Capturing Data Before the Page Changes
This is the most common failure, and it's structural rather than a typo. The email input lives on /checkout. The conversion tag fires on /order-confirmation. By the time the tag runs, the input element does not exist. Automatic collection finds nothing, manual CSS selectors resolve to undefined, and the conversion goes out with no user data attached.
Single-page apps have the same problem wearing a different hat. There's no page load, but the checkout component unmounts on the route transition and the field is gone from the DOM just as thoroughly.
The right fix is server-side rendering of user_data into the confirmation page, as above. Your backend knows who bought. Ask for it. When engineering can't ship that this quarter, capture on submit and hand off through session storage:
// checkout page: capture at submit time, before navigation
document.querySelector('#checkout-form').addEventListener('submit', function () {
var el = document.querySelector('#email');
var email = el && el.value.trim().toLowerCase();
if (email) sessionStorage.setItem('ec_email', email);
});
// confirmation page: hand off, then clear
(function () {
var stored = sessionStorage.getItem('ec_email');
if (!stored) return;
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({ event: 'ec_data_ready', user_data: { email: stored } });
sessionStorage.removeItem('ec_email');
})();
Ordering matters more than the code does. The ec_data_ready push must land before the conversion tag fires, or the variable resolves to undefined when it's read. The safe pattern is to make the conversion tag's trigger depend on a custom event that only fires once user data is in the dataLayer, or to set tag sequencing so the data push is the setup tag.
Use sessionStorage, not localStorage, and remove the key after reading it. You're holding a customer's email in the browser for the length of one checkout, which is defensible. Leaving it there for months is not.
Formatting Phone Numbers as E.164
Phone numbers must be E.164: a plus sign, the country code, then the national number with no spaces, hyphens, brackets, or leading trunk zero. +442079460958 matches. (020) 7946 0958, 020 7946 0958, and 07700 900123 all match nothing at all.
Almost every CRM and checkout form stores local format, because that's what people type. So the transform has to happen somewhere. Best case, the backend does it before the dataLayer push. When you can't touch the backend, a Custom JavaScript variable in GTM works:
function () {
var raw = {{DLV - user_data.phone_number}};
if (!raw) return undefined;
var digits = String(raw).replace(/[^0-9]/g, '');
if (!digits) return undefined;
// Single-market site, default region GB.
if (digits.charAt(0) === '0') digits = '44' + digits.slice(1);
if (digits.slice(0, 2) !== '44') return undefined; // don't guess
if (digits.length !== 12) return undefined; // 44 + 10 national digits
return '+' + digits;
}
Note what it does when it isn't sure: it returns undefined. That's deliberate. A malformed number lowers your match rate, and a confidently wrong number can match a different person entirely. Sending nothing is strictly better than sending a guess.
If you operate across markets, don't extend this with a chain of country guesses. Store the country code alongside the number at capture time, or use a proper phone parsing library on the server. Region inference from number length is the kind of clever that breaks silently in one country and nobody finds out for a year.
Email needs less work, since Google handles lowercasing, trimming, and Gmail dot removal. Trim and lowercase anyway. It rules out a whole class of question.
Respecting the ad_user_data Signal
Consent Mode v2 added ad_user_data and ad_personalization alongside the original ad_storage and analytics_storage. Enhanced conversions data is gated on ad_user_data, and this catches people out constantly.
The failure looks like this. The CMP maps its marketing category to ad_storage, which is what it did before v2, and leaves ad_user_data at its denied default forever. The user accepts everything. The conversion tag fires normally. The user-provided data payload is stripped before transmission. Nothing in GTM Preview shows an error, because nothing errored. You just get a conversion with no identifiers attached, every single time.
Check it in two places. In GTM Preview, select the conversion event and open the Consent tab, and confirm ad_user_data reads granted after acceptance. Then check the wire: in the Network panel, find the request to googleadservices.com/pagead/conversion and look at the gcs parameter. G111 means storage granted. The gcd parameter encodes all four signals, and it's the one that tells you whether ad_user_data specifically came through.
To be clear, the fix is not to bypass consent. If the user denied, you should not be sending identifiers, and enhanced conversions doesn't give you a way around that. The fix is making granted actually mean granted, which is a CMP mapping change rather than a GTM change.
Reading Match Rate in Diagnostics
Go to Goals, then Conversions, then Summary, click your conversion action, and open the Diagnostics tab. Give it a day or two before judging anything.
| What you see | Likely cause | Where to look first |
|---|---|---|
| Status never leaves "Needs attention" | Customer data terms not accepted | Tools, then Data manager |
| Coverage near zero, tag fires fine | User data captured after navigation | GTM Preview, the variable value on the conversion event |
| Coverage healthy, match rate very low | Unformatted phone or stale email field | Raw value of the phone variable |
| Everything looks right, no enhanced data recorded | ad_user_data denied by the CMP | Consent tab in Preview, then the gcd parameter |
| Warning about unformatted data | Whitespace, mixed case, or plus-sign-free numbers | The dataLayer push itself |
Two numbers matter. Coverage is the share of your conversions that arrived carrying a user-provided data payload. That one is entirely in your control, and it should land close to the share of converters who actually give you an email. Match rate is the share of those that Google could tie to an account, and it depends on your audience: consumer sites with personal Gmail addresses land higher than B2B products where everyone uses a work domain.
Judge match rate against your own account over time rather than a number somebody published in a case study. What you care about is whether it moved after a release, not whether it beats an average you can't verify.
What to Expect Over Six Weeks
Diagnostics starts showing data within a day or two of a correct setup. That's your confirmation the plumbing works, and it's the only fast feedback you get.
Conversion restatement takes longer. Google backfills matched conversions into the click dates they belong to, so numbers for last week can change this week, and reports you exported on day three won't reconcile with the same report on day fourteen. That's expected, not a bug, and it makes daily comparisons worthless for the first few weeks.
Bidding takes longest. Smart Bidding needs a few weeks of the new signal before its model reflects the improved attribution, and the adjustment shows up as shifted spend rather than as a headline number. Do not change bid strategy, target CPA, or budget in the same week you turn enhanced conversions on. If you do, you'll have two variables moving at once and no way to tell which one did what.
A reasonable rhythm: check diagnostics on day two, leave everything alone for three weeks, then compare a clean three week window before and after against the same period in your backend. The metric worth watching is the gap between backend orders and attributed conversions, because closing that gap is the entire job.
A Short Verification Routine
Site releases break tagging, and enhanced conversions breaks quietly. Keep a five-step check and rerun it after anything that touches checkout, the CMP, or the confirmation page.
- Complete a real transaction in GTM Preview and read the actual resolved value of the user-provided data variable on the conversion event, not just whether the tag fired.
- Confirm the phone value starts with a plus sign and contains only digits after it.
- Confirm
ad_user_datais granted in the Consent tab after accepting the banner. - Confirm the conversion request carries a transaction ID, so refreshes of the confirmation page don't double count.
- Check Diagnostics coverage two days later and compare it to what it read before the release.
None of that takes fifteen minutes, and it catches the regression that would otherwise sit there for a quarter while every dashboard reports success.