Analytics engineering · 10 min read
Why GA4 and Shopify never show the same revenue — and how to close the gap
Why GA4 and Shopify revenue differ — consent, ad blockers, duplicate purchases, refunds, definitions, time zones — and how to reconcile them with SQL.
Every Shopify brand that runs GA4 eventually puts the two revenue numbers side by side and finds they disagree. That is expected. The useful question is not “why don’t they match?” but “is the gap made of things we accept, or things we can fix?” Here is how to answer it.
Why can’t GA4 and Shopify ever match exactly?
They count different things. Shopify creates an order on its own servers, so every paid order exists in Shopify regardless of what happened in the customer’s browser. GA4 only knows about a purchase if a purchase event reached Google from the browser (or from a server you control). Anything that stops, repeats or changes that event moves GA4 away from Shopify.
So treat Shopify as the system of record for revenue, and GA4 as a behavioral dataset that covers most — but never all — online-store purchases. The goal of reconciliation is to make the gap small, stable and explained.
What are the most common causes of the gap?
Most gaps come from a short list of causes. Some lose purchases, some add them, and some change the value.
| Cause | Direction | Fixable? | How to spot it |
|---|---|---|---|
| Visitor declined analytics consent | GA4 lower | No (by design) | Gap rises in regions with opt-in consent |
| Ad blockers and browser privacy tools | GA4 lower | Partly, with server-side events | Gap higher on desktop browsers and tech-savvy audiences |
| Non-online-store orders (POS, draft orders, subscription renewals, marketplaces) | GA4 lower | No — exclude them | Missing orders share a source_name |
| Two tags sending the same purchase | GA4 higher | Yes | More than one purchase event per transaction ID |
| Empty or inconsistent transaction ID | Either | Yes | Blank or non-order IDs in ecommerce.transaction_id |
| Revenue definition (tax, shipping, discounts) | GA4 lower or higher | Yes — align definitions | Matched orders with consistent value differences |
| Refunds and cancellations | GA4 higher | Yes | Difference grows as refunds are processed |
| Currency (presentment vs shop currency) | Either | Yes | Mismatches cluster on international orders |
| Time zone differences | Shifts revenue between days | Yes | Daily totals differ, weekly totals agree |
Consent and ad blockers
Shopify’s pixel manager “will only load your pixel if there is visitor permission for all of the settings that your pixel declares as required.” If a visitor declines analytics or marketing consent in a region that requires opt-in, pixels that need that permission never run for that visitor. Their order still appears in Shopify, but not in GA4. That part of the gap is intended behavior and should not be “fixed”.
With Google’s consent mode in its advanced form, tags send cookieless pings when consent is denied, and GA4 can use them for modeling once a property meets Google’s data thresholds. Modeled data helps reports, but it does not put individual transactions back into your BigQuery export.
Ad blockers and browser tracking protection are a separate loss. They block requests to analytics domains regardless of what the visitor chose at the banner.
Duplicate and missing purchase events
The most fixable gap is two tags reporting the same order: the Google & YouTube app plus a Google Tag Manager container loaded as a custom pixel, or an old tag left over from a previous agency. Google Analytics deduplicates purchase events that share a transaction ID, but only for web streams — and if the ID is blank, Google warns that it “will deduplicate all purchase events that have transaction_id=”“”, which silently collapses unrelated orders into one.
Missing purchases are the other side: a tag that only fires on some checkout paths, or a pixel that requires a consent category the banner never grants.
Checkout extensibility and Customer Events
Shopify has moved checkout tracking to web pixels managed under Customer Events. The additional scripts box in checkout settings has been view-only since August 28, 2025, and August 26, 2026 was the deadline for non-Plus stores to upgrade the Thank you and Order status pages. If your purchase tracking used to live in additional scripts, it needs to be rebuilt as an app pixel or custom pixel — and stores that upgraded without doing that can lose purchase tracking entirely.
Two details matter for reconciliation:
- The
checkout_completedevent “is triggered once for each checkout, typically on the Thank you page.” If you use a post-purchase upsell, it fires on the first upsell page instead and not again on the Thank you page. A tag built on page views rather than this event can double count or miss upsell orders. - Custom pixels run in a sandbox. Shopify notes that GA4 page URLs inside a custom pixel include information about the sandbox, and that Tag Assistant’s troubleshooting does not detect tags in custom pixels. Test with GA4 DebugView and real orders instead.
Revenue definitions: tax, shipping and discounts
Google’s event reference tells you to set the purchase value to the sum of price times quantity for the items, and “Don’t include shipping or tax” — those have their own parameters. Shopify’s finance reports define net sales as gross sales minus discounts and sales reversals, excluding shipping and taxes, and total sales as net sales plus taxes, shipping and fees.
So the fair comparison is GA4 purchase revenue against Shopify net sales for online-store orders — not against total sales. Integrations differ in what they actually send, so check a few real orders before assuming.
Refunds
Shopify records sales reversals “on the date the return was processed,” as a negative number. Refunds are issued in the Shopify admin, so no browser pixel ever sees them. GA4 only reflects a refund if something sends a refund event with the original transaction_id. Without that, GA4 revenue stays at the original order value forever, and the gap grows each month as returns come in.
Currency and time zones
For international stores, decide whether you compare in the shop’s currency or the customer’s presentment currency, and make sure the GA4 currency parameter matches what you send as value. GA4’s BigQuery export provides both purchase_revenue (in the event’s currency) and purchase_revenue_in_usd.
GA4 assigns events to dates using the property’s time zone. If that differs from the time zone set in your Shopify admin, revenue moves between days. Daily numbers disagree while weekly totals agree — a clear signature of this problem.
How do you reconcile GA4 and Shopify order by order?
Totals hide causes. Reconcile at the order level, then aggregate.
- Pick a closed period. Use a complete week or month that ended at least three days ago, so late-arriving GA4 events are processed.
- Pull Shopify orders. Order ID and order name, created date in the store’s time zone,
source_name, currency, subtotal after discounts, tax, shipping, cancellation status and refunds. - Pull GA4 purchases. From the BigQuery export:
ecommerce.transaction_id,event_date, purchase revenue and a count of purchase events per transaction. - Confirm the join key. Check which identifier your integration sends as
transaction_id— the numeric order ID or the order name such as#1001— and normalize both sides. - Exclude orders GA4 can’t see. POS, draft orders, subscription renewals and marketplace orders go in a separate bucket.
- Classify each order as matched, missing in GA4, GA4-only, duplicated or value-mismatched.
- Fix in order of impact. Duplicates and broken IDs first, then definitions, then refunds, then time zones. Leave consent-related loss alone.
What does the reconciliation SQL look like?
This BigQuery query assumes the GA4 export dataset and a Shopify orders table loaded from the Admin API. Adjust table names, the time zone and the join key to your setup.
WITH ga4 AS (
SELECT
ecommerce.transaction_id AS transaction_id,
COUNT(*) AS purchase_events,
SUM(ecommerce.purchase_revenue) AS ga4_revenue
FROM `my-project.analytics_123456789.events_*`
WHERE _TABLE_SUFFIX BETWEEN '20260901' AND '20260930'
AND event_name = 'purchase'
GROUP BY transaction_id
),
shop AS (
SELECT
CAST(order_id AS STRING) AS transaction_id,
source_name,
subtotal_after_discounts AS net_value
FROM `my-project.shopify.orders`
WHERE DATE(created_at, 'America/New_York') BETWEEN '2026-09-01' AND '2026-09-30'
AND cancelled_at IS NULL
AND test = FALSE
)
SELECT
CASE
WHEN shop.transaction_id IS NULL THEN 'ga4_only'
WHEN shop.source_name != 'web' THEN 'not_online_store'
WHEN ga4.transaction_id IS NULL THEN 'missing_in_ga4'
WHEN ga4.purchase_events > 1 THEN 'duplicate_events'
WHEN ABS(ga4.ga4_revenue - shop.net_value) > 0.01 THEN 'value_mismatch'
ELSE 'matched'
END AS status,
COUNT(*) AS orders,
ROUND(SUM(shop.net_value), 2) AS shopify_net,
ROUND(SUM(ga4.ga4_revenue), 2) AS ga4_revenue
FROM shop
FULL OUTER JOIN ga4 USING (transaction_id)
GROUP BY status
ORDER BY orders DESC;
GA4’s standard reports deduplicate repeated transaction IDs on web streams, so counting purchase events per transaction in the raw export is how you see duplicates that the interface hides. Note that ga4_revenue for a duplicated order is summed across its events, so read value mismatches only on rows that are not duplicates.
How do you read the results?
- Many
missing_in_ga4orders spread evenly across devices and regions: likely a tag that fails on some checkout path. Place test orders on each path. missing_in_ga4concentrated in opt-in regions: consent. Expected.- Any
duplicate_events: two tags, or a tag firing on both an upsell page and the Thank you page. Remove one. ga4_onlyrows: transaction IDs that don’t match a Shopify order — usually a formatting mismatch in the join key, test orders or a broken ID.- Consistent
value_mismatch: a definition difference, such as GA4 receiving totals including tax and shipping.
Once duplicates and IDs are fixed, re-run the query each month. A stable gap made of consent and ad-blocker loss is a healthy result. A gap that moves without explanation is a signal that something in the tracking changed.
When is it worth bringing in help?
If the gap is large, unstable or you are about to make budget decisions from GA4 or ad-platform numbers, an order-level reconciliation is the fastest way to know what you can trust — and it is the first step of the GA4 and server-side tracking work Asabisoft does for Shopify brands.
Key takeaways
- Shopify is the system of record for revenue; GA4 is a behavioral sample of online-store purchases. Reconcile GA4 to Shopify, never the other way round.
- Compare like with like: GA4 purchase value should be checked against Shopify net sales (excluding tax and shipping), not total sales.
- Join on transaction ID and classify every order as matched, missing in GA4, duplicated or value-mismatched — the mix tells you what to fix.
- POS, draft, subscription renewal and marketplace orders never pass through the online checkout, so GA4 will never see them.
- Refunds happen in the Shopify admin, not the browser; GA4 only knows about them if something sends a refund event.
FAQ
What is an acceptable gap between GA4 and Shopify revenue?
There is no universal number. It depends on your consent rate, your audience's ad blocker use and your channel mix. Instead of chasing a target percentage, reconcile order by order and make sure the remaining gap is explained by causes you accept, such as declined consent, rather than by duplicate or missing events.
Why does GA4 show more purchases than Shopify?
Usually duplicate purchase events: two tags sending the same purchase (for example the Google & YouTube app plus a hand-installed tag or a Google Tag Manager custom pixel), or purchases sent with an empty or changing transaction ID. GA4 deduplicates purchases that share a transaction ID on web streams, so a changing ID defeats it.
Does GA4 deduplicate purchase events?
Yes, for web streams. Google Analytics deduplicates purchase events with the same transaction ID. It does not do this for app streams, and if you send an empty transaction_id, GA4 treats all those purchases as duplicates of each other.
Should GA4 revenue include tax and shipping?
Google's event reference says to set the purchase value to the sum of price times quantity for the items and not to include shipping or tax, which are sent in their own parameters. Check what your integration actually sends before comparing it to a Shopify report.
Sources
- Google Analytics — Recommended events reference (purchase, refund)
- Google Analytics Help — Minimize duplicate key events with transaction IDs
- Shopify Help Center — Finance reports (gross, net and total sales)
- Shopify.dev — checkout_completed standard event
- Shopify.dev — Pixel privacy
- Shopify Help Center — Upgrading to checkout extensibility