Most operators ship Meta CAPI first because Meta talks about it loudest. Google quietly built the same kind of system under a different name and shipped it as Enhanced Conversions. If you already have the server-side Meta build working, the Google parallel takes a fraction of the time, but only if you map the schema differences correctly the first time. This tutorial walks the schema, the GTM server-container pattern that fires both ad platforms from the same purchase event, where the two diverge in their dedup model, and how to decide which one to ship first based on your paid-spend ratio.
What this tutorial covers
You will end up with a clear map of how Google Enhanced Conversions CAPI mirrors Meta CAPI, how the field names and hash formats differ, and how to wire both through a single GTM server container so a Shopify Purchase event fans out to both ad platforms. The tutorial also lays out a decision rule for which one to ship first based on your spend mix.
I built the parallel-platform pattern during a Q2 2024 Shopify DTC engagement where the brand was running roughly 60 percent Meta and 40 percent Google. The Meta CAPI rebuild took the first 48 hours. The Google Enhanced Conversions wiring was a one-day add on top of the same GTM server container. Match rate in Google Ads stabilized at 78 percent inside two weeks, which gave Smart Bidding enough signal to actually optimize.
Prerequisites
Five things to have in place before this tutorial helps you.
A working server-side Meta CAPI implementation. If you do not have one, the schema-by-schema breakdown of Meta CAPI fields covers the build. The Google build sits on the same plumbing.
A GTM server container deployed somewhere reachable. Stape, Google Cloud Run, or your own host. The endpoint must answer from your storefront with low latency and from your origin server for server-to-server fires.
A Google Ads account with at least one conversion action configured (typically Purchase) and Enhanced Conversions enabled at the conversion level. Open the action in Google Ads, scroll to Enhanced conversions, switch it on, pick "API" as the source.
A Shopify store (or comparable storefront) emitting purchase events through your existing server pipeline. The example code below assumes a Shopify webhook on orders/create, but the shape is the same for BigCommerce, WooCommerce, or a custom storefront.
A way to capture and persist gclid from the landing page through to checkout. Without this, your Google match rate caps around 50 percent regardless of how clean the user-data block is.

Step 1: Map the schema differences
Both platforms accept a hashed user-identity block on the server-side conversion event. The fields are similar in spirit and meaningfully different in shape. Side by side:
| Concept | Meta CAPI field | Google Enhanced Conversions field |
|---|---|---|
em (SHA-256, lowercase, trimmed) | email_address (SHA-256, lowercase, trimmed) | |
| Phone | ph (SHA-256, E.164 digits with leading +) | phone_number (SHA-256, E.164 digits with leading +) |
| First name | fn (SHA-256, lowercase) | address.first_name (SHA-256, lowercase) |
| Last name | ln (SHA-256, lowercase) | address.last_name (SHA-256, lowercase) |
| Street address | not transmitted as a hashed field | address.street (SHA-256, lowercase) |
| City | ct (SHA-256, lowercase, alphanumeric only) | address.city (SHA-256, lowercase) |
| Region / state | st (SHA-256, lowercase, alphanumeric only) | address.region (SHA-256, lowercase) |
| Postal code | zp (SHA-256, first 5 digits for US) | address.postal_code (SHA-256, full code) |
| Country | country (SHA-256, two-letter ISO) | address.country (two-letter ISO, NOT hashed) |
| Click identifier | fbc (raw cookie value) | gclid (raw, on the conversion event) |
| Browser identifier | fbp (raw cookie) | none directly; gclid covers attribution |
| Customer key | external_id (SHA-256 of customer id) | not used; identity is derived from email and phone |
Three differences matter most for getting the build right. Google wants a structured address object instead of flat city, state, and zip fields. The country code is passed raw (two-letter ISO), not hashed. And there's no counterpart to external_id on the Google side, so the customer-key trick that lifts Meta match quality has no Google equivalent. Plan accordingly.
Step 2: Wire Google Enhanced Conversions through your existing GTM server container
The whole point of running a server container is that adding a second ad platform is mostly tag configuration, not a new pipeline. Your storefront already POSTs purchase events to the server container's collection endpoint. You add one more tag on the server side that subscribes to the same event and dispatches to Google.
In your GTM server container, create a new tag using the Google Ads Conversion Tracking template. Configure four things:
Tag type: Google Ads Conversion Tracking
Conversion ID: AW-1234567890 (from Google Ads → Conversions)
Conversion Label: AbCdEfGhIjKl-MnO (the conversion-specific label)
Currency Code: {{Event Data - currency}}
Conversion Value: {{Event Data - value}}
Order ID: {{Event Data - transaction_id}}
gclid: {{Event Data - user_data.gclid}}
User Provided Data: {{Variable - GA4 user_data block}}
Set the trigger to "Custom" and match on the same event name your Meta CAPI tag triggers on (typically purchase if you are using the GA4 client to receive events, or your own Purchase if you are using a custom client).
The whole tag is roughly five minutes of configuration. The work that matters lives in the variable references: the user_data block has to carry the right fields in the shape Google expects, which is the Step 3 problem.

Step 3: Configure user-provided data the right way
If you followed the per-event hashing tutorial for Meta, you already have a buildUserData function that normalizes raw inputs and SHA-256 hashes them. The Google version is a sibling function that consumes the same input shape and emits the Google field names and structure.
Here is the pattern. Keep your normalizers shared (one source of truth for "lowercase and trim email"). Add a buildGoogleUserData next to the Meta buildUserData.
// src/lib/capi/google-user-data.ts
import { hashField } from "./hash";
type UserDataInput = {
email?: string | null;
phone?: string | null;
firstName?: string | null;
lastName?: string | null;
street?: string | null;
city?: string | null;
region?: string | null;
postalCode?: string | null;
country?: string | null;
gclid?: string | null;
};
export function buildGoogleUserData(input: UserDataInput) {
const address = {
first_name: hashField("name", input.firstName),
last_name: hashField("name", input.lastName),
street: hashField("alphaNum", input.street),
city: hashField("alphaNum", input.city),
region: hashField("alphaNum", input.region),
postal_code: hashField("alphaNum", input.postalCode),
country: input.country?.toUpperCase() ?? undefined, // NOT hashed
};
return {
email_address: hashField("email", input.email),
phone_number: hashField("phone", input.phone),
address,
gclid: input.gclid ?? undefined,
};
}
Two things to watch in this code.
The country field at the bottom of the address block is the only field in the Google payload that goes through unhashed. Hashing it sends a value that never matches Google's records. I have audited stores where every other field was correct and address.country was being hashed, which silently dropped the address-block contribution to match rate.
The street and city and region and postal_code go through the same alphanumeric normalizer used for Meta's ct and st and zp fields. The normalization rule is "lowercase, strip non-alphanumeric, hash." The same primitive works for both platforms.
When the server container fires the Google Ads tag, the user_data block from this function lands in the conversion event payload and Enhanced Conversions does the matching on Google's side.

Step 4: The dedup story differs
Meta CAPI uses an event_id shared between the browser pixel and the server fire. Both sides send the same hash for the same logical event. Meta deduplicates inside its ingest pipeline. Without the shared key, Meta double-counts for a day or two before its own dedup catches up, and your ROAS chart inverts.
Google does not work that way.
Google deduplicates Enhanced Conversions against the gtag pixel by transaction_id. If your gtag fires a Purchase with transaction_id: shop-order-4821 and your server also fires a Google Ads Conversion with the same transaction_id, Google takes the Enhanced Conversions data as authoritative and ignores the gtag fire for matching purposes. If your transaction_id is missing or different across the two fires, Google counts both and your conversion totals look 30 to 50 percent inflated.
The other piece is gclid. Google attribution for paid clicks runs through the click identifier, not through cookies the way Meta uses fbp. Capture gclid at landing, store it in a first-party cookie that survives the checkout flow, read it back on the order confirmation page, and pass it through to the server-side conversion event.
// src/lib/capi/gclid.ts
const COOKIE = "_dtc_gclid";
const TTL_DAYS = 90;
export function captureGclidFromUrl(req: Request): string | undefined {
const url = new URL(req.url);
const gclid = url.searchParams.get("gclid") ?? undefined;
return gclid?.trim() || undefined;
}
export function persistGclid(res: Response, gclid: string): void {
const expires = new Date(Date.now() + TTL_DAYS * 24 * 3600 * 1000);
res.headers.append(
"Set-Cookie",
`${COOKIE}=${gclid}; Path=/; Expires=${expires.toUTCString()}; HttpOnly; Secure; SameSite=Lax`,
);
}
export function readPersistedGclid(req: Request): string | undefined {
const cookie = req.headers.get("cookie") ?? "";
const match = cookie.match(new RegExp(`${COOKIE}=([^;]+)`));
return match ? decodeURIComponent(match[1]) : undefined;
}
Wire captureGclidFromUrl and persistGclid into the landing-page route handler, and readPersistedGclid into the order-creation handler that builds the conversion event. A 90-day TTL aligns with Google's standard click-attribution window and gives most carts time to convert. Once that pipe is intact, you have deterministic identifiers for the majority of paid traffic and Enhanced Conversions does the work for the rest.
Step 5: Modeled vs deterministic, and what that means for trust
Both ad platforms report a quality metric that tells you how well your server data is matching their records. Meta calls it match quality, scored 1 to 10. Google calls it match rate, reported as a percentage. The numbers are not directly comparable but they live in the same trust dial.
Meta's model is mostly deterministic. A hashed email matches a Facebook account, or it does not. The match quality score reflects how many fields you sent versus how many were usable. The Meta CAPI match quality scoring audit walks the per-field contribution.
Google's model is hybrid. Deterministic match happens when a hashed email or phone number matches a Google account on file. Modeled conversions happen when a deterministic match fails but the click came through a gclid-tagged ad and Google has enough signal to estimate that a conversion occurred. The match rate metric tells you how much of your reported volume is deterministic versus modeled.
A healthy Google Enhanced Conversions setup typically sits at 65 to 85 percent match rate. Above 85, you are sending an unusually clean user-data block (good for you). Below 50, something is broken. Common culprits: hashed country, missing gclid on most events, a bad address.region normalizer.
“The two systems share an event_id-equivalent only structurally. Meta dedups by a hash of order_id and event_name. Google dedups by transaction_id alone. If your team treats them as the same key, you ship one system fully and one system half-broken.
”
The trust shift to manage with the marketing team is around Google's modeled half. When they see "Conversions: 1,200, of which 40 percent modeled" in Google Ads, they will ask whether the modeled half is real. The answer is "yes, it is statistically real, but it is not the same kind of fact as the deterministic half." Track them separately in your warehouse. Use deterministic-only for ROAS reporting if the leadership team needs the conservative number; use the full reported number for bid optimization, since that is what Google's algorithm is acting on.
Step 6: When to ship Google Enhanced Conversions before or after Meta CAPI
The default assumption I see in DTC is "ship Meta first because everyone talks about Meta." That is right when Meta is your biggest channel. If Google is bigger, ship Google first.
Three rules of thumb from auditing about a dozen DTC stacks:
If Meta spend is more than 60 percent of paid, ship Meta CAPI first. Most DTC brands are here. Meta CAPI takes about two days for the first build (one day for the GTM server container, one day for event tags and dedup wiring). Google Enhanced Conversions on top of the same infrastructure adds half a day to a day.
If Google spend is more than 60 percent of paid, ship Google Enhanced Conversions first. Lead-gen brands, B2B-adjacent DTC, and operators running heavy YouTube prospecting fall here. The build sequence is the same; only the order changes.
If both are within 20 percent of each other, ship them in the same sprint. Configure the GTM server container, add both ad-platform tags, test both with their respective Test Events tools, enable both. The marginal cost of doing Google immediately after Meta is small and the infrastructure context is fresh.
One footnote. If you serve a regulated category (alcohol, gambling, healthcare, financial services), Google's policy reviews are stricter and Enhanced Conversions setup may take an extra week for conversion-action approval. Do not assume same-sprint shipping in those categories without checking approval status first.

Common mistakes
Six things I see fail in production builds.
Sending the Meta em value to Google's email_address field with the wrong normalization. Meta and Google both want lowercase trimmed email hashed with SHA-256. They are compatible by accident, not by contract. If you change normalization for one platform, change it for both at the same time, or document the divergence loudly.
Hashing address.country. Google takes the two-letter ISO code raw. Hashing it sends a value that never matches and the address block contributes nothing.
Treating Enhanced Conversions as one tag. It is two flows. The Google Ads Conversion tag fires the conversion. The GA4 server-side event is a separate stream that feeds GA4 reporting. If you are running GA4 server-side, the user-data block on GA4 events also matters, and its field names match the Google Ads side. Build them as a pair.
Not capturing gclid early enough. Visitors who bounce and come back without the click ID lose the deterministic match. Capture on the first paid landing, persist for 90 days, read back at conversion.
Skipping the transaction_id on server fires. Without it, Google double-counts against the gtag pixel for the entire dedup window. Every server-side conversion event must carry the order ID.
Logging the raw user-data block. This is the same trap as Meta. The raw email or phone or address ends up in Datadog or Sentry through a debug log or a JSON-stringified error message. Route Google through the same log-hygiene pipeline you built for Meta. The whole point of routing through buildUserData and buildGoogleUserData is that they do not log the input.
What to try next
Wire observability around the match-rate metric. Pull match rate from the Google Ads API daily into your warehouse alongside Meta's match quality. Both numbers should be inside a control band for your store's profile. When either drops outside the band, the alert is the same shape: "your server-side conversion data is degrading and your bidding algorithms are losing signal."
Do the same audit for Consent Mode v2 and CAPI behavior. Google's Consent Mode v2 interacts with Enhanced Conversions in subtle ways: when consent is denied, the conversion still fires for measurement but not for personalization, and the modeled-conversion share grows. Your match-rate metric will move. Do not panic.
If you want a third-party scan that checks both platforms (event_id dedup, transaction_id presence, hash-format correctness, gclid persistence, log-hygiene leaks), the CAPI Leak Report covers 14 checks across Meta and Google. It is the scan I built after seeing the same failure modes on the Q2 2024 Shopify rebuild and on every audit since.
Can I use the same hashed email for Meta and Google?
Yes if you use the same normalization (lowercase, trim, SHA-256). The hash itself is interchangeable. The fields it lands in differ. Meta wants em. Google wants email_address. Same value, two field names.
Do I need a separate GTM server container for Google?
No. One server container handles both. You add a second tag inside the same container that subscribes to the same event and dispatches to Google Ads. The container itself is platform-agnostic.
Why does Google not hash the country code?
Two-letter ISO codes are a small, fixed set (about 250 values). Hashing them is reversible by lookup, so hashing adds no privacy benefit and Google does not match against a hashed country. Send it raw, uppercase.
What is a good Google match rate to aim for?
65 to 85 percent for most DTC brands. Below 50 percent suggests a structural problem (missing gclid, wrong field names, hashed country). Above 85 percent is unusually good and usually means you are sending an exceptionally clean user-data block; lead-gen brands with high authenticated traffic often see this.
Does Enhanced Conversions replace the gtag conversion pixel?
No. They run in parallel. Enhanced Conversions enriches the conversion event with hashed user data so Google can match it deterministically. The gtag still fires from the browser. Dedup happens on Google's side using transaction_id.
What happens to modeled conversions when Consent Mode v2 is denied?
The conversion event still fires for measurement, but user data is not sent for matching. The modeled-conversion share goes up because Google has less deterministic signal and falls back on its statistical model. You will see your match rate dip and your modeled share rise; this is the expected behavior, not a bug.
Sources and specifics
- The parallel-platform pattern was production-tested on the Q2 2024 Shopify DTC rebuild documented in the tracking gap engagement, where Meta CAPI and Google Enhanced Conversions both ran through one Stape-hosted GTM server container.
- Match-rate ranges (65 to 85 percent typical, below 50 percent broken) are based on roughly a dozen DTC stack audits between 2024 and 2026; they are not a published Google benchmark.
- Google Enhanced Conversions field names (
email_address,phone_number,address.first_name, etc.) match the API spec published in the Google Ads Conversion Tracking documentation as of April 2026. - The country-code hashing exception (raw two-letter ISO instead of SHA-256) is documented in Google's Enhanced Conversions for the Web API reference.
- The 90-day
gclidpersistence window aligns with Google's default click-attribution window and is what most DTC carts need to capture late conversions.
