Referral Source Attribution

Read-only: parses each order's landing site and referrer URL to break down orders, revenue, and AOV by traffic source — direct, organic, paid, social, email, or referral domain.

shopify-admin-referral-source-attribution


Purpose

Aggregates orders by their first-touch traffic source — extracted from each order's landingPageUrl, referrerUrl, and any UTM parameters embedded in the landing URL. Produces an attribution table showing orders, revenue, and AOV per source so merchants can see which channels are actually converting. Read-only — no mutations. Use when native Shopify analytics dashboards aren't granular enough or when you need to export raw attribution data for an external model.


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)
    formatstringnohumanOutput format: human or json
    days_backintegerno30Lookback window in days
    min_ordersintegerno1Minimum orders per source to include in the human-readable summary
    group_bystringnocategoryGrouping level: category (direct/organic/paid/social/email/referral), domain (raw referrer host), or utm_source (UTM param value)
    include_utmboolnotrueWhen true, parse utm_source, utm_medium, utm_campaign from landingPageUrl query string

    Safety


    > ℹ️ Read-only skill — no mutations are executed. Safe to run at any time.


    Workflow Steps


  • OPERATION: orders — query
  • Inputs: query: "created_at:>=''", first: 250, select id, name, createdAt, landingPageUrl, referrerUrl, customerJourneySummary { firstVisit { landingPage referrerUrl source sourceType utmParameters { source medium campaign term content } } }, totalPriceSet, customer { numberOfOrders }, pagination cursor

    Expected output: Orders with their landing/referrer/UTM data; paginate until hasNextPage: false


  • For each order, derive a normalized source:
  • If customerJourneySummary.firstVisit.utmParameters.source is set → use it (strongest signal)
  • Else parse UTM params from landingPageUrl query string when include_utm: true
  • Else extract host from referrerUrl and map to a category:
  • empty/null → direct
  • google.com / bing.com / duckduckgo.com → organic-search
  • googleads/doubleclick → paid-search
  • facebook.com / instagram.com / tiktok.com / x.com / twitter.com / pinterest.com / youtube.com → social-
  • mail/gmail/outlook hosts → email
  • any other host → referral-

  • Aggregate by the chosen group_by dimension:
  • orders count
  • revenue = Σ totalPriceSet.shopMoney.amount
  • AOV = revenue / orders
  • new-customer % (orders where customer.numberOfOrders == 1 divided by total in source)

  • GraphQL Operations


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

    Session Tracking


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


    On start, emit:

    ╔══════════════════════════════════════════════╗
    ║  SKILL: Referral Source Attribution          ║
    ║  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):

    ══════════════════════════════════════════════
    ATTRIBUTION REPORT  (<days_back> days, group: <group_by>)
      Orders analyzed:    <n>
      Total revenue:      $<amount>
      Sources detected:   <n>
    
      Top sources by revenue
      ─────────────────────────────────────────
      <source>            Orders: <n>  Revenue: $<n>  AOV: $<n>  New cust: <pct>%
      <source>            Orders: <n>  Revenue: $<n>  AOV: $<n>  New cust: <pct>%
      ...
    
      Output: attribution_<date>.csv
    ══════════════════════════════════════════════
    

    For format: json, emit:

    {
      "skill": "referral-source-attribution",
      "store": "<domain>",
      "period_days": 30,
      "group_by": "category",
      "totals": {
        "orders": 0,
        "revenue": 0,
        "currency": "USD"
      },
      "sources": [
        {
          "source": "<name>",
          "orders": 0,
          "revenue": 0,
          "aov": 0,
          "new_customer_pct": 0
        }
      ],
      "output_file": "attribution_<date>.csv"
    }
    

    Output Format

    CSV file attribution_.csv with columns:

    order_id, order_name, created_at, source, source_category, referrer_url, landing_page_url, utm_source, utm_medium, utm_campaign, revenue, is_new_customer


    Error Handling

    ErrorCauseRecovery
    THROTTLEDAPI rate limit exceededWait 2 seconds, retry up to 3 times
    Null landingPageUrl and referrerUrlPOS, draft, or import orderCategorize as unattributed
    Malformed UTM paramsUnencoded characters in landing URLSkip UTM parse, fall back to referrer host
    customerJourneySummary not availableOlder order or app-created orderFall back to top-level landingPageUrl/referrerUrl

    Best Practices

  • Use group_by: utm_source when running structured campaigns with consistent UTM tagging — this is the highest-fidelity attribution signal.
  • Use group_by: category for board-level summaries; merchants want "how much came from social" before "how much came from instagram.com/p/abc".
  • Cross-reference with discount-roi-calculator — combining "which source drives the order" with "which discount the order used" reveals where paid acquisition actually pays off.
  • Beware of "direct" inflation — many email-app and social-app clicks lose their referrer and surface as direct. Use UTM tagging on outbound links to recover that signal.
  • Run on a multi-month horizon (days_back: 90) for low-volume stores so percentage breakdowns aren't dominated by a handful of orders.