The ad platform says 100 sales. Your bank says 20.
Ad platforms, analytics and your bank never agree on how many sales your ads made. Why each counts something different, and how we built a number you can check.

Ask three systems how many sales your ads made last month and you get three answers. Meta's Ads Manager says 100. Google Analytics says 20 of them came from Facebook. Your bank statement has a figure that matches neither. Most people conclude that somebody is lying. Nobody is. They are counting different things.
This post is about why those numbers can never agree, what a proper fix looks like if you were to build one yourself, and how we built it into Growomat.
The platform is grading its own homework
An ad platform reports a conversion whenever its tracking tag fires on your website and it can connect that visitor to an ad it showed them. The connecting is the whole game, and each platform decides for itself how generous to be.
Meta's default setting counts a sale if the person clicked an ad in the last seven days, saw one in the last day, or watched a video ad for a few seconds in the last day. Someone scrolls past your video on their phone without clicking, searches for you twelve hours later and buys, and Meta claims the whole sale. This January Meta removed its longer view-through windows altogether, which tells you something about how defensible they were.
Google Ads has its own windows, Microsoft its own, Reddit its own. Every report is honest by its own rules and none of the rules match, so the four numbers cannot be added. If two platforms both touched the same customer, both count her. Add them up and you have sold more than you sold.
Analytics sees a different world
Google Analytics only knows about things that happen on your website. It cannot see the video view inside the Instagram app. It cannot see an impression. It sees a visitor arrive and has to work out where from, using whatever survived the journey: the referrer, and the tracking parameters on the URL.
That is a fragile chain. Instagram's in-app browser, a privacy extension, a redirect through a link shortener: any one of them can strip the parameters before your page loads, and the visit lands in analytics as "Direct". The sale is real, the ad caused it, and the report says it came from nowhere.
Since late 2023 GA4 has also defaulted to a data-driven attribution model, which splits credit across touchpoints using a model you cannot inspect. So GA4 is not just seeing fewer conversions than Meta. It is dividing them differently, under a different definition of what counts.
Then there is the gap between clicks and sessions. A platform records a click the instant a thumb lands on the ad. Analytics records a session only after your page has loaded, the tracking script has run and the beacon has reached Google. On a slow mobile connection, if the visitor gives up after three seconds, you paid for the click and analytics saw nothing. Safari caps cookies set by scripts at seven days, and at 24 hours when the visitor arrived through a URL carrying a click id. A returning customer becomes a new visitor every week, and every ad click resets the clock to a day. Firefox does something similar. That is a large slice of your audience, permanently blurred.
The checkout steals the sale
For anyone selling online there is a quieter failure that goes unnoticed for months. The customer clicks your ad, browses, goes to pay. The checkout hands them to PayPal, or Shop Pay, or a bank's verification page. They pay and get sent back to your thank-you page.
To analytics, that return is a brand new visit referred by paypal.com. The purchase is credited to PayPal. The ad click that brought the customer has been overwritten, and the ad platform never hears about the sale at all, because the parameters it needed were lost on the domain hop. The fix is a setting called "unwanted referrals" in the GA4 admin, which you would only know to look for if you already knew the problem existed. Meanwhile the reported return on your ads sinks, and people cancel campaigns that were working.
The tag itself is fragile
Everything above assumes the tracking code is installed correctly. It usually isn't. Google Tag Manager is sold as the tool that lets a marketer wire this up without a developer, and it does let you deploy code without editing the website. Understanding what that code does is another matter.
Form tracking is where most of it breaks. The built-in "form submission" trigger only hears traditional forms that reload the page. Modern forms submit in the background and the trigger hears nothing. So people fall back to "fire when the Submit button is clicked", which fires when the button is clicked whether or not the form was valid. Every visitor who forgot their email address becomes a lead. The bidding algorithm is then trained, diligently, to find more people who click buttons.
Then there are the timing problems, where a tag reads a variable before the page has set it and the order value arrives as undefined. The duplicate installs, where Shopify's channel app injects the Meta pixel and a second one arrives through Tag Manager, so every purchase is reported twice and your return-on-spend bidding is tuned to imaginary revenue. And the simple weight of it: every tag is a script the visitor's browser has to download and run before the page is usable.
Server-side tracking fixes some of this, at a price
The platforms' answer is to stop trusting the browser. Meta calls it the Conversions API; Google has Enhanced Conversions and a server-side Tag Manager. The idea is right. Your server tells the platform about the sale directly, so no ad blocker or browser policy can get in the way, and you decide exactly what data is shared.
Building it is where a small business falls off. Server-side Tag Manager means renting a cloud container, pointing a subdomain at it, and running two Tag Manager containers that have to agree about where events go. Get one URL wrong and half your events route through the server while the other half go straight to Google, and nothing tells you which half. The preview tool is unreliable enough that people end up testing with curl. Losing a fortnight to this is normal.
The cheaper route is a webhook from your payment tool or CRM, through Zapier,
straight to the platform. This works until it double-counts. The browser pixel
reports a lead; seconds later the webhook reports the same lead; unless both
carry the same event id, the platform counts two. Meta deduplicates on an
event_id you have to generate once and send both ways. Most Zapier setups
never do.
And there is a ceiling on what any of this achieves. Better data only helps a campaign that has enough conversions for the platform to learn from. Two sales a week is not a signal, however precisely you report them.
Then the law arrives
Since March 2024 Google has required Consent Mode v2 from anyone with visitors
in Europe. It adds two signals to the tag, ad_user_data and
ad_personalization, and both have to be granted before features like
Enhanced Conversions work. Microsoft's own consent mode became mandatory in
May 2025; every UET event carries an asc flag, G or D, and in Europe the
default is D. Reddit's pixel sits behind the same cookie banner.
The mechanics are unforgiving. The banner has to set a default of denied before any tag fires, then send an update when the visitor accepts. If the update is mistimed or the parameters are not mapped, the platform treats the visitor as having refused forever. Conversions fall off a cliff overnight, remarketing audiences empty out, and the campaigns go blind. This is the most common cause of the "our tracking broke and we don't know when" call.
None of this is a bug. It is what happens when you ask an advertising platform, an analytics tool and a browser vendor to agree on a fact that none of them owns: whether you got paid.
What a real fix looks like
Take the vendors away and the fix is a short list. Hard to do well, but not mysterious.
Decide what a conversion is, once. The honest answer is money you received, or a lead your own system confirmed. Not a pixel firing, not a form button. If you cannot reconstruct the number from your own records, it is not your number.
Capture the click on your own domain, at landing. Every platform stamps an
id on the URL when someone clicks an ad: gclid for Google, msclkid for
Microsoft, rdt_cid for Reddit. Read it the moment the visitor arrives and
store it first-party, on your server, against the person once you know who
they are. A cookie capped at a day cannot hurt you if the click id already
lives in your database beside the email address.
Close the loop from the system of record. The event that means "sale" should come from the thing that took the money: your payment processor, your CRM, your order confirmation. Server to server. The browser is where you capture the click. It is not where you count the sale.
Deduplicate with an order id, at every layer. Same order id at the tag, at the server, at the platform. A reloaded thank-you page or a replayed webhook then becomes a duplicate, not a second sale.
Send it back to the platforms, once, and remember that you did. Uploading a matched sale is what teaches bidding to look for customers instead of clicks. But the platform will then report that sale back to you as its own. Anything you uploaded has to be marked, so you never add it to the platform's total a second time. This is the mistake almost every home-built dashboard makes.
Watch the loop, not just the number. A dead tag looks exactly like a slow month. You need to know when the tag was last seen, when each conversion action last recorded, and what share of your sales you could actually match to a click.
Refuse to model what you cannot see. Visitors who declined tracking, sales that arrived with nothing to match: name them as a band and leave them there. The moment you extrapolate, the number stops being one anyone can check.
How Growomat does it
We built that list into the product, then spent most of the effort on the two ends nobody builds: switching it on without a developer, and saying what the result means in plain words.
Two lines on your website. Our snippet reads the click id off the landing URL and holds it in memory. It writes nothing, no cookie, no storage, no beacon, until your cookie banner says yes. It listens for the standard signals (the IAB consent framework, Google's Consent Mode, or a one-line call from your own banner) and only then keeps the click in a first-party cookie for 90 days and tells our server. A visitor who declines is discarded and counted only as a decline. An early version of the tag stored the click before asking. We fixed that before the Revenue page shipped, and we are saying so here because "we wait for your banner" is a promise the code has to keep, not the copy.
A memory of who came from which click. When a visitor later signs up or buys, we join the click to a hashed email on our side, so a sale reported weeks later by email alone can still find its click. Those records expire after 180 days without use. The longest any platform gives us to report a conversion is 90 days after the click, so holding anything longer would be retention without a reason.
The sale comes from wherever you know about it. The Revenue page opens with one question: how do you know when you have made a sale? A thank-you page works with any checkout and starts counting today; the snippet reads the amount and order id off the confirmation page. If your sales close on the phone, "Mark a sale as won" records one in ten seconds. Anything that can make an HTTP request can post a sale to our API, which is how Zapier and Make users do it. Direct connections to payment tools are coming, and the product says so, rather than offering a Connect button that does nothing.
A ladder, not a coin flip. Each incoming sale is matched the best way available: by the click id it carries; failing that, by looking up the email; failing that, by identity alone where a platform supports it. Every row on the Revenue page says which rung it landed on: "Matched by click", "Matched by email", "Matched by identity", or "Not connected", with the reason. Sales from a thank-you page carry a label saying that tier can never see a refund, because a cancelled order does not re-render a thank-you page.
One order id, three checks. The tag deduplicates on the order id, the server does it again, and the platform does it a third time. A reloaded confirmation page is a duplicate, not a sale.
Counted once, and we know which side counted it. Every sale we upload to a platform is stamped with that platform's name. When you look at a period that includes that platform's own report, the sale is already inside the platform's number and is not added again. Where we cannot prove the overlap, the page says "may overlap" instead of presenting a sum. A sale uploaded an hour ago that the platform has not echoed yet shows as pending, not missing. And where a platform publishes no value at all, as Reddit does, you see a dash and a reason, never a zero.
The declined band stays visible. "14 visitors declined tracking. Their sales can never be matched, and that is the law working, not us failing." That sentence has no fix button, on purpose. When someone declines, none of their hashed identifiers leave our server, whatever the platform would accept.
We watch the loop. The health card tells you when the tag was last seen on your website, when each conversion action last recorded, whether your match rate has dropped, and whether the sale you care about is actually the goal your campaigns bid on. Each finding is a sentence and at most one button.
The answer is a sentence. "Your ads made €4,180 in August. You spent €1,290 to get it. Counted from your own sales, matched one by one to the ad that earned them, not from the platforms' reports." Underneath it, one comparison line: "The platforms' own reports claim €6,900 for the same period. Platforms credit themselves generously; we count each sale once, from your money." When we can only trace part of your sales, the headline changes to "at least", and says how many of them we could see.
And what changed because of it. Once enough traced sales reach a platform, the page says so: "Google now bids on customers instead of clicks." Until then it says the opposite, plainly: your platforms are still bidding for clicks, and every sale we can connect teaches them what a good customer looks like.
What we deliberately don't do
We do not do modelled or multi-touch attribution. A click id is one click, so the number is single-touch by construction, and it is a number you could rebuild yourself from your own orders and our records. The measurement industry mostly sells the other thing: fractional credit across touchpoints, produced by a model you take on trust. We decided, in writing, not to drift into it one "assisted conversion" at a time. If we ever add a modelled view it will be a separate decision with a separate label, never a quiet change to the headline.
The whole loop is also on the public API and in the Growomat MCP connector,
under the conversions scopes: post a sale, check the health of the loop, ask
which ads made you money this month. The dashboard uses the same endpoints.
The platform's number is not a lie. It is the answer to a different question, asked in the platform's own interest. Yours should be counted from your own money, once, in a way you could check. That is the whole product decision. Everything else in this post is the engineering it took to keep the promise.
Growomat Dev Team
Run all four platforms from one place.
Build, optimize and report on Google, Microsoft, Reddit and Meta campaigns without an agency retainer.
See what it costsRead next

A Reddit campaign with a hero image, in one conversation
One prompt, six tool calls and a paused Reddit image ad — plus the promoted post we shipped that read like an ad, and the change that stopped the next one.

How to create beautiful user-centric products with AI
The models are the easy part. What keeps a product user-centric is the harness around them, from principles and stories to prototypes, tests and gates.