Your GA4 property reports 4,180 purchases last month. Your order table says 4,690. Asked which is right, the honest answer is neither, until you can say where the missing 510 orders went. This is not a tidiness problem. Every experiment result and every prioritisation score you produce is calculated on a number that is wrong by an unknown amount in an unknown direction.
Google treats a large unmeasured slice as normal: behavioural modelling for consent mode does not switch on until a property records at least 1,000 events per day with analytics_storage='denied' for at least 7 days. Below that, missing data stays missing.
This is for whoever owns analytics or CRO at an e-commerce team. By the end you will have a reconciliation table, a decomposition of your gap into named causes with a test for each, and a tolerance figure you can defend.
What a purchase reconciliation actually measures
A reconciliation is not "make the numbers match". They will never match, and a team that forces them to has usually done something worse than leaving the gap alone. It answers three questions: how large is the gap, which named mechanisms produce it, and how much is recoverable at what cost. We run it before any experimentation engagement, because a test measured on a conversion event that silently drops an unknown share of orders is not an experiment.
Step 1: pull two tables that can be joined on order ID
You need one row per order, from both systems, for the same calendar range, with a key that exists on both sides.
- Backend: order ID, created timestamp in the property's reporting timezone, order value on the same tax and shipping basis you configured in GA4, payment method, device, country, status.
- GA4: the BigQuery export, not the UI. Query
purchaseevents withecommerce.transaction_id,event_timestamp,ecommerce.purchase_revenue, device category, and traffic source.
Use BigQuery deliberately. Modelled data from consent mode appears in GA4 standard reports and some explorations, but Google states it is not in the BigQuery export, audiences, user explorer, cohort or retention explorations, or predictive metrics. Compare the UI to your order table and you are comparing an estimate to a fact.
Then full outer join on order ID. You now have three buckets: in both, backend only, GA4 only. The third is smaller but the most diagnostic.
Step 2: express the gap three ways, not one
A single monthly percentage hides everything. Compute all three:
- Order-count gap: (backend orders minus matched GA4 purchases) / backend orders. This isolates tracking loss.
- Revenue gap: the same calculation on value. If the count gap is 3% and the revenue gap is 11%, you do not have a tracking problem, you have a value-definition problem (tax, shipping, discounts, currency, gift cards).
- Daily gap variance: plot the daily order-count gap. A flat line means a structural cause such as consent or blockers. Spikes mean deploys, campaign bursts or a payment provider incident.
Step 3: decompose the gap into named causes
Work down this table. Each cause has a test that can confirm or eliminate it in under an hour.
| Cause | How to test it | Signature in the data |
|---|---|---|
| Consent denied, tag never fires | Compare your CMP's consent-state log to GA4 sessions for the same period | Stable daily percentage; concentrated in EU and Turkish traffic |
| Ad and tracker blockers | Count server-log requests for the confirmation URL, compare to GA4 purchase events on that URL | Skews desktop, Firefox and privacy browsers; stable over time |
| Confirmation page never loads | Count backend-only orders whose confirmation URL was never requested in server logs | Concentrated in bank-redirect and 3DS payment methods |
| Duplicate purchase events | Count GA4 purchase events per transaction_id | GA4 exceeds backend on some days; refresh and back-button heavy pages |
| Cross-domain link broken | Inspect whether _gl survives the return trip from the payment provider | Purchases land in new sessions attributed to the payment domain as a referral |
| Cookie lifetime reset | Segment new-user rate by browser | Inflated new users and direct traffic on Safari and iOS |
| Modelled versus raw | Compare GA4 UI totals to BigQuery for an identical date range | The two Google numbers disagree before you even open the order table |
| Value definition | Recompute backend revenue with and without tax, shipping and discounts | Counts match, revenue does not |
| Timezone boundary | Re-aggregate to full weeks | Gap shrinks toward zero at weekly granularity |
Two deserve extra care. transaction_id deduplication works on web streams only, not app streams, and Google warns that sending an empty string makes Analytics deduplicate every purchase event carrying transaction_id=''. That one bug can erase most of a day. Separately, the unwanted-referrals list is capped at 50 domains per data stream, which bites if you use several acquirers, wallets and bank pages.
[INTERNAL DATA NEEDED: the observed distribution of these nine causes across Switas reconciliation engagements, so readers can see which ones usually dominate.]
Step 4: set a tolerance you can defend
Google publishes no acceptable discrepancy figure, and the widely quoted "5% is fine" appears in no Google documentation we could find. Set your own, conditional on decomposition rather than on the number alone.
| Monthly order-count gap | Reading | Action |
|---|---|---|
| Up to 2% | Timezone edges and normal dedupe | Record the causes, re-check quarterly |
| 2% to 5% | Acceptable only if every point is attributed to a named cause | Document, then proceed to experimentation |
| 5% to 10% | At least one structural cause is unfixed | Fix before trusting absolute conversion numbers |
| Above 10% | Not fit for experimentation | Stop using GA4 purchases as the primary experiment metric until fixed |
These are our operating thresholds, not an industry standard. The qualifier is the point: an unexplained 3% is worse than an explained 8%.
Step 5: fix in the order that recovers the most orders per hour of work
- Value definition and timezone. Configuration, not engineering, and usually an afternoon.
- Duplicates and empty transaction IDs. One dataLayer fix.
- Cross-domain and unwanted referrals. Configure both, then re-test the live payment return path, not a staging one.
- Server-side purchase event. Send the purchase from your order system with the Measurement Protocol, using the
client_idyour web tag generated. Google requires these events within 48 hours of the original client-side event timestamp, or attribution may not process as expected. This is what recovers orders lost to blockers and to confirmation pages that never rendered. - Consent architecture. The slowest and most political. Treat the remainder as a known, sized, documented gap.
What changes if you sell in Turkey or the EU
Two regional facts move the numbers.
Consent is opt-in in Turkey too, but without Google's safety net. The Turkish data protection authority fined a website operator 300,000 TL in decision 2022/1358 (23 December 2022) for running advertising and marketing cookies without a legal basis, and required active opt-in for non-essential cookies. Many Turkish sites answered with a banner that blocks the Google tag outright instead of one that sends consent-mode signals. That distinction is expensive: modelling needs analytics_storage='denied' events to exist. A banner that fires nothing gives Google nothing to model from, so the data is gone rather than estimated.
Turkish 3DS flows return the buyer by POST. With iyzico, the merchant renders a decoded 3DS page and, after OTP confirmation, iyzico redirects to the merchant's callbackUrl with status, paymentId and mdStatus. A POST-driven return is exactly what Google names as breaking cross-domain measurement, since redirects can strip _gl before the destination loads. If your gap sits in card payments and is near zero for cash on delivery, this is why.
Two widely repeated claims we could not verify
"Safari caps first-party cookies at 7 days." Imprecise. WebKit's documentation says ITP deletes JavaScript cookies and other script-writable storage after 7 days of no user interaction, and separately caps JavaScript cookies to 24 hours when the landing page was reached by link decoration from a classified domain. Simo Ahava's testing found the clock runs on days of browser use, not calendar days, so a returning customer can hold an identifier well past a week.
"A 5% discrepancy is acceptable." We found no Google source for this figure. It circulates through vendor blogs and forum answers. Use it as folklore, not as a threshold.
Where this method breaks down
It does not cover app streams, where transaction_id deduplication does not apply, or subscription and partial-shipment models, where "an order" is not one event. It cannot recover a consent-denied user's identity, only estimate the volume. It does not fix channel attribution, a separate and harder problem. And it assumes your order table is clean: if cancelled and fraud-blocked orders are counted inconsistently on the two sides, you will chase a gap that exists only in your query.
Frequently asked questions
How often should we run this? Fully once, then a daily gap chart. Re-run the full decomposition after any checkout, CMP, payment provider or tag manager change.
Why not use the GA4 interface instead of BigQuery? The interface can include modelled users and sessions that the export does not, so the two Google numbers disagree with each other. Reconciling against an estimate makes the gap impossible to decompose.
Will server-side tagging close the gap completely? No. It recovers orders lost to blockers and to confirmation pages that never rendered. It does not recover users who refused consent.
GA4 is higher than our backend. Is that possible? Yes. Usually duplicate purchase events from page refreshes, an empty or reused transaction_id, or staging orders firing into the production property.
Does consent mode fix the Turkish consent gap? Partly. Denied signals make modelling possible, subject to Google's volume thresholds. It is not a substitute for a lawful banner under Turkish rules.
What counts as a good gap? One where every percentage point has a name. A documented 8% beats an unexplained 3%.
Should we restate historical reports after fixing this? No. Annotate the fix date and treat the periods as separate series. Restating tracked conversions destroys the audit trail.
Can we run A/B tests while the gap is above 10%? Run them on relative metrics measured inside the testing tool, but do not report absolute revenue impact from GA4 until the gap is decomposed.
Run this reconciliation on your own store this month, or have our analytics team run it with you and hand you the decomposition, the tolerance and the fix order.
Sources
- [GA4] Behavioral modeling for consent mode, Google Analytics Help
- [GA4] Minimize duplicate key events with transaction IDs, Google Analytics Help
- [GA4] Unwanted referrals, Google Analytics Help
- [GA4] Cross-domain measurement, Google Analytics Help
- Send Measurement Protocol events to Google Analytics, Google for Developers
- Consent mode parameters, Google Tag Platform documentation
- Tracking Prevention in WebKit
- Expiration Cap Removed From JavaScript Cookies In WebKit Browsers, Simo Ahava
- KVKK Kurul karari 2022/1358, 23 December 2022
- 3DS Implementation, iyzico developer documentation







