Google is now writing purchase events into your GA4 property from Shopify stores without you setting anything up, without you consenting to it, and in many cases without your GTM container even firing. If you run a Shopify store connected to Google & YouTube (the official channel app) and you have a GA4 property linked, there’s a decent chance a second, invisible purchase pipeline is running alongside your own. It looks like a gift. It isn’t. It’s silently colliding with your existing tags, dropping click IDs, ignoring your consent mode configuration, and quietly inflating or deflating revenue depending on how your dedup logic is written.
I’ve audited this on nine merchant properties over the last few months. Every single one had discrepancies between Shopify’s order report and GA4’s purchase count once the silent route was active. The average revenue delta was 6.8%, with the worst offender at 19% inflation because the merchant’s own GTM purchase tag was firing on the thank-you page and Google’s server-side route was firing from the Shopify backend, both writing to the same GA4 stream with different transaction IDs.
This post is the practitioner audit nobody else is writing. If you manage GA4 for a Shopify store, read this before your next monthly revenue reconciliation.
What Google is actually doing
The mechanism sits inside the Google & YouTube channel app on Shopify. When a merchant connects a GA4 property (or Google Ads account) through that app, Google now uses Shopify’s checkout webhooks and its own server-to-server pipeline to send purchase events directly into the linked GA4 measurement ID. No gtag.js call. No GTM tag. No dataLayer push. The event originates from Google’s servers, not the browser.
This is different from the older client-side integration where the Google channel app injected a gtag snippet into the checkout thank-you page. That still happens on some stores. What’s new is the parallel server-side path that runs regardless of whether the client-side snippet fires, whether the customer completed checkout with an ad blocker on, or whether your consent mode has denied analytics_storage.
The intent, from Google’s side, is obvious: reduce measurement loss from ITP, ad blockers, and consent gating so Ads can attribute more conversions. The side effect for anyone with a considered GA4 implementation is that you now have two purchase pipelines, and Google didn’t tell you the second one exists.
How to detect if the silent route is active on your store
There are three reliable signals. Check all three before you conclude anything.
Signal 1: The GA4 DebugView with the browser offline
This is the fastest test. Open your Shopify store in an incognito window, install a test order coupon, and complete a real checkout while watching GA4 DebugView. Then repeat the test with your network tab open and filtered to google-analytics.com and analytics.google.com.
If you see a purchase event land in DebugView but you see zero outbound requests to Google Analytics collect endpoints from the browser, the event came from Google’s server. That’s the silent route.
To make this even cleaner, block the GA collect endpoints at the browser level (uBlock Origin’s advanced mode works) and rerun the test. If the purchase still appears in GA4, it’s server-side.
Signal 2: BigQuery export inspection
If you have BigQuery export enabled on your GA4 property (and you should — our GA4 setup service enables this by default), the silent route leaves fingerprints in the raw event data.
Run this query against your last 7 days of purchase events:
SELECT
event_date,
event_name,
(SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'transaction_id') AS txn_id,
(SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'source') AS param_source,
stream_id,
platform,
device.category,
device.web_info.browser,
user_pseudo_id,
privacy_info.analytics_storage,
COUNT(*) OVER (PARTITION BY
(SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'transaction_id')
) AS txn_event_count
FROM `your-project.analytics_XXXXXXXX.events_*`
WHERE _TABLE_SUFFIX BETWEEN
FORMAT_DATE('%Y%m%d', DATE_SUB(CURRENT_DATE(), INTERVAL 7 DAY))
AND FORMAT_DATE('%Y%m%d', CURRENT_DATE())
AND event_name = 'purchase'
ORDER BY event_date DESC, txn_id;
Look for two tells:
- Rows where
device.web_info.browseris null or empty butplatform = 'WEB'. Server-originated events often have empty browser fields because there’s no user agent to parse. user_pseudo_idvalues that don’t match the format your GTM setup produces, or purchase events where theuser_pseudo_idhas no correspondingsession_startorpage_viewin the same session. That’s a giveaway — a purchase with no preceding session activity in the same client is almost always a server-injected event.
If txn_event_count > 1 for the same transaction_id, you have duplication. More on that in a moment.
Signal 3: The GA4 Realtime “event source” breakdown
In GA4 Realtime, add a secondary dimension of Stream ID and check event_count for purchase. If you see two distinct stream IDs writing purchases, or if the total exceeds what your GTM tag should be producing based on session count, the silent pipeline is contributing.
What Google’s silent route actually sends
Here’s where the practitioner detail matters. Google’s server-side purchase event is not equivalent to a well-instrumented client-side event. It’s a stripped-down subset. The table below is compiled from BigQuery inspection across the properties I audited.
| Parameter | Your GTM purchase event | Google’s silent route |
|---|---|---|
transaction_id | Shopify order name (e.g. #1042) | Shopify order ID (numeric, e.g. 5218473) |
value | Present | Present |
currency | Present | Present |
items[] | Full product array with item_id, item_name, item_brand, item_category, price, quantity | Reduced array, often missing item_brand, custom item params, and any GTM-injected metadata |
gclid / gbraid / wbraid | Captured if present in URL | Frequently missing — no browser context |
client_id (_ga cookie) | Consistent with session | Regenerated server-side, does not match browser session |
session_id | Matches prior page_view session | Often null or synthetic |
page_location | Thank-you page URL | Empty or generic |
user_id (if set) | Present via login hook | Not sent |
| Consent state | Respects your Consent Mode config | Bypasses browser consent state |
Custom dimensions (e.g. customer_type, discount_code) | Present if instrumented | Not sent |
The most damaging omissions are the click IDs and the client ID mismatch. Because Google’s server-side event fires with a synthetic client ID, the purchase does not stitch to the user’s browsing session in GA4. You end up with a “direct / (none)” attribution on a purchase that was actually driven by a Google Ads click, which — ironically — makes Google Ads attribution worse, not better, unless you’re relying on the Google Ads conversion linker separately.
That transaction ID format mismatch is also a landmine. Shopify’s order name (#1042) is what appears in the admin UI and in most Shopify apps. The order ID (5218473) is the internal numeric identifier. If your GTM setup uses one format and Google’s silent route uses the other, deduplication by transaction_id will fail because the two events look like different transactions.
The duplication risk and how to fix it
If both pipelines are active, you have three possible states for any given order:
- Both events fire successfully → duplication (revenue inflated)
- Only the GTM event fires → normal
- Only Google’s server event fires → normal count, but attribution stripped
State 1 is the loud problem. State 3 is the quiet one. Both need fixing.
Fixing duplication by normalising transaction ID
The cleanest fix is to force both pipelines to use the same transaction_id format. You cannot modify what Google’s server-side route sends, but you can modify your GTM tag to match it. The Shopify order object exposes both the name and the ID in Liquid; use the ID.
If your GTM setup pulls the transaction ID from the dataLayer on the thank-you page, update the Additional Scripts field in Shopify’s checkout settings (Settings → Checkout → Order status page → Additional scripts) to push the numeric ID:
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
event: 'purchase',
ecommerce: {
transaction_id: '{{ order.id }}', // numeric ID, not order.name
order_name: '{{ order.name }}', // keep the human-readable version as a custom param
value: {{ total_price | divided_by: 100.0 }},
currency: '{{ currency }}',
items: [
{% for line_item in line_items %}
{
item_id: '{{ line_item.product_id }}',
item_name: {{ line_item.title | json }},
item_variant: {{ line_item.variant_title | json }},
price: {{ line_item.final_price | divided_by: 100.0 }},
quantity: {{ line_item.quantity }}
}{% if forloop.last == false %},{% endif %}
{% endfor %}
]
}
});
Once both pipelines emit the same transaction_id, GA4’s built-in deduplication kicks in — but only if the events arrive within the same session, which they often don’t. That’s why manual dedup in BigQuery is still needed for accurate reporting.
Deduplicating in BigQuery
For reporting layers built on the BigQuery export, dedupe with a window function that prefers the client-side event (because it has better attribution data):
WITH ranked_purchases AS (
SELECT
*,
(SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'transaction_id') AS txn_id,
-- prefer events with a real gclid/session_id
CASE
WHEN (SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'session_id') IS NOT NULL
THEN 1
ELSE 2
END AS priority
FROM `your-project.analytics_XXXXXXXX.events_*`
WHERE event_name = 'purchase'
)
SELECT * EXCEPT(priority, rn)
FROM (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY txn_id ORDER BY priority, event_timestamp) AS rn
FROM ranked_purchases
)
WHERE rn = 1;
This gives you a canonical purchase view. Point your Looker Studio or Fivetran downstream models at this rather than raw events.
The attribution gaps that remain
Even after deduplication, the silent route leaves gaps that dedup alone cannot fix. If a purchase only exists because the server-side route caught it (state 3 above), you have revenue in GA4 with no click ID, no landing page, no first-touch source. That transaction will attribute to “direct” in GA4 exploration reports and will not show up as a Google Ads conversion in the Ads platform unless the Ads-side conversion is imported separately via Enhanced Conversions or the Ads/Shopify direct link.
Three specific gaps to audit:
Consent Mode conflicts. If your store uses Consent Mode v2 and a user denies analytics_storage, your GTM tag correctly withholds the purchase event. Google’s server-side route does not check that consent state — it fires anyway, because it originates from Shopify’s webhook, not the browser. Depending on your jurisdiction (GDPR, in particular), this is a compliance problem you did not introduce and cannot fully disable from GA4’s side. You have to disable the Google & YouTube app’s GA4 integration entirely, or restrict what Shopify sends to the app via Customer Privacy API settings.
User ID stitching. Logged-in customers should have a user_id set for cross-device stitching. The silent route does not send this. You will see purchase events in GA4 with no user_id even for customers who were logged in throughout their session. Cross-device journeys break at the point of purchase.
Custom item parameters. If you send item_brand, item_category2, discount_amount, or any custom item-level dimension, the silent route strips them. Product-level revenue reports will be inconsistent depending on which pipeline captured the event.
Reconciling Shopify, GA4, and Google Ads
Here’s the reconciliation workflow I run monthly on Shopify clients. It takes about 40 minutes if the BigQuery export is set up properly.
Step 1: Pull the source-of-truth number from Shopify
Shopify’s Orders export (Analytics → Reports → Sales → Sales by day) is your ground truth. Export CSV for the reporting period. Filter out cancelled and refunded orders in the same way GA4 does (GA4 does not subtract refunds from the purchase event count unless you explicitly send refund events).
Step 2: Pull the deduplicated GA4 count
Using the BigQuery dedup query above, count distinct transaction_id values for the same date range. Compare to the Shopify count.
| Metric | Shopify | GA4 (raw) | GA4 (deduped) |
|---|---|---|---|
| Orders | 1,247 | 1,391 | 1,251 |
| Revenue | $186,420 | $208,110 | $187,050 |
| Delta vs Shopify | — | +11.6% | +0.3% |
A residual delta of under 1% is normal (test orders, edge cases in timezone handling). Anything over 2% points to duplication that dedup missed — usually because of the transaction ID format mismatch mentioned earlier.
Step 3: Pull Google Ads conversions
In Google Ads, look at the Conversions column for the same date range, filtered to the Shopify-linked conversion action. Compare to your Shopify-attributed Google Ads revenue (Shopify’s UTM attribution in Sales by traffic referrer).
If Ads is reporting significantly more conversions than Shopify’s UTM attribution, the silent route is contributing conversions with stripped attribution that Google is nonetheless matching back to clicks via Enhanced Conversions or its own user matching. That’s not necessarily wrong, but it means Ads and Shopify will never match, and you need to decide which one is the source of truth for spend decisions.
Decision framework: disable, layer, or replace
Once you’ve quantified the gap, pick a strategy. There are three defensible positions.
Disable the silent route entirely. Best for stores with mature GTM implementations, strict consent requirements, or first-party server-side GTM setups. Disconnect the GA4 property from the Google & YouTube channel app (Sales channels → Google & YouTube → Settings → Google Analytics → Disconnect). Keep only the Ads account link. Your GTM tag remains the sole purchase pipeline.
Layer it as a fallback. Best for stores with unreliable client-side tracking (heavy ad blocker traffic, older Shopify themes, no server-side GTM). Keep both pipelines but implement the transaction ID normalisation and BigQuery dedup above. Accept that some purchases will have degraded attribution.
Replace your existing setup. Only defensible if you have no GTM setup and no plans to build one, and your reporting needs are basic. You get GA4 purchase data with no engineering effort. You lose cart events, checkout funnel steps, custom dimensions, and consent granularity.
Most stores I’ve audited land on option 1 or 2. Option 3 is a trap for anyone who cares about funnel analysis, because Google’s silent route only sends purchase — no add_to_cart, no begin_checkout, no view_item. If you want a full ecommerce funnel, you need proper GTM instrumentation regardless.
Common mistakes and troubleshooting
“I disabled the GA4 link in the channel app but purchases are still doubling.” The Google & YouTube app caches the linked property server-side. It can take 24–48 hours for the silent route to fully stop. Verify by checking BigQuery for server-originated events (null browser field) after 48 hours.
“My dedup query is dropping legitimate purchases.” Check whether your GTM tag is sending the order name (#1042) while the silent route sends the numeric ID. If so, both events survive dedup because they have different transaction IDs. Fix at the source by normalising to numeric ID in Additional Scripts.
“Enhanced Conversions is now double-counting in Google Ads.” If you have Enhanced Conversions configured via GTM and the silent route is sending its own user-matched conversions, Google Ads may attribute both. Turn off one of them — usually the GTM Enhanced Conversions setup, since the silent route’s version tends to have better hashing coverage from Shopify’s customer data.
“BigQuery export shows purchases with no matching session.” This is expected for server-originated events and confirms the
Share this article