How to use Building Contribution Margin-Adjusted LTV From Shopify Order Data

An operational walkthrough for turning raw Shopify orders into a contribution margin-adjusted LTV curve — the fields to pull, the costs to attach, and the cohort math to trust.
Building Contribution Margin-Adjusted LTV From Shopify Order Data
The end-to-end process of turning raw Shopify orders into a per-cohort LTV curve that reflects true unit economics, not gross revenue.
Contribution margin-adjusted LTV (CM-LTV) is the version of lifetime value you can actually spend against. Instead of stacking gross order value across a customer's history, you subtract the variable costs a Shopify order really carries — variant-level COGS, real outbound shipping, payment fees, discounts, and refunds — then sum the surviving margin per acquisition cohort over time.
Building it from Shopify data is mostly a data-plumbing job: pick an extraction method, attach costs the export doesn't include, exclude orders that pollute the numerator, and roll the cleaned per-order CM into a monthly cohort curve. Done well, the resulting LTV number holds up in a board meeting and against your payback-period model.
Most Shopify stores in the €1M–€15M range are still running LTV off gross revenue, usually because that's what the admin dashboard hands them for free. The problem shows up the moment you divide it by CAC: the ratio flatters everything, and paid channels look far healthier than they are.
This guide walks through the four blocks of work it takes to fix that on Shopify data specifically: pulling the right fields, attaching variant-level COGS, subtracting the costs Shopify doesn't compute for you, and rolling everything into a cohort curve you can chart. Each block has a dedicated spoke page if you want to go deeper on the mechanics.
Step 1: Pulling the right fields from Shopify
Before you touch a spreadsheet, pick your extraction method. The three realistic options are the Shopify Admin CSV export, the Bulk Operations API, or a warehouse sync (Fivetran, Hightouch reverse, a native connector). The right choice depends on order volume and how often you need to refresh the model.
For a store doing under ~50k orders/year, the Admin export is fine for a quarterly refresh. Above that, or if you need weekly cohort updates, jump straight to Bulk Operations or a warehouse sync — the CSV export truncates line-item detail and loses refund timing. There's a dedicated comparison of Shopify Admin export vs Bulk Operations API vs warehouse sync if you're on the fence.
Whichever route you pick, you need these fields at minimum: order_id, customer_id, created_at, financial_status, currency, subtotal_price, total_discounts, total_shipping_price_set, total_tax, and — critically — line_items with variant_id, quantity, price, and discount_allocations. On the customer side, first_order_date (or the derived earliest created_at per customer_id) becomes your cohort key.
Exclude before you aggregate
Draft orders, test orders (usually tagged or from a specific staff email), and gift-card sales all need to come out before anything else. Gift cards are a liability, not revenue, and test orders will silently pull your average CM down if a colleague ran a €0.01 checkout twelve times. Filter on financial_status IN ('paid','partially_refunded','refunded') and drop line items where product_type = 'Gift Card'.
Step 2: Attaching COGS at the variant level
Shopify has a `cost` field on the variant, but most stores either don't populate it, populate it once and never update it, or use landed cost inconsistently across SKUs. That's the single biggest source of error in a CM-LTV model.
Build a variant-cost table externally — a Google Sheet keyed on variant_id or SKU is fine to start — and left-join it onto every line item at query time. Update it monthly if your COGS moves with FX or supplier pricing. If you're missing the variant.cost field entirely, there's a spoke on mapping variant-level COGS to Shopify line items without a cost field that covers the SKU-parsing fallbacks.
Cumulative CM-adjusted LTV by month since first order (illustrative apparel cohort)
Gross-revenue LTV
CM-adjusted LTV
The gap in that chart — roughly 3.1× at month 24 — is exactly what a gross-revenue LTV hides. If your paid-media team is bidding against the top line, they're overpaying by that same multiple. This is the mechanism behind why gross-revenue LTV overstates LTV:CAC by 2-3× and why finance quietly disagrees with growth on channel ROI.
Step 3: Subtracting real shipping, payment fees, and discounts
The Shopify order object gives you `total_shipping_price_set` — that's what the customer paid, not what shipping actually cost you. Real outbound cost lives in your carrier invoices (Sendcloud, ShipStation, DHL portal) and needs to be joined in by shipping zone and weight tier. Netting the customer-paid amount against the invoiced amount is what the spoke on real shipping cost vs customer-paid shipping in Shopify exports walks through in detail.
Payment fees are more mechanical: Shopify Payments takes a percentage plus a fixed fee per transaction, and third-party gateways (PayPal, Klarna, Mollie) each have their own schedule. Allocate the fee on the settled amount, per order. Discounts are already in `total_discounts` — the tricky part is attributing automatic vs code-based discounts to the right CM line, which matters when you later slice LTV by acquisition offer.
Typical per-order contribution margin ranges by DTC vertical (Shopify stores, €1M-€15M revenue band)
| Vertical | Blended CM % | Avg CM / order | Biggest CM leak |
|---|---|---|---|
| Apparel & accessories | 28-38% | €18-€32 | Returns (15-30% return rate) |
| Beauty & skincare | 45-60% | €22-€40 | Sample/GWP allocation |
| Home & lifestyle | 30-42% | €25-€55 | Outbound shipping on bulky SKUs |
| Consumer electronics | 12-22% | €15-€38 | Payment fees + warranty accrual |
| Food & supplements | 35-50% | €10-€22 | Subscription discount stacking |
Two more line items complete the stack: refunds and multi-currency. Refunds need to hit the cohort of the original order, not the month they were processed — otherwise your recent cohorts look artificially healthy. And if you sell in more than one currency, convert every order to a base currency at the order's created_at date, not today's rate; there's a spoke on multi-currency Shopify stores in a single CM-LTV model that covers the FX policy choices.
Step 4: Rolling per-order CM into a cohort LTV curve
Once every row in your orders table has a defensible `contribution_margin_eur` column, the cohort math is straightforward. Assign each customer to their first-order month, then for every order compute `months_since_first_order = date_diff(order.created_at, customer.first_order_month, 'month')`. Group by cohort_month and months_since, sum CM, and divide by the cohort's headcount.
The output is a triangle: rows are acquisition cohorts, columns are months of tenure, cells are cumulative CM per customer. Reading down the first column gives you first-order CM by cohort (a proxy for cohort quality). Reading across a row gives you the LTV curve for that cohort. The mechanics of translating that triangle into a smoothed curve are covered in rolling per-order CM into a monthly cohort LTV curve.
The payback line matters more than the terminal number
Don't obsess over 24-month LTV until you know your CM payback month — the month at which cumulative CM per cohort customer exceeds blended CAC. For most Shopify apparel brands that's month 4-9; for beauty subscription models, month 1-3. If your payback is drifting later cohort-over-cohort, that's the leading indicator, not the terminal LTV.
Frequently asked questions
None of them, alone. The Admin CSV export covers orders and line items but drops nested cost data and can lose refund detail. Bulk Operations API is the cleanest single source for a script-driven refresh. A warehouse sync (Fivetran, Airbyte, native connectors) is the right choice above ~50k orders/year or when you need daily refreshes.
Build a separate variant-cost table in a sheet or a warehouse table keyed on variant_id or SKU, and left-join it at query time. This is the standard fallback and is more maintainable than trying to backfill Shopify's `cost` field retroactively — supplier pricing changes over time and Shopify only stores the current value.
Real carrier cost, always. Customer-paid shipping is a revenue line; it belongs above the CM line, not as a substitute for the cost line. Join outbound cost from your carrier invoices by zone and weight tier, and treat the delta between paid and real as a separate CM contributor.
Net the refunded CM against the ORIGINAL order's cohort and tenure month, not the month the refund was processed. Otherwise refunds silently inflate recent cohorts. For partial refunds, prorate the CM impact by the refunded line-item value.
No. Gift-card sales are a liability until redeemed. Exclude gift-card line items on the sale side, and count them as revenue only on the redemption order (where you'll see them as a discount or store-credit line). Otherwise you double-count during Q4.
Monthly is the norm for reporting; weekly if you're actively bidding paid media against it. Update the variant-cost table monthly and the shipping-cost reference table whenever your carrier contract changes. FX rates for multi-currency conversion should be locked at order-date, not refresh-date.
Gross-revenue LTV typically overstates the number a growth team can spend against by 2-3× on Shopify stores, because it ignores COGS (40-70% of order value), real shipping (5-12%), payment fees (2-4%), and refunds (5-15%). The ratio compresses further once you also strip discounts and returns.
Treat every recurring charge as its own order in the cohort triangle, using the original acquisition month as the cohort key. The CM per subscription order is often lower than the initial order (no acquisition offer, but also often free-shipping thresholds), so segment subscription cohorts separately from one-time-purchase cohorts.
For stores under ~30k orders/year, yes — a Google Sheet with a monthly Bulk Operations export and a variant-cost lookup gets you 80% of the way. Above that, sheet performance breaks down and a warehouse (BigQuery, Snowflake, Postgres) becomes the pragmatic choice.
Two sanity checks. First, sum a full year of contribution_margin_eur across all orders and reconcile it against your P&L gross margin line — they should be within 3-5%. Second, plot cohorts on the same axes: if the shapes are erratic month-over-month rather than trending, you likely have a cost-attachment bug rather than a real business signal.
Track CAC, channels, and funnel conversion in one place
Metricuno connects ad spend, funnel events, and revenue so you can see CAC by channel, cohort, and campaign — without stitching together five tools.