How to Report Shopify Revenue by UTM Campaign Without Leaving Shopify

|Dan Giura
How to Report Shopify Revenue by UTM Campaign Without Leaving Shopify

TL;DR

Shopify already holds your campaign revenue. Its reports show it one attribution model at a time, and ShopifyQL, which runs inside the admin, shows every model at once. The campaign_sales schema puts four models' order counts and sales in one row per campaign, which is the comparison a campaign review needs. The sales schema adds a second campaign value, attached to the order itself, that you can group net sales by with no model involved. Two dates matter: Shopify deprecated the old campaign sales metrics in July 2026, and campaign data only exists for orders with a storefront session behind them.

Key Takeaways

  • Open a data exploration at Analytics → Reports → New exploration, write ShopifyQL and click ▶ Run; Shopify says creating one "is available to merchants on all Shopify plans" [2].
  • Query campaign_sales for campaign_first_click_total_sales, campaign_last_click_total_sales, campaign_last_non_direct_click_total_sales and campaign_linear_total_sales, grouped by utm_campaign [6].
  • Any click has no column in campaign_sales. Add WITH ANY_CLICK_ATTRIBUTION to a sales query instead, and its campaign totals can add up to more than the store's [7].
  • Group net_sales by order_utm_campaign in the sales schema for one campaign value per order, no model applied [8].
  • Read the SHOW line of every saved campaign exploration: campaign_<model>_sales metrics are deprecated since 2026-07, replaced by campaign_<model>_total_sales [6].
  • When two campaign views disagree, compare order counts before money. Counts are the closer comparison; the sales metrics are defined differently.
  • Keep GA4's session campaign figures beside Shopify's. The app's server-side GA4 purchase carries the campaign values of the visit it matched.

If a report is enough, use the report

Most stores can answer "how much did this campaign sell" without writing anything. Shopify's channel performance report and its campaign drill-down cover where those views live and which model each one defaults to. The help center's marketing reports add "Performance by marketing campaign", grouped "by campaign first", and "Sales attributed to marketing", which counts Marketing-section activities and "externally managed efforts that use Urchin Tracking Module (UTM) parameters" [1]. Which plan gets which report is its own article.

This one starts where the reports stop, at the query. A report shows one attribution model per view. When you want four models in one table, or a campaign split no report offers, you write ShopifyQL. The steps, from Shopify's data explorations page [2]:

  1. From your Shopify admin, go to Analytics → Reports.
  2. Click New exploration.
  3. Write a query in the ShopifyQL editor and click ▶ Run. Shopify needs at least one metric before anything shows.
  4. Click Save. Name it after the question it answers.

Shopify says creating an exploration "is available to merchants on all Shopify plans" [2], and the Basic plan page describes "access to all reports, including the ability to create custom reports with data explorations" [4]. The Starter plan page lists marketing reports but doesn't mention explorations [3], so a Starter store should look for the New exploration button before planning around it. Shopify's developer reference lists the "ShopifyQL editor in the Shopify admin" first among the places ShopifyQL runs [5]. No app, no API key, no export.

Which orders will the query never see?

Any order with no storefront session behind it. The campaign_sales schema dates each row by "The day an order from a campaign-driven session was processed", and its utm_campaign dimension is "The campaign name from the UTM tracking on the session" [6]. No session, no campaign.

Shopify's order conversion summary names two kinds of order with no session: orders that began as draft orders, and orders from outside the online store, such as POS sales [9]. Renewal orders a subscription app creates without anyone browsing your store usually land there too. They count in total sales and never reach a campaign row. If you need them in an ad platform, that's a separate import job.

Two more gaps sit upstream of any query. Traffic without a utm_campaign value isn't attributed to a campaign at all, and the UTM tagging rules cover that rule along with the casing slips that split one campaign into two rows. Orders from logged-in and cross-device shoppers often land in Direct instead of the campaign that started them.

Then there's definition. Shopify credits campaigns by clicks into its own storefront sessions. Meta and Google credit their ads with their own windows and rules, so a Shopify campaign figure won't tie to either platform's, and no query will make it.

Campaign revenue isn't campaign profit either. order_utm_campaign lives in the same sales schema as gross_profit [8], which is where a product-margin check would start.

How do you put four attribution models in one table?

Query campaign_sales. Its money metrics are named by model, campaign_first_click_total_sales, campaign_last_click_total_sales, campaign_last_non_direct_click_total_sales and campaign_linear_total_sales, and each has a matching _order_count [6]. Group them by utm_campaign and every campaign gets one row with four answers:

FROM campaign_sales
  SHOW campaign_last_non_direct_click_total_sales,
    campaign_last_click_total_sales,
    campaign_first_click_total_sales,
    campaign_linear_total_sales,
    campaign_last_click_order_count
  GROUP BY utm_campaign WITH TOTALS
  DURING last_month
  ORDER BY campaign_last_non_direct_click_total_sales DESC
  LIMIT 25

The shape follows Shopify's own examples for this schema [6]. WITH TOTALS adds a __totals column for each metric across the whole result [7], and the sort decides which 25 campaigns survive the LIMIT. Swap utm_campaign for utm_source or utm_medium to see the same split one level up.

Run the arithmetic on an illustrative store with three spring campaigns:

utm_campaign (illustrative) First click Last click Last non-direct Linear
spring-prospecting $9,400 $3,100 $4,800 $6,200
spring-retargeting $2,200 $7,900 $8,300 $4,900
spring-newsletter $1,100 $4,600 $5,000 $2,700

Prospecting earns three times as much under first click as under last click. Retargeting loses about 72% of its credit going the other way. The columns don't sum to one total because each model hands some orders to touchpoints outside these three rows. How Shopify's attribution models work explains what each one credits; the point of the query is that you see all four at once instead of flipping a report between them.

Linear order counts come back as decimals, because "linear attribution shares credit across campaign sessions" [6]. And campaign_sales has no any-click column at all. For any click, put the WITH modifier on a sales query:

FROM sales
  SHOW net_sales
  GROUP BY utm_campaign WITH ANY_CLICK_ATTRIBUTION, LAST_CLICK_ATTRIBUTION
  SINCE -30d UNTIL today
  ORDER BY net_sales__any_click DESC

Shopify warns that ANY_CLICK_ATTRIBUTION "gives full credit to every touchpoint, so its attributed totals can add up to more than the metric's overall value" [7]. Read it as reach. This query reports net_sales, a different money column from the campaign_sales ones, so compare it with the four-model table by rank and leave the dollars alone.

Which campaign belongs to the order itself?

The sales schema carries two campaign fields. utm_campaign is the "UTM name of the campaign associated with the session", the value attribution models credit. order_utm_campaign is the "UTM name of the campaign associated with the order" [8]: one value per order with no model on top.

Grouping by the order's value gives a plain revenue split:

FROM sales
  SHOW net_sales, orders
  GROUP BY order_utm_campaign WITH TOTALS
  DURING last_month
  ORDER BY net_sales DESC

net_sales is revenue "after discounts and sales reversals, excluding taxes, shipping, duties, and fees" [8], so a refund in week three comes off the campaign that sold the order. Shopify notes a single order "can produce multiple rows if refunds or adjustments follow" [8]. The totals handle that; just don't count rows as orders. If the result includes a blank campaign row, that's orders with no order-level campaign value.

What Shopify doesn't say is which visit the order's campaign comes from. It could be the first tagged visit, the last one, or some other rule, and we found no Shopify page that settles it. Two people can pull two revenue figures for the same campaign straight from Shopify, one from each field. Test it on your own store before you report on it:

  1. Run a campaign_sales query showing campaign_first_click_order_count and campaign_last_click_order_count by utm_campaign for last month.
  2. Run the order_utm_campaign query above for the same month.
  3. For your three biggest campaigns, set the orders column beside both counts.

Order counts are the closer comparison, because the money columns are defined differently in each schema. Whichever model the order-level counts track most closely is the best description you'll get of that field on your store. If they track neither, make campaign decisions on the modelled columns and keep the order-level split as a reconciliation total.

A campaign report saved before July 2026 may not match one built today

Shopify deprecated the campaign_<model>_sales metrics, defined as "gross sales - discounts", along with campaign_<model>_average_order_value, both "Deprecated since 2026-07" [6]. The replacements are campaign_<model>_total_sales and campaign_<model>_total_average_order_value. Shopify's definition of the new sales metric reads, in full, "Total sales for the last click attribution model" [6]. It doesn't say what's inside, so don't assume it equals the old figure or the sales schema's total_sales.

In practice, an exploration saved in spring that still shows campaign_last_click_sales and a fresh one built on campaign_last_click_total_sales can disagree about the same campaign in the same month. Nothing in your tracking changed; the metric definition did. Before comparing months:

  1. Open each saved campaign exploration and read its SHOW line.
  2. If it names a campaign_<model>_sales metric, rebuild it on the _total_sales replacement and re-run the earlier months on the new metric too.
  3. Compare order counts first. They weren't part of the swap.

Keep the old exploration under a new name. Someone will ask where last spring's numbers came from.

Check it against GA4

A second campaign view that doesn't share Shopify's attribution logic catches tagging problems neither view shows alone.

On a store running WeltPixel Conversion Tracking, the app's server-side GA4 purchase carries the campaign values of the visit it matched, in GA4's own source, medium and campaign parameters, on every plan including the free one. A purchase from a visit with no campaign goes without; the app doesn't invent one. The app writes no campaign data into Shopify, so GA4's campaign figures and Shopify's are two independent counts of the same orders.

They won't match. GA4 builds its own sessions and applies its own attribution settings, and the Shopify-vs-GA4 revenue article lists the reasons. Compare rank order instead. If a campaign is your top earner in campaign_sales and tenth in GA4, check the tagging on both sides first; that's the gap worth an afternoon. When you need GA4's session detail beside Shopify order IDs, the BigQuery join does that, since ShopifyQL can't query GA4.

FAQ

Can I see revenue by UTM campaign in Shopify without an app?

Yes. The Performance by marketing campaign and Sales attributed to marketing reports show it [1], and a data exploration at Analytics → Reports → New exploration lets you query campaign_sales directly [2][6].

Why don't my campaign_sales totals match my total sales?

Only orders from a campaign-driven session appear in campaign_sales [6]. Staff-built draft orders and POS sales have no storefront session [9], and untagged traffic has no campaign name to group by.

What's the difference between utm_campaign and order_utm_campaign?

utm_campaign belongs to the session and is what attribution models credit; order_utm_campaign is one value attached to the order [8]. Shopify doesn't document which visit the order value comes from, so compare the two on your own store.

Does ShopifyQL support any-click attribution?

Yes, through WITH ANY_CLICK_ATTRIBUTION on a query that groups by a marketing dimension, not as a campaign_sales column [6][7]. Its totals can exceed the store total because every touchpoint gets full credit.

Keep a second campaign view in GA4

WeltPixel Conversion Tracking sends Shopify purchases to GA4 from the server with the campaign values of the visit it matched. Build the four-model table in Shopify, then check the campaign ranking against GA4 the same week.

Sources

  1. Shopify Help Center, marketing reports, help.shopify.com/en/manual/reports-and-analytics/shopify-reports/report-types/default-reports/marketing-reports, accessed October 6, 2026
  2. Shopify Help Center, creating a new data exploration, help.shopify.com/en/manual/reports-and-analytics/shopify-reports/report-types/custom-reports/create-custom-explorations, accessed October 6, 2026
  3. Shopify Help Center, Shopify Starter plan features, help.shopify.com/en/manual/intro-to-shopify/pricing-plans/plans-features/shopify-starter-plan, accessed October 6, 2026
  4. Shopify Help Center, Basic plan features, help.shopify.com/en/manual/intro-to-shopify/pricing-plans/plans-features/basic-shopify-plan, accessed October 6, 2026
  5. Shopify Developer Docs, "ShopifyQL" reference (API version 2026-10), shopify.dev/docs/api/shopifyql/latest, accessed October 6, 2026
  6. Shopify Developer Docs, "campaign_sales" schema, shopify.dev/docs/api/shopifyql/latest/schemas/marketing/campaign_sales, accessed October 6, 2026
  7. Shopify Developer Docs, ShopifyQL "WITH" (totals and attribution models), shopify.dev/docs/api/shopifyql/latest/syntax/with, accessed October 6, 2026
  8. Shopify Developer Docs, "sales" schema, shopify.dev/docs/api/shopifyql/latest/schemas/sales_revenue/sales, accessed October 6, 2026
  9. Shopify Help Center, order conversion summary, help.shopify.com/en/manual/orders/conversion-summary, accessed October 6, 2026

Ready to upgrade your tracking?

Server-side tracking for Magento and Shopify — accurate data, better attribution, full privacy compliance.