Back to Case Studies

The Profit per Order Nobody Can See

Revenue per order is easy. Profit per order is not. How we built an order-level P&L for a UK retailer selling across Shopify, Amazon, eBay and TikTok Shop.


Case Study Highlights

  • Systems: Three Shopify stores plus Amazon, eBay and TikTok Shop, with Linnworks as the source of truth for orders, stock and costs, and a custom profit and loss engine sitting across all of it

  • Problem: Every channel reported revenue per order. None reported profit per order. The costs that decide whether an order made money were scattered across seven systems, so nobody could answer the simplest question in retail: did we make anything on that?

  • Solution: An order-level P&L that takes what the customer actually paid and subtracts every cost attached to that order, down to live ad spend read from Google Ads and Microsoft Advertising. Every rate is a setting the client controls, so a carrier increase takes a minute and doesn’t need us

  • Result: Every order on every channel now carries a profit figure in pounds and a margin percentage, shown both before and after marketing and recurring costs. The orders that lost money are visible the day they happen, instead of a quarter later

An ecommerce retailer working out profit per order sits at a cluttered desk, one hand on their forehead and a pen in the other, looking past a calculator at printed reports marked up with red question marks. A laptop beside them shows an accounting dashboard of bar and pie charts. On the whiteboard behind, the columns of a profit and loss calculation, revenue, COGS and net income, are crossed out and rewritten, with "THE MATH ISN'T MATHING" across them in red capitals.

Ask an online retailer what an order was worth and you’ll get an answer immediately. It’s on the order in the Shopify admin, it’s in every report, and it’s the number the business is run on. Revenue.

Ask what the same order made, and the room goes quiet.

If you’ve ever had the suspicion that some of your orders lose money, without being able to point at which ones, that suspicion is usually right. The reason you can’t prove it isn’t carelessness. The numbers you’d need are sitting in seven different places, and none of them is the place you’re looking.

That gap is where a UK retailer we work with at SaaS Glue had been operating for years. They run three Shopify stores of their own and sell alongside them on Amazon, eBay and TikTok Shop. Thousands of products, mostly small and inexpensive, at the kind of volume where a few pence per order is the difference between a good year and a flat one.

Every channel told them what came in. None of them could tell them what stayed.

The second half of the work, using product grading and stock cover to move prices automatically, is a separate part of the system and a separate story.

Gross margin isn’t profit

Most retailers reach for gross margin. Take what the order sold for, subtract the cost per item recorded against each variant, call the difference margin.

You know what’s on the menu. You don’t know what the meal cost the kitchen.

Every platform will show you a version of this, and Shopify’s own documentation is explicit about the formula its profit reports use, price minus cost over price, and equally explicit that profit is only reported for variants that had a cost per item recorded at the time they sold. It’s an honest number for what it measures.

On a small order it’s also close to meaningless.

Here’s the shape of it. Round figures, and arithmetic people do in their heads far more often than any system prints it.

A £6 order. The item cost £3. Three pounds of margin on a six pound sale, 50%, healthy enough that nobody looks further.

Then the rest of the order lands on it.

Illustrative order£
Customer paid6.00
VAT−1.00
Cost of goods−3.00
Payment fee (percentage plus fixed)−0.34
Carrier−1.29
Packaging−0.08
Per-order cost of the systems−0.05
Share of advertising−0.60
Actually made−0.36

A fixed payment fee doesn’t care how big the order is: it’s the same whether you sold a doormat or a sofa, which is why small baskets take it worst. The carrier charged what the carrier charges regardless of what the customer paid for delivery. And somewhere upstream, an advertising platform was paid to bring that customer in at all.

An order that looked like it earned £3 earned nothing. Nothing in the business would say so. The order completed, the customer was happy, the revenue line went up.

That’s survivable when it happens occasionally. At several thousand orders a week it decides the year.

Where the money goes

Getting to a true figure is mostly the work of finding every layer and attaching it to the right order. Each one is easy to describe and awkward to source.

Tax first

Part of what the customer paid was never the retailer’s money. VAT was passing through on its way to HMRC, and it comes off before anything else.

Everyone knows that one.

Here’s the one that catches people, and it only bites if you sell at more than one VAT rate. If everything you sell is taxed the same, this never touches you, and you can skip to the next bit.

If you sell anything at a lower or zero rate and you charge for delivery, the tax on that delivery charge is wrong on every one of those orders.

We assumed delivery was taxed at the normal rate, because nearly every platform behaves as though it is. HMRC’s guidance on postage and delivery disagrees in one line: an additional charge for delivery follows the liability of the goods. Delivery is a hitchhiker.

Nothing flags the error, in whichever direction the platform guessed, and the report carries it downstream looking perfectly confident. The rate also depends on where the parcel is going, so the engine works it out from the delivery address rather than taking the channel’s word for it.

How they paid

This is the one most calculations get wrong by guessing, and it’s the easiest to fix once the order is the unit of analysis.

Shopify Payments, an accelerated checkout like Shop Pay, a buy-now-pay-later provider such as Klarna or Clearpay, and a cross-border checkout all cost different amounts, and the contracts behind them are structured differently. Some are a flat percentage. Some are a percentage plus a fixed amount per transaction. Some change rate above a certain basket value.

Because the engine works from the order, it doesn’t have to assume any of this. Shopify records the payment gateway on the order itself, so the engine reads which one the customer used and applies that contract’s fee structure to the amount they actually paid.

A Klarna order and a Shopify Payments order of identical value land on different numbers, as they should.

In several territories the marketplace charges a digital services tax on top of its own fees, so the fee gets taxed on its way to you. A tax on a fee. It’s a small percentage of a small number and it has never once made it into anybody’s spreadsheet.

Delivery, twice

One number arrives at checkout. The other arrives on a carrier invoice weeks later. They’re easy to confuse and they rarely match.

What the customer paid for delivery is revenue, and above a certain basket value this retailer charges nothing at all. What the carrier charges arrives later and doesn’t care about any of that.

The calculation has to take one as income and the other as cost, and treat a free-delivery order as income of zero against a cost that is very much not zero.

Working from the order makes the cost side honest. The engine reads the shipping service actually assigned to that parcel, where it went, and the day it was dispatched.

The date matters more than it sounds. Royal Mail runs a peak surcharge on its business tracked services from November into January, so the same parcel costs more in December than in March. The parcel hasn’t changed, the invoice has, and a date-aware calculation applies the surcharge only inside the window where it genuinely applies.

Marketplaces add their own version. A parcel fulfilled through the marketplace’s own shipping program costs something different from the identical parcel on the retailer’s carrier account. The customer sees no difference at all.

Packaging, once per parcel

Here’s a cost that only makes sense at order level.

A five-item order doesn’t go out in five parcels. It goes out in one, and the packaging is chosen by whatever the most awkward lump in the basket is.

So the packaging cost of an order is the cost of one container: the single box or bag big enough to hold everything in the basket. Any per-product calculation either over-counts this badly on multi-item orders or ignores it entirely.

Working from the order, the engine takes the items in the parcel and prices the packaging that would actually be needed.

Advertising and the monthly bills

This is the layer almost nobody includes, and for a business that buys traffic it’s often the one that decides the result.

The engine connects directly to Google Ads and Microsoft Advertising (which everybody still calls Bing) and reads what was actually spent over the period being looked at. Nobody exports a CSV and nobody has to remember to do it.

That spend is then spread across the orders in the same period, weighted by order value. A £100 order carries ten times the share a £10 order does, rather than the same share.

Recurring costs get the same treatment through a screen of their own. It lists every ongoing cost the business carries, from software subscriptions through to the services it pays for month after month, with the figure attached to each one and a single place to keep them current. Most businesses are paying for more tools than they think they are, and this is the screen where that stops being an abstraction.

Those totals are prorated by day and spread across that day’s orders the same way, weighted by order value.

Dividing by day means a quiet day spreads the same fixed cost over fewer orders, so every one of them carries more of it. That looks like volatility and it isn’t. The subscriptions were paid either way, so a quiet day genuinely did cover less of them, and smoothing that away would hide the one thing the all-in figure exists to show. No product decision gets made on that number anyway, because the margin before those costs sits alongside it and doesn’t move with volume.

Neither apportionment is a precise attribution of a specific cost to a specific customer, and we were careful not to present it as one. It’s a fair share of money the business genuinely spends, landing on the orders that spending existed to produce.

Timing is the reason it matters. Advertising and subscriptions are exactly the costs that surface months later inside a set of accounts, long after the trading decisions that made them worth paying.

Per-order fees

Order management, listing tools and the integrations between them each carry a per-order cost. Trivial in isolation, material at volume.

It belongs in the calculation for the same reason the fixed payment fee does. On small baskets, the flat costs are the ones that decide the outcome.

Seven sources

It’s fair to ask why a business wouldn’t simply work this out. The answer is that the arithmetic is easy and the data assembly is not.

What the customer paid lives on the sales channel. What the goods cost lives in the inventory system. Which parcel service was used lives on the dispatch record. Carrier rates live in a rate card that changes. Payment rates live in commercial contracts. Packaging costs live in supplier invoices. Advertising spend lives in two separate ad platforms.

Seven sources. For one order. Then again for the next one, several thousand times a week, and again next month when a rate moves.

Someone in most businesses has built a version of this in a spreadsheet at least once. It’s right on the day it’s built, and then it sits there quietly fibbing while everyone keeps trusting it, because the costs underneath it keep moving and the spreadsheet doesn’t.

That’s the same failure mode we see whenever a business cannot trust the numbers on its own screens: an absence of trustworthy data at the exact moment someone needs to decide something.

What we built

The stack

The engine reads orders from Linnworks, which already pulls them in from the Shopify stores and the marketplaces, groups the line items of each order together, and works down every layer above: what the customer paid, minus tax, goods, payment, carrier, packaging, systems, and a share of advertising and subscriptions.

What comes out is a profit figure in pounds and a margin percentage for that specific order.

Margins are color-banded so the shape of a day is visible at a glance, and any order can be opened to see exactly which costs produced its number. Orders still in progress are included alongside dispatched ones, so the screen shows today as well as last month.

Two margins

Every order carries its margin twice. Once with advertising and recurring costs included, and once without.

That sounds like a small thing and it’s the most used feature in the system.

The figure without those costs tells you whether the product economics work: whether this item, sold at this price, to this place, on this channel, covers what it costs to buy and deliver. An accountant would call it contribution margin.

The all-in figure answers a harder question. Did the orders taken today actually cover everything it costs to run this business today, advertising and software included?

That’s the question a business normally waits on its management accounts to answer, one quarter at a time, by which point the period being explained is finished and nothing can be done about it. Having it daily changes what it’s for. It stops being a report and starts being a control.

A catalog can be perfectly healthy on the first number and underwater on the second, and the fix for each is completely different. Collapsing them into a single figure hides which problem you have.

Settings, not tickets

This is the part that keeps the screen honest a year later.

Payment rates by method and store, carrier rates by service, packaging costs by type, and per-country variations are all stored as editable settings the client controls. When a carrier raises its rates, someone updates a figure and the P&L reflects it on the next run. Nobody files a ticket and nobody waits for us.

A rate change should only ever apply from the point it takes effect. An order that shipped last November was costed at last November’s prices, and adding this year’s increase must not quietly rewrite what that order made.

Otherwise last year’s profit moves every time someone updates a setting, and a figure that changes retrospectively is a figure nobody can plan against.

So the parts of the calculation that vary over time are driven by the order’s own date, the peak surcharge included.

We take the same view here as we do about the software that runs a back office generally. A system the client can’t adjust without calling the people who built it is a system that goes stale, and a profit figure that has gone stale is worse than no figure, because it’s trusted.

The missing-cost list

A profit calculation is only as good as the cost prices underneath it, and in a catalog of this size there are always products with no cost recorded at all. An order containing one of those shows an excellent margin and means nothing.

Shopify documents this limitation on its own reports, which simply omit any variant that had no cost recorded when it sold. Omitting them keeps the report honest and leaves the merchant with no idea how much of the catalog is missing.

So a separate screen exists purely to list products with no cost price, and it stays there as a standing job long after the first cleanup. It’s the least interesting screen in the system and the only reason any of the others are true.

We usually find the same gap when we look across a client’s stack for the first time. The expensive problem is the missing inputs underneath a report, which quietly make it untrustworthy long before anyone questions it.

The unit

The unit of analysis was the design decision the whole system turned on.

A per-product margin estimate has to assume how the item will be paid for, how it will be shipped, and what it will go out in.

An order-level P&L doesn’t assume any of it. The customer has already chosen how to pay, the parcel has already been assigned a service and a destination, the basket already determines the packaging, and the amount paid already reflects any discount code applied at checkout.

That last point mattered more than we expected. The revenue figure is what actually landed in the account, discount and all, so the business could finally see the real cost of a promotion while it was running.

What we’d do differently

Three things, and the first is the softest number in the whole stack.

Packaging depends on knowing how big the items are, and product dimensions are the least reliable field in any catalog. Where they’re missing or wrong, the engine is pricing a parcel it can’t fully see. It’s a small cost, and it’s the one we’d tighten first, which means a dimensions cleanup belongs at the start of a job like this rather than after the first output.

The second is the apportionment. Weighting advertising by order value beats splitting it flat, and it still isn’t attribution. A high-value order that arrived through search cost nothing to acquire and gets charged as though it did. Apportioning by channel first, then by value inside the channel, would land closer to the truth, and that’s the version we’d build next.

The third isn’t a flaw so much as a cost we chose. Every rate in the system is a setting someone has to remember to update, which is the right trade against a system only we can change. But a settings screen with a stale figure in it looks exactly like a settings screen with a current one. A nudge when a rate hasn’t been touched in a year would be worth building, and isn’t there yet.

What actually changed

The change can’t honestly be written up as a percentage saving, and it would be easy to dress it up as one. On other jobs the hours saved are countable and the arithmetic makes the case on its own.

This one is different. What changed is what the business is able to know.

Every order, on every channel, now carries a profit figure in pounds and a margin percentage, shown with and without marketing costs, and the orders that lost money are visible the day they happen instead of being averaged into a quarter that looked fine.

Before this existed, that question could be answered for a handful of orders by hand, over an afternoon, and the answer went stale as soon as a rate moved. Now it’s answered for every order, continuously, and it stays right when the costs underneath it change.

What that unlocked is ordinary and valuable. The real cost of a free-delivery threshold became something that could be checked instead of argued about, and the product economics could finally be separated from the marketing spend, which is the distinction that tells a business whether to fix its buying or fix its advertising.

The deeper change is in how fast a decision can be made. A figure that’s close to right today is worth more than a perfect one next quarter, because by next quarter the decision has already been made by default and already been paid for.

When the numbers arrive late, a business runs on instinct and argument. Someone is sure a particular bundle is losing money, someone else is sure it’s fine, nobody can prove either, and the thing carries on until an accountant mentions it months later.

When the numbers arrive daily and everyone is looking at the same ones, that conversation takes a minute and ends in a decision. A promotion that isn’t working gets stopped in its first week instead of its last, and a channel that looked busy and paid badly gets less attention and less stock.

None of that requires the figures to be perfect. It requires them to be real, and current, and the same for everyone in the room.

In most of the work we do, the expensive problem turns out to be a decision made over and over on the wrong information, the way a fulfillment system that almost works quietly charges for every order that misses the happy path, or the way compensation goes unclaimed simply because finding the evidence takes too long.

Get the information right and the decisions correct themselves.

Check it yourself

If you’d rather read the source material than take our word for any of the above, three documents settled most of the arguments in this post: HMRC’s notice on VAT for postage and delivery, Shopify’s description of what its profit reports do and don’t include, and Royal Mail’s published peak surcharges. All three are short, and all three are more useful than any app’s pricing page.

Frequently Asked Questions – Profit per Order

What is profit per order?
Profit per order is what's left of the money a customer actually paid once every cost attached to that order has come off it: tax, the cost of the goods, the payment processing fee, what the carrier charged, the packaging, the per-order cost of the systems that handled it, and a share of the advertising and recurring costs the business paid over the same period. Gross margin stops after the cost of the goods, which is why an order can show a healthy margin and still lose money. The unit is what makes it work. Calculated per order rather than per product, nothing has to be assumed about how the customer paid, how the parcel shipped or what it went out in, because all of that is already recorded on the order itself.
Doesn't Shopify Analytics already show me my profit?
Shopify's profit margin report works from the cost per item you entered against each variant, which makes it a gross margin report. That's a useful number and it isn't profit. It doesn't subtract your Shopify Payments or Klarna fees, the carrier cost, the packaging, the per-order cost of your systems, the advertising that brought the customer in, or the VAT portion of the price. On higher-value orders the gap is small enough to live with. On low-value, high-volume orders it is often the entire result. Third-party apps approximate parts of this, generally by applying blended assumptions for shipping and packaging, which is exactly where the accuracy is lost.
Does this work on Shopify Plus, or standard Shopify?
Either. Nothing in the calculation depends on the plan. It reads orders, payment gateways and line items, which every Shopify plan exposes through the admin API. The same applies to the marketplaces alongside it. What changes the difficulty is how many channels you sell on and how completely your cost data is recorded, not which tier you are on.
Why does it matter which channel the order came from?
Because almost every cost in the stack changes by channel. The payment gateways are priced differently, marketplaces charge their own fees and in some territories tax those fees, and the shipping service used can differ for an identical parcel. The same basket can be comfortably profitable on one channel and lose money on another, which is impossible to see if you only track margin at business level.
How can you attribute advertising spend to an individual order?
You can't, precisely, and we haven't claimed to. The engine takes real spend from the ad platforms for the period and spreads it across the orders in that period, weighted by order value, so a £100 order carries ten times the share a £10 order does. It is a fair distribution of a cost the business genuinely pays, not a claim about which ad produced which sale. It is also why every order carries its margin twice: the figure you rank products and channels on excludes advertising entirely, so nobody has to accept the apportionment to use it. Weighting by channel before value would be closer still, and is the version we would build next.
Doesn't a quiet day make every order look unprofitable?
On the all-in figure, yes, and that is the correct answer rather than a fault in the method. Subscriptions and ad budgets do not shrink because you took fewer orders, so a quiet day spreads the same fixed cost over fewer orders and every one of them really did carry more of it. Both are weighted by order value, so a large order always takes a bigger share than a small one, but the day's total has to land somewhere. A system that smoothed that away would be hiding exactly what the all-in number exists to show: whether the day's trading paid for the day's business. The margin before those costs is unaffected by volume, and that is the one you judge a product on. If your volume swings hard by weekday, read the all-in figure across a rolling week rather than a single day.
What is the hardest part to get right?
Shipping, by a distance. You have to separate what the customer paid from what the carrier will charge, use the shipping service and destination actually recorded on the parcel, and account for rates that change across the peak season. Most rough calculations apply one blended shipping figure to everything, and that single shortcut is usually enough to make the whole output unreliable.
We don't have reliable cost prices on everything. Is this still worth doing?
Yes, and the first thing it will do is tell you exactly where those gaps are. Nearly every catalog we look at has products with no cost recorded, which produce orders showing a perfect margin and meaning nothing. Surfacing that list is usually the first real output, and it tends to be quick to fix once someone can see it.
What stops the figures going stale when rates change?
Nothing automatic, which is the honest answer and a trade we made deliberately. Carrier, payment and packaging rates are settings the client controls, so a rate change takes a minute and does not need us. The risk is the obvious one: a settings screen with a stale figure looks exactly like one with a current figure. Rates are dated, so an order that shipped last November keeps last November's costs and an update never rewrites history. A prompt when a rate has not been touched in a year would close the rest of the gap, and it isn't built yet.
How long does something like this take to build?
It depends far more on your data than on the calculation. If cost prices and packaging types are already recorded, your orders carry the payment method and shipping service, and your carrier and payment rates are documented, it moves quickly. If they're not, that work comes first. We start with a discovery phase to see what is actually recorded and what will need sourcing, and quote against that instead of against a guess.
Do we have to change our systems to use this?
No. This sits on top of what is already there. The sales channels stay as they are, the order and inventory system stays the source of truth, and the P&L engine reads from both and replaces neither. That's the usual shape of our work: add the layer that was missing, leave the systems that work alone.

Not sure which of your orders actually make money? Drop us a message. No pitch, just a conversation.

You can also see more of our work or read our manifesto to understand how we approach these problems.