Order Attribution Report

Read-only: parses UTM source/medium/campaign from order landing site URLs to attribute revenue, AOV, and conversion volume to marketing channels.

shopify-admin-order-attribution-report


Purpose

Pulls recent orders, extracts the UTM parameters embedded in each order's landingPageUrl query string, and rolls up revenue, order count, and average order value (AOV) by utm_source, utm_medium, and utm_campaign. Builds a marketing attribution report directly from first-party Shopify order data — no external analytics tool required. Read-only — no mutations.


Prerequisites

  • Authenticated Shopify CLI session: shopify store auth --store --scopes read_orders
  • API scopes: read_orders

  • Parameters


    ParameterTypeRequiredDefaultDescription
    storestringyesStore domain (e.g., mystore.myshopify.com)
    days_backintegerno30Lookback window for orders to attribute
    group_bystringnosourcePrimary grouping dimension: source, medium, campaign, or source_medium
    min_ordersintegerno1Minimum orders per group to include in the report
    include_organicboolnotrueWhen false, omit orders with no UTM parameters from the breakdown
    formatstringnohumanOutput format: human or json

    Safety


    > ℹ️ Read-only skill — no mutations are executed. Safe to run at any time. Attribution accuracy depends on whether the storefront propagates UTM parameters into the checkout — orders that bypass the storefront (POS, draft orders, subscriptions) will not have landing site URLs.


    Workflow Steps


  • OPERATION: orders — query
  • Inputs: query: "created_at:>='' financial_status:paid", first: 250, select landingPageUrl, referrerUrl, customerJourneySummary, totalPriceSet, pagination cursor

    Expected output: All paid orders in the window with landing page URLs; paginate until hasNextPage: false


  • For each order, parse landingPageUrl query string and extract utm_source, utm_medium, utm_campaign, utm_term, utm_content. Orders without UTM params are bucketed as (direct/organic) if include_organic: true.

  • Aggregate by the group_by dimension: sum order count, sum revenue (in shop currency), compute AOV = revenue / orders.

  • Sort groups by revenue descending; filter out groups below min_orders.

  • GraphQL Operations


    # orders:query — validated against api_version 2025-01
    query OrdersWithAttribution($query: String!, $after: String) {
      orders(first: 250, after: $after, query: $query) {
        edges {
          node {
            id
            name
            createdAt
            landingPageUrl
            referrerUrl
            displayFinancialStatus
            totalPriceSet {
              shopMoney {
                amount
                currencyCode
              }
            }
            customerJourneySummary {
              firstVisit {
                landingPage
                source
                sourceType
                referrerUrl
                utmParameters {
                  source
                  medium
                  campaign
                  term
                  content
                }
              }
              lastVisit {
                landingPage
                source
                sourceType
                utmParameters {
                  source
                  medium
                  campaign
                }
              }
              momentsCount
            }
          }
        }
        pageInfo {
          hasNextPage
          endCursor
        }
      }
    }
    

    Session Tracking


    Claude MUST emit the following output at each stage. This is mandatory.


    On start, emit:

    ╔══════════════════════════════════════════════╗
    ║  SKILL: Order Attribution Report             ║
    ║  Store: <store domain>                       ║
    ║  Started: <YYYY-MM-DD HH:MM UTC>             ║
    ╚══════════════════════════════════════════════╝
    

    After each step, emit:

    [N/TOTAL] <QUERY|MUTATION>  <OperationName>
              → Params: <brief summary of key inputs>
              → Result: <count or outcome>
    

    On completion, emit:


    For format: human (default):

    ══════════════════════════════════════════════
    ORDER ATTRIBUTION REPORT  (<days_back> days)
      Orders attributed:   <n>
      Total revenue:       $<amount>
      Untagged (direct):   <n>  (<pct>%)
    
      Top sources by revenue:
        <source>     Orders: <n>   Revenue: $<n>   AOV: $<n>
        <source>     Orders: <n>   Revenue: $<n>   AOV: $<n>
      Output: attribution_report_<date>.csv
    ══════════════════════════════════════════════
    

    For format: json, emit:

    {
      "skill": "order-attribution-report",
      "store": "<domain>",
      "period_days": 30,
      "group_by": "source",
      "orders_attributed": 0,
      "total_revenue": 0,
      "currency": "USD",
      "groups": [
        { "key": "google", "orders": 0, "revenue": 0, "aov": 0 }
      ],
      "output_file": "attribution_report_<date>.csv"
    }
    

    Output Format

    CSV file attribution_report_.csv with columns:

    group_key, utm_source, utm_medium, utm_campaign, orders, revenue, aov, currency, pct_of_revenue


    Error Handling

    ErrorCauseRecovery
    THROTTLEDAPI rate limit exceededWait 2 seconds, retry up to 3 times
    landingPageUrl is nullOrder placed via POS, draft, or subscriptionBucket as (direct/organic), count separately
    Malformed query stringManual or partial UTM taggingSkip parse failure, treat as direct, log count
    customerJourneySummary access deniedStore on plan that does not expose this fieldFall back to landingPageUrl parsing only

    Best Practices

  • Use group_by: source_medium to distinguish paid traffic (google/cpc) from organic (google/organic).
  • A high (direct/organic) percentage usually means UTM tagging is missing on paid campaigns — fix the campaign URLs, not the report.
  • Run weekly during active campaigns to track attribution drift; run monthly for steady-state reporting.
  • Cross-reference revenue here with ad spend from your ad platforms to compute true ROAS — this skill provides the order-side numerator only.
  • For multi-touch attribution, also surface customerJourneySummary.firstVisit vs lastVisit to compare first-click vs last-click models.