Tracking & Attribution

How Do You Calculate True ROAS From CRM Revenue and Refunds?

Ads Manager says 4.13×, your books say 2.03×. How to calculate true ROAS from CRM revenue net of refunds, matched to the campaign that earned it.

Quick answer

True ROAS is the revenue that actually settled in your business, net of refunds and chargebacks, matched back to the campaign that caused it, divided by the spend for the same window. Your ad platform cannot calculate it. It reports the conversions it claims, at the value it was sent, gross, inside its own attribution window. You calculate true ROAS from CRM or payment-processor revenue instead: take real orders, subtract what came back, keep only revenue you can tie to a click, and divide by spend over the same dates in the same currency and timezone.

Tell us what's broken. We'll fix your tracking — free.

Describe the tracking/attribution problem you're stuck on and we'll map it to a fix: server-side conversions to Meta, Google, TikTok and Pinterest, plus first-party tracking that survives Safari. No code required.

True ROAS is the revenue that actually settled, net of refunds and chargebacks, matched back to the campaign that caused it, divided by the spend for the same window. Your ad platform cannot calculate it: it reports its own claimed conversions, at the value it was sent, gross of returns, inside its own attribution window.

So you build the number yourself, from CRM or payment-processor data. Four inputs decide whether it is honest: which revenue counts, what you subtract, what spend sits underneath, and whether both sides cover the same dates.

Why does your ad platform’s ROAS disagree with your accounting?

Because they measure different things. Ads Manager answers “how much revenue did events attributed to my ads report?” Your books answer “how much money did we keep?” They diverge for four structural reasons, none of them a bug.

The platform counts what it was sent. A purchase event carries a value and a currency, and the platform takes both at face value (Meta, Conversions API custom data parameters). Send order totals including shipping and tax, and ROAS is computed on shipping and tax.

The platform counts inside its own window. Every network decides for itself how long after a click — and usually after a view — a conversion still belongs to the ad. Two platforms running the same week will both claim the same sale, which is why Google Ads and Meta both claim the same conversion and why summing platform-reported revenue always exceeds what the bank shows.

The platform never hears about the refund. Conversions are append-only: once an event is accepted it stays accepted, and no retraction API reaches back into yesterday’s reported revenue. Whatever you refund next week is still sitting in last week’s ROAS.

Modelling fills the gaps. When a pixel is blocked or a user opts out, platforms estimate — useful for bidding, not a receipt.

None of that makes platform ROAS a lie. It makes it a media-buying metric — fine for deciding which ad set to pause, wrong for deciding whether the business made money. Reading it as a business number is the most common way to misread an attribution report.

Which revenue belongs in the numerator?

Revenue you have actually collected, for orders you can tie to a click, valued the way a finance person would. Three rules keep it clean.

Use settled money, not authorised money. An order that failed capture, bounced on a fraud check or sat unpaid is not revenue. Payment-processor status is the source of truth, not the order table in your store.

Strip what is not yours. Shipping collected and remitted taxes flow through you to someone else. Including them inflates every ROAS you report. Pick one basis — net goods revenue is the usual choice — and use it on both the report and the target you judge it against.

Count the order once. A store, a CRM and a processor each hold a version of the same sale. Deduplicate on an order or invoice identifier before anything gets summed.

That gives you gross matched revenue. It is still not the numerator.

How do refunds and chargebacks change the number?

They come out, dated to the order they belong to. A refund is not a negative sale in the week it was processed — it is a correction to a sale that already happened, and to the ROAS you reported for it.

The mechanics matter because refunds arrive late. A thirty-day return window means the campaign you are evaluating today is still accumulating refunds for another month, so a fresh week’s ROAS drifts down for weeks afterwards. That is not instability, it is the truth catching up — the same effect that makes refunds inflate attributed revenue on any report built from purchase events alone.

Chargebacks are worse in one way: they land months later, they take the fee with them, and they concentrate in exactly the audiences a loose prospecting campaign attracts. A campaign with a good headline ROAS and a bad dispute rate is not a good campaign.

Two practical rules:

  • Net refunds against the original order date. October’s ROAS should reflect October’s orders as they finally stand, not October’s orders minus September’s returns.
  • Re-read recent periods rather than freezing them. Treat the last thirty to sixty days as provisional and say so in the report, so nobody budgets on a number that has not finished settling.

Purchases ledger in a dark dashboard showing four paid orders with source badges, a refund row in red netting against one of them, and a net revenue total

Which spend belongs in the denominator?

All of the media cost for the channel you are measuring, at the same grain as the revenue above it. The failure here is almost always a missing cost rather than a wrong one.

Platform-reported spend covers the auction cost and the fees baked into the billed amount. It excludes everything else that made the campaign possible: agency retainers, creative production, affiliate and influencer payouts, the tools in the stack, and any offline media running in the same window.

Decide deliberately which of those you include, and keep the decision stable. A blended number that includes agency fees is honest; a denominator that changes definition every month is worse than useless, because the trend then measures your accounting, not your marketing. Grain matters as much as scope — campaign-level revenue against account-level spend is a ratio of two different things, and it will be wrong in the flattering direction.

Why do both sides have to cover the same window and timezone?

Because the divide only means something when both sides describe the same period, and most reporting stacks quietly disagree about what “yesterday” means. Ad platforms report in the account’s timezone, your store records orders in its own, and a payment processor records in UTC.

Off-by-one-day boundaries move daily ROAS by more than most optimisation decisions do, and they never cancel out on short windows. If your dashboard has a timezone control, set it once, write it down, and make every report use it — the same discipline that keeps attribution reporting across multiple currencies from producing three different totals for the same week.

A second timing problem no timezone setting fixes: lag. Spend happens on click day, revenue happens when the customer decides, so a same-day ROAS on a long sales cycle always reads low. Measure over a window comfortably longer than your typical time-to-purchase, or report by cohort — week one’s spend against all the revenue those clicks eventually produced.

How do you match CRM revenue back to the campaign that earned it?

By identity, not by cookie. The join you need is order → person → session → click, and only the first hop exists in your CRM.

A CRM order carries an email, usually a phone number, sometimes a customer id. A session carries a click id, UTM parameters, a landing page and a referrer. Matching means keeping a durable identifier on both sides and resolving the same human across them, which is the whole job of visitor identity resolution.

Three things break that join in practice: the checkout sits on another domain or a hosted page, so the buying session looks like a brand-new visitor with no source; the buyer switched device between the click and the purchase; or the click id is gone, cleared by the browser or stripped by a redirect.

Every unmatched order is revenue you cannot attribute, and that has one effect on ROAS: it pushes every channel’s number down while “Direct” quietly swells. So check your match rate before you trust a low ROAS. A channel often looks unprofitable not because it is, but because a third of its revenue landed in the unmatched bucket.

What does the calculation look like step by step?

Six steps, in this order. Illustrative figures below, not benchmarks.

1. Gross matched revenue      $48,200   orders tied to a click
2. Less shipping and tax      -$5,900   not your revenue
3. Less refunds (order-dated) -$4,300   including late returns
4. Less chargebacks           -$600     plus dispute fees
   Net attributed revenue     $37,400
5. Media spend, same window   $16,000   platform-billed
   Plus agency and tooling    $2,400    if you count them
   Total cost                 $18,400
6. True ROAS = 37,400 / 18,400 = 2.03x

Run the same six lines per channel and the comparison becomes fair, because every channel is judged on money that arrived. Keep the platform’s own ROAS beside it: the gap is a diagnostic. A channel whose platform ROAS is double its true ROAS is either over-claiming attribution, selling to people who refund, or both.

How does PartialLeads calculate true ROAS from matched revenue?

It builds the numerator from purchases rather than pixel events, and keeps the netting and the matching in one pipeline.

Purchases arrive server-side from Shopify, WooCommerce, GoHighLevel, Stripe or a universal webhook. Dedup is idempotent on client, source, order id and status phase, so a redelivered webhook cannot double-count — and a refund arrives as its own status-phase row that nets against the paid order rather than as a second sale. That is step three, automatic, dated to the order it corrects.

Matching is tiered: a visitor id echoed back from the checkout carries the highest confidence, then email, then phone, then IP. When it lands, the purchase flips the lead to Completed with revenue attached and writes both a first-touch and a last-touch attribution row, snapshotting UTMs, click ids, referrer, landing page and time-to-purchase. Purchases that match nothing still land and stay re-matchable, so unmatched revenue stays visible — the honest version of attributing ecommerce revenue to the ad click.

The Attribution report is where the division happens. Its channels table carries Revenue, Purchases, AOV, Avg Lag, Visitors, V→L and ROAS per channel, with a First / Last / Resolved toggle that shows how much of a channel’s ROAS depends on the model you picked. One period selector and one timezone selector apply to the whole report, keeping both sides of the ratio on the same dates, and a currency selector normalises multi-currency revenue into one reporting currency, footnoted with the ECB rate date used.

Two honest constraints. Refunds are netted only when they arrive through a connected purchase source — one issued somewhere PartialLeads never hears about is not subtracted. And the spend side is only as complete as the costs you feed it: agency fees, creative and offline media are not platform line items, so the denominator is still your call.

Attribution report channels table with a First / Last / Resolved model toggle, revenue, purchases, AOV and ROAS columns, and an AUD currency chip

What breaks The mechanism Where you see it in the dashboard
Reported revenue stays gross after refunds Refunds land as their own status-phase row and net against the paid order Purchases ledger, net revenue
Revenue can’t be tied to a campaign Purchase matched to a session by visitor id, then email, then phone, then IP Attribution report, Match Rate and Revenue by Match Quality
Two platforms claim the same sale First-touch, last-touch and resolved models computed on the same purchases Attribution report, Attribution Models
Mixed currencies don’t add up Revenue normalised into one reporting currency at ECB rates Currency selector and its ECB rate-date footnote
Spend window and revenue window disagree One period and one timezone applied across the whole report Period and timezone selectors
Revenue with no session attached Unmatched purchases still land and stay re-matchable Purchases ledger, matched and unmatched wedge

Tell us what's broken. We'll fix your tracking — free.

Describe the tracking/attribution problem you're stuck on and we'll map it to a fix: server-side conversions to Meta, Google, TikTok and Pinterest, plus first-party tracking that survives Safari. No code required.

Sources

  1. Meta — Conversions API, custom data parameters (value, currency): https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/custom-data
  2. Meta — Conversions API, deduplicating pixel and server events: https://developers.facebook.com/docs/marketing-api/conversions-api/deduplicate-pixel-and-server-events
  3. Meta — Conversions API best practices (event timing and delivery): https://developers.facebook.com/docs/marketing-api/conversions-api/best-practices

Frequently asked questions

QWhat is a good true ROAS?
There is no universal number, because true ROAS depends on your gross margin. A 2× ROAS is comfortable on software margins and loss-making on a product with a 40% cost of goods plus shipping. Work out your break-even ROAS first — total cost divided by contribution per order — and judge campaigns against that, not against a figure quoted in a case study.
QWhy is my true ROAS so much lower than Ads Manager's?
Three causes, usually stacked. The platform counts conversions inside its own attribution window, including view-through and modelled ones. It reports the value it was sent, which often includes shipping and tax. And it never hears about refunds. Add a low match rate on your side and the gap widens further, because revenue you could not attribute is missing from your numerator entirely.
QShould refunds be subtracted on the refund date or the order date?
The order date, for campaign evaluation. A refund corrects a sale that already happened, so netting it against the week it was processed makes a good week look bad and a bad week look fine. Finance may book it on the refund date for cash reporting — that is a different question from which campaign to fund.
QDo chargebacks belong in the ROAS calculation?
Yes, and with their fees. A chargeback removes the revenue and adds a cost, so a campaign with a high dispute rate is being flattered twice by a gross number. Because disputes land months later, treat them the way you treat late refunds: re-read recent periods rather than freezing them.
QShould I include agency fees and tools in ad spend?
Include them if you are asking whether the channel pays for itself, exclude them if you are asking whether an ad set is worth more budget today. Both are valid; mixing them is not. Whichever you choose, keep it stable over time, because a denominator that changes definition makes your trend line meaningless.
QHow do I calculate true ROAS when my sales cycle is weeks long?
Report by cohort instead of by day. Take a week's spend and follow all the revenue those clicks eventually produce, rather than dividing today's revenue by today's spend. A same-day ratio on a long cycle always reads low early and high later, which makes it useless for deciding when to scale.
QWhat if a big share of my revenue can't be matched to any campaign?
Fix the match before you fix the budget. Unmatched revenue pushes every channel's ROAS down and inflates Direct, so a channel can look unprofitable purely because its buyers switched device, cleared cookies, or checked out on another domain. Check the match rate first — an unattributed bucket over roughly a fifth of revenue makes channel comparison unreliable.
QCan I send the corrected revenue back to my ad platform?
Not as a retraction. Conversion APIs are append-only, so an event that has been accepted stays in the platform's reported numbers. What you can control is what you send next: order values net of shipping and tax, deduplicated against the pixel, so the platform's optimisation at least learns from the value you actually keep.

Find the qualified leads your forms are currently throwing away.

Install PartialLeads on one landing page, send traffic, and compare what your CRM captured against what PartialLeads recovered and qualified.