Shopify and GA4 revenue don't match? How to find the gap with AI
Hi, this is Vivek, building Contextflo. I share practical notes on getting answers from your data, a couple of times a month.

Shopify and GA4 will never agree exactly, and they aren't supposed to. Shopify counts every order your store took. GA4 counts the purchases its tag saw in a browser that let it. The fix isn't to make the totals match. Match the two at order level on the transaction ID, and every Shopify order with no partner in GA4 is a specific missed purchase you can group by cause: consent declined, a checkout path where the tag never fired, a POS sale that never touched the site. Then decide how big a gap is normal for your store and watch for the week it moves.
Where the revenue goes missing
Most of the gap comes from a short list of suspects. Some make GA4 lower, a few make it higher, and one only shifts revenue between days.
| Suspect | Effect on GA4 | How to spot it |
|---|---|---|
| Ad blockers, declined cookies | Lower | Missing orders spread evenly across products and days, often heavier on some browsers or regions |
| Purchase tag doesn't fire (thank-you page closed early, not loaded, or skipped by a checkout path your tag doesn't cover) | Lower | Missing orders cluster by payment method, checkout type or date of a checkout change |
| Orders that never touch the site (POS, draft, wholesale) | Lower | Shopify's Source column says something other than web |
| Refunds and cancellations | Higher | Shopify net sales drop after the fact, GA4 purchase revenue doesn't |
| Tax and shipping counted on one side only | Either | GA4 revenue per order sits close to Shopify's Subtotal instead of its Total, or the other way round |
| Currency | Either | Multi-currency store, GA4 revenue off by roughly an exchange rate on some orders |
| Time zones | Shifts days | Daily totals disagree, monthly ones mostly agree |
| Duplicate transactions (thank-you page reloads, GA4 installed twice) | Higher | Same transaction ID more than once, or more GA4 purchases than Shopify web orders on some days |
A few of these are worth a closer look.
Consent is a gap by design
When a shopper declines analytics cookies under consent mode, Google Analytics doesn't read or write its cookies. With the advanced setup it still sends cookieless pings, which Google uses to model the missing conversions; with the basic setup you get no modeled data for those users at all.[1] Either way, don't expect to find those orders in GA4 as individual transactions you can join to. They set your floor, and no amount of tag fixing moves it.
Duplicates are usually a tagging problem
Google says GA4 deduplicates purchase events that share a transaction ID from the same user.[2] So when GA4 shows more purchases than you had, look at the tag: purchases sent with no transaction ID, a different ID on each reload, or two installs (Shopify's Google & YouTube sales channel plus a hand-added tag) each sending their own.
Time zones only look like missing money
Shopify reports in the store's time zone. GA4 uses the property's reporting time zone, and even the BigQuery export's event_date follows it, while event_timestamp is in UTC.[3] If the two settings differ, late-evening orders land on different days, and a daily comparison looks broken while the month is fine.
How much gap is normal?
There's no official number, and I wouldn't trust anyone who quotes one for your store, me included. The share of shoppers who block tracking depends on who they are, and the share who decline cookies depends on your banner and your markets.
Measure your own instead. Take a quiet, clean month, match it order by order, drop the orders that can't be tracked by design (POS, drafts, wholesale), and see what share of web orders never reached GA4. That's your gap budget. If it's 9% most months and jumps to 20% the week you changed themes or checkout apps, the jump is the bug. The 9% is just the cost of doing business in a browser.
Knowing the budget pays off elsewhere too. When you're working out why sales dropped, a GA4 conversion rate that fell only because tracking broke is a false lead. And GA4's channel reports carry the same gap, which is one reason they disagree with your own orders when you compare channels by revenue.
Quick way: two exports and a Claude chat
- Export Shopify orders. Orders, then Export, as CSV. Keep
Name,Created at,Subtotal,Shipping,Taxes,Total,Currency,Financial Status,Refunded Amount,Canceled atandSource.[4] - Export GA4 purchases with transaction IDs. Build an Exploration with transaction ID and date as dimensions and purchase revenue as the metric, then use Export data, which offers Google Sheets, CSV and TSV.[5]
- Upload both files to a Claude chat, or put them in one Google Sheet and link it through Claude's Google Drive connector.[6][7]
Then ask:
Match the Shopify orders to the GA4 transactions. Check 10 rows first and
tell me whether GA4's transaction ID holds Shopify's order number (Name,
like #1001) or something else, and normalize the IDs before joining.
List Shopify orders from the web with no matching GA4 transaction, and
GA4 transactions with no Shopify order. Group the unmatched Shopify
orders by Source, day of week and payment method. What share of web
orders is missing?
For matched orders, compare GA4 revenue to Shopify Subtotal and to Total.
Which one does GA4 track more closely? Then compare daily totals and flag
any day where the gap is more than twice the monthly average.
What gets in the way
Claude caps how many files a chat can hold and how large each one can be, so check Anthropic's current upload limits, though size is fine for most stores.[6] The harder limits are elsewhere. Check that the GA4 export's row count looks roughly like your web order count before trusting the join; an export that got filtered by accident or cut short looks just like missing orders. Claude also works out the ID cleanup fresh each chat, and a different guess next month quietly changes the gap. And it's a snapshot, so next month you export both sides again.
Connect the files once with Contextflo
Connect the Sheet holding the GA4 export to Contextflo once, and Claude or ChatGPT can read it directly alongside the Shopify orders CSV you upload, and your warehouse too if you have one.
The useful part is saving the rules once: which Source values count as web orders, which Shopify column GA4 revenue should be compared with, and your gap budget. Next month's question starts there instead of at "check 10 rows first".
Share that setup with the team, so whoever asks next month gets the same gap number, matched the same way, instead of a fresh guess at the ID cleanup.
Want it every week? Put both in BigQuery
GA4 has a native BigQuery export. It writes each event with ecommerce.transaction_id, ecommerce.purchase_revenue and ecommerce.refund_value,[3] it can run in the free BigQuery sandbox, and standard properties have a daily event export cap, so check Google's current BigQuery Export limits before you count on capturing everything.[8] It only holds data from the day you link it, so turn it on before you need it. The steps are in how to connect Google Analytics to Claude.
For the Shopify side, Fivetran and Airbyte both have Shopify connectors that load orders into BigQuery on a schedule.[9][10] Once both land in the same dataset, the match is a daily query:
-- Shopify orders from the web with no GA4 purchase
SELECT o.name, o.created_at, o.total_price
FROM shopify.orders o
LEFT JOIN (
SELECT DISTINCT ecommerce.transaction_id AS tid
FROM `analytics_123456.events_*`
WHERE event_name = 'purchase'
) g ON g.tid = o.name
WHERE o.source_name = 'web' AND g.tid IS NULL
Table and column names vary by connector and by what your tag sends as the transaction ID, so treat that as a shape, not a copy-paste. Counting distinct transaction IDs also keeps reload duplicates from inflating the GA4 side. Point Claude at the dataset directly, as in Claude with BigQuery, or through Contextflo so the gap budget and the web-order rule are shared by everyone who asks.
Worked example: closing an $18,600 gap
The numbers are made up for illustration. A home-goods store's September: Shopify says $96,000 across 800 orders. GA4 says $77,400 across 690 purchases. That's a 19% revenue gap. The store's gap budget, the share of web orders that normally never reach GA4, is about 8%.
Bridging from Shopify down to GA4 one cause at a time:
| Step | Orders | Revenue |
|---|---|---|
| Shopify, all orders | 800 | $96,000 |
| Minus POS and draft orders (never on the site) | 740 | $89,500 |
| Minus tax and shipping (GA4 tracks subtotal here) | 740 | $81,900 |
| Minus web orders missing from GA4, spread evenly (consent, blockers) | 684 | $75,700 |
| Minus web orders missing from GA4, all Shop Pay since Sept 12 | 640 | $71,900 |
| Plus reload duplicates with no transaction ID | 690 | $77,400 |
The first three lines are background. POS and tax were never going to match, and 56 of 740 web orders lost to consent and blockers is right on the 8% budget. The last two lines are news. Since September 12, Shop Pay orders stopped reaching GA4 at all, which lines up with a checkout app the team installed that week. And 50 purchases arrived with no transaction ID, a second install of the tag that nobody remembered adding.
Neither one shows up in a totals comparison, where they partly cancel out. Matched by order, each one has a date, a payment method and a fix.
FAQ
Why is my GA4 revenue lower than Shopify? GA4 only records a purchase when its tag fires in the shopper's browser and the shopper allows it. Ad blockers, declined cookies, and a thank-you page that never loads all drop orders. Shopify also counts orders that never touch your site, like POS, draft and wholesale orders. Totals can differ further if one side includes tax and shipping and the other doesn't, or if the two use different time zones.
How much difference between Shopify and GA4 is normal? There's no official number, and it depends on your audience, your consent banner and how your tag is installed. Measure your own baseline: match a clean month order by order, count web orders that never reached GA4, and treat that share as your normal gap. What matters is when the gap moves, not its size.
Why does GA4 show duplicate purchases? Usually the purchase tag fires more than once, for example when a shopper reloads the thank-you page, or when GA4 is installed twice (an app plus a manual tag). Google says GA4 deduplicates purchase events with the same transaction ID from the same user, so duplicates that survive are often events sent without a transaction ID or with a different one.
How do I match Shopify orders to GA4 transactions? Export Shopify orders with their order number and GA4 purchases with their transaction ID, then join the two on that ID. Check a few rows first, since depending on how the tag was set up GA4 may hold the order number or a different order ID. The unmatched Shopify orders are the ones GA4 missed, and you can group them by source, day and payment method to see why.
Can Claude reconcile Shopify and Google Analytics data? Yes, if you give it both sides at order level. Upload a Shopify orders CSV and a GA4 export with transaction IDs, or link a Google Sheet holding both, and ask it to match orders, list the unmatched ones and group them by cause. It can't see orders that GA4 never recorded unless the Shopify side is there to compare against.
Find out if Contextflo is the right fit for you.
See how teams use Contextflo
Related posts
Keep reading

Conversational analytics: 5 ways to set it up, compared
7 min read

How to connect Claude to BigQuery (and fix the errors you'll hit)
8 min read

How to connect Claude to Postgres (and keep it read-only)
7 min read

How to build a BI dashboard with Claude that your team can actually trust
8 min read

How to give Claude read-only access to your database (it isn't always the default)
7 min read

You connected your warehouse to Claude, now what?
5 min read

Why we built our own Postgres MCP server
7 min read

Are my ads actually profitable? How to analyze Facebook and TikTok ads together with AI
7 min read

Still building the weekly KPI report by hand? How to automate it with Claude
7 min read




