Every marketing platform will tell you what it produced. None of them know what it was worth, because the number that decides it — gross profit, months later, net of returns — lives in the ERP. Here is how to connect the two, and where the honest limits are.
B2B revenue attribution connects marketing activity to the revenue and gross profit it eventually produced — not to clicks, form fills or MQLs. In B2B that means joining marketing data to order and margin data in the ERP, because that is the only place the money is actually recorded.
There is a familiar meeting in most B2B businesses. Marketing presents a deck showing traffic up, conversions up, cost per lead down. Finance looks at gross profit, which has not moved. Neither party is lying and neither can reconcile the other's numbers, because the two are measuring different things in different systems and the join between them has never been built.
The uncomfortable part is that marketing's numbers are usually correct and simply not decision-useful. A conversion is a proxy. It was invented because the real outcome was unmeasurable, and in B2B it remains a proxy for something that happens months later, in a different system, to a different set of people.
The attribution machinery most platforms provide was built for ecommerce, where someone clicks an ad and buys twenty minutes later in the same browser session. Four things about B2B break it:
Channels get judged on a metric that is measured early, in the wrong system, on the wrong entity (a session rather than a company), and stops counting long before the value arrives. It is not surprising the answer is wrong; it would be surprising if it were right.
The instinct is to try harder at tracking — more pixels, longer windows, better identity resolution. In B2B this mostly produces expensive noise.
The productive move is to change the unit of analysis. Stop trying to attribute orders to sessions, and start attributing customers to sources. A company is a stable entity that exists in both systems. It has one record in your ERP, it accumulates orders and gross profit over years, and it only needs to be connected to its origin once.
That reframing makes the problem tractable, because you no longer need to observe a journey. You need one durable field: where this customer came from.
Everything downstream depends on this and it is where most implementations quietly fail. The requirement is that every customer record in NetSuite carries an immutable acquisition source, set once at creation and never overwritten.
Three rules make it work:
NetSuite's standard leadsource field on the customer record is usually adequate. If it is
already used for something else, add a custom field — but add it before you need the history, because
this data cannot be reconstructed retrospectively.
Before attributing anything to a channel, establish how much of your revenue is actually new business. In most established B2B businesses the honest answer is 5–15%, and that single number reframes what marketing can plausibly be held responsible for.
-- New vs existing customer revenue and gross profit, by month. WITH first_order AS ( SELECT t.entity AS cust, MIN(t.trandate) AS first_order_date FROM transaction t WHERE t.type = 'SalesOrd' AND t.voided = 'F' GROUP BY t.entity ), monthly AS ( SELECT TO_CHAR(t.trandate, 'YYYY-MM') AS month, t.entity AS cust, SUM(tl.rate * ABS(tl.quantity)) AS revenue, SUM((tl.rate * ABS(tl.quantity)) - tl.costestimate) AS gp FROM transaction t JOIN transactionline tl ON tl.transaction = t.id WHERE t.type = 'SalesOrd' AND t.voided = 'F' AND tl.mainline = 'F' AND tl.taxline = 'F' AND t.trandate >= ADD_MONTHS(CURRENT_DATE, -24) GROUP BY TO_CHAR(t.trandate, 'YYYY-MM'), t.entity ) SELECT m.month, COUNT(DISTINCT CASE WHEN TO_CHAR(f.first_order_date, 'YYYY-MM') = m.month THEN m.cust END) AS new_customers, ROUND(SUM(CASE WHEN TO_CHAR(f.first_order_date, 'YYYY-MM') = m.month THEN m.revenue ELSE 0 END), 0) AS new_revenue, ROUND(SUM(CASE WHEN TO_CHAR(f.first_order_date, 'YYYY-MM') <> m.month THEN m.revenue ELSE 0 END), 0) AS existing_revenue, ROUND(SUM(CASE WHEN TO_CHAR(f.first_order_date, 'YYYY-MM') = m.month THEN m.gp ELSE 0 END), 0) AS new_gp, ROUND(SUM(m.revenue), 0) AS total_revenue, ROUND(SUM(CASE WHEN TO_CHAR(f.first_order_date, 'YYYY-MM') = m.month THEN m.revenue ELSE 0 END) / NULLIF(SUM(m.revenue), 0) * 100, 1) AS pct_from_new FROM monthly m JOIN first_order f ON f.cust = m.cust GROUP BY m.month ORDER BY m.month
"New" here means first-ever order, which is the right definition for acquisition. If you also want reactivated dormant accounts, add a second flag for customers whose previous order was more than eighteen months earlier — they behave like new business commercially but shouldn't be charged to acquisition spend.
This is the one that changes budgets. Group customers by the month they first ordered and the source they came from, then measure what each cohort went on to be worth.
-- Customer cohorts by acquisition month and lead source, -- measured on lifetime gross profit rather than first order. WITH first_order AS ( SELECT t.entity AS cust, MIN(t.trandate) AS first_order_date FROM transaction t WHERE t.type = 'SalesOrd' AND t.voided = 'F' GROUP BY t.entity ), perf AS ( SELECT t.entity AS cust, COUNT(DISTINCT t.id) AS orders, SUM(tl.rate * ABS(tl.quantity)) AS revenue, SUM((tl.rate * ABS(tl.quantity)) - tl.costestimate) AS gross_profit FROM transaction t JOIN transactionline tl ON tl.transaction = t.id WHERE t.type = 'SalesOrd' AND t.voided = 'F' AND tl.mainline = 'F' AND tl.taxline = 'F' GROUP BY t.entity ) SELECT TO_CHAR(f.first_order_date, 'YYYY-MM') AS cohort_month, NVL(BUILTIN.DF(c.leadsource), 'Unattributed') AS source, COUNT(*) AS customers, ROUND(SUM(p.revenue), 0) AS lifetime_revenue, ROUND(SUM(p.gross_profit), 0) AS lifetime_gp, ROUND(SUM(p.gross_profit) / NULLIF(COUNT(*), 0), 0) AS gp_per_customer, ROUND(SUM(p.gross_profit) / NULLIF(SUM(p.revenue), 0) * 100, 1) AS gm_pct, ROUND(AVG(p.orders), 1) AS avg_orders, ROUND(AVG(CURRENT_DATE - f.first_order_date) / 30, 1) AS avg_months_held FROM first_order f JOIN perf p ON p.cust = f.cust JOIN customer c ON c.id = f.cust WHERE f.first_order_date >= ADD_MONTHS(CURRENT_DATE, -36) GROUP BY TO_CHAR(f.first_order_date, 'YYYY-MM'), NVL(BUILTIN.DF(c.leadsource), 'Unattributed') ORDER BY cohort_month, lifetime_gp DESC
Drop cohort_month from the SELECT and GROUP BY to get straight source-level totals.
Keep it in when you want to see whether a channel's quality is improving or degrading over time —
which is usually the more interesting question.
Watch the Unattributed row. If it is more than about a third of your customers, the
source stamping isn't working and every other number here is unreliable. Fix that before drawing
conclusions.
This is the part that justifies the whole exercise. Two channels, same twelve-month spend:
| Paid search | Referral / word of mouth | |
|---|---|---|
| Spend | £60,000 | £60,000 |
| New customers | 124 | 38 |
| Cost per customer | £484 | £1,579 |
| Lifetime revenue | £1,240,000 | £988,000 |
| Average gross margin | 19.2% | 31.4% |
| Lifetime gross profit | £238,000 | £310,000 |
| GP per customer | £1,919 | £8,158 |
| Return on spend (GP) | 4.0× | 5.2× |
On every metric a marketing platform reports — leads, customers, cost per acquisition, revenue — paid search wins comfortably and by a wide margin. On gross profit it loses.
The mechanism is not mysterious. Search traffic arrives comparing prices, converts on the cheapest option, and buys at a discount. Referral arrives pre-sold, negotiates less, and stays longer. Those are real and persistent differences in customer quality, and revenue-based measurement is structurally blind to all of them.
Once you can see this, the interesting move is usually not "cut paid search". It's "why does paid search convert at 19% margin?" Often the answer is a discount code in the ad copy, or a landing page leading on price — which makes it a fixable pricing problem rather than a channel problem. See finding revenue leakage in NetSuite for the margin side of that.
NetSuite holds the return; your ad platforms and payroll hold the investment. The join is manual and that is fine — it's a monthly spreadsheet, not an integration project.
Four numbers per source:
| Metric | Calculation | What it tells you |
|---|---|---|
| CAC | Total channel cost ÷ new customers acquired | What buying a customer here costs |
| GP per customer | From Query 2 | What one is worth, so far |
| GP payback | CAC ÷ (GP per customer ÷ months held) | Months to recover the spend — the cash-flow constraint |
| GP:CAC ratio | GP per customer ÷ CAC | Whether the channel is worth scaling |
Include people cost in channel cost. A channel that needs constant manual work is more expensive than its media spend suggests, and excluding the labour is how businesses over-invest in channels that quietly consume a full-time role.
Payback deserves particular attention in a distribution business, because acquisition spend and inventory compete for the same working capital. A channel with excellent lifetime economics and a fourteen-month payback may still be the wrong one to scale this year. That trade-off connects directly to how quickly your customers pay.
Four things this method does not do, worth stating before someone else does:
It won't give you multi-touch attribution. It assigns a customer to one origin. In reality they saw a LinkedIn post, met you at a trade show, and were referred by an existing customer. Single-touch is a simplification — but it is a transparent one, whereas multi-touch models in B2B allocate credit using assumptions dressed as evidence. Being visibly approximate beats being invisibly wrong.
It can't measure brand or demand creation. The activity that made someone type your name into Google six months later gets recorded as "direct". Direct is not a channel; it's a bucket of things that worked earlier and can't be traced. Don't let anyone conclude that brand spend does nothing because it doesn't appear here.
Young cohorts look worse than they are. A customer acquired last month has one order. One acquired three years ago has forty. Only compare cohorts at the same age, or you will conclude that every channel has recently stopped working.
It depends entirely on the source field. All of it. If nobody stamps the source at first contact, or if someone helpfully "tidies up" the field, the entire analysis becomes fiction. That's a process discipline problem, not a data problem, and it's the one worth investing in first.
None of these limits stop the method being far more useful than what it replaces. Going from "cost per lead is down 12%" to "referral produces customers worth 4× more gross profit than paid search" is a change in the quality of the conversation, not just the precision of the number.
Revaion Demand connects campaign performance across search, web, email, social and paid media to the customers, orders and gross profit already inside NetSuite — one weekly answer, not five dashboards.
See how Revaion Demand works →Book a 30-minute discovery call. We'll walk through how Revaion attributes gross profit — not just revenue — back to source, and what that changes about your spend.
Or email [email protected]
We use your email only to reply to this enquiry. See our privacy policy.