Skip to main content
Audience: This guide is for developers and technical teams who need to add UTM attribution data to Shopify orders—either backfilling historical orders or capturing UTMs for future orders.

Overview

SourceMedium extracts UTM attribution data from Shopify order-level customAttributes. This guide covers two scenarios:
  1. Backfilling existing orders — You have attribution data (from spreadsheets, surveys, external tools) and want to write it to historical orders
  2. Capturing UTMs going forward — You want to automatically capture UTM parameters at checkout for future orders

Supported Keys

SourceMedium extracts the following keys from customAttributes. Keys are case-sensitive—use exact lowercase.
KeyDescriptionExample Value
utm_sourceTraffic sourcefacebook, google, klaviyo
utm_mediumMarketing mediumcpc, email, social
utm_campaignCampaign namesummer_sale_2025
utm_contentAd content identifiercarousel_v2
utm_termSearch term (paid search)running+shoes
utm_idCampaign ID120211234567890
gclidGoogle Click IDEAIaIQobChMI...
fbclidFacebook Click IDIwAR3x...
referrerReferring URLhttps://blog.example.com/review
Case Sensitivity: Keys must be exact lowercase. UTM_SOURCE, Utm_Source, and utm_Source will not be extracted.

Backfilling Historical Orders

If you have attribution data for existing orders (e.g., from post-purchase surveys, manual tracking, or external tools), you can write it to Shopify orders so SourceMedium can extract it.

How It Works

Available Methods

MethodBest ForTechnical Skill Required
Shopify FlowSmall batches, no-code users, Shopify PlusLow (no coding)
Admin API ScriptLarge bulk backfills, automationMedium (Python/Node.js)
Third-Party AppsLimited - most don’t support order attributesVaries
No CSV import available: Unlike products, Shopify does not support CSV import for order attributes. Matrixify and similar bulk import tools also do not support customAttributes on orders.

Option 1: Shopify Flow (No-Code)

If you have Shopify Plus, you can use Shopify Flow with the “Send Admin API request” action. This is ideal for smaller batches or when you want to trigger updates based on conditions.
1

Create a Flow workflow

Go to Settings → Flow and create a new workflow. Use a trigger like “Order created” for new orders, or tag orders you want to backfill and trigger on “Order tags added”.
2

Add 'Send Admin API request' action

Select GraphQL Admin API and choose the orderUpdate mutation.
3

Configure the mutation

Use this template to preserve existing attributes while adding new ones:Mutation:
mutation orderUpdate($input: OrderInput!) {
  orderUpdate(input: $input) {
    order { id }
    userErrors { field message }
  }
}
Variables (JSON with Liquid):
{
  "input": {
    "id": "{{ order.id }}",
    "customAttributes": [
      {%- for attr in order.customAttributes -%}
        { "key": "{{ attr.key }}", "value": "{{ attr.value }}" }{% unless forloop.last %},{% endunless %}
      {%- endfor -%},
      { "key": "utm_source", "value": "facebook" },
      { "key": "utm_medium", "value": "cpc" }
    ]
  }
}
The orderUpdate mutation replaces all customAttributes. The Liquid loop above preserves existing attributes—don’t skip it or you’ll lose data.

Option 2: Admin API Script (Bulk)

For large backfills (hundreds or thousands of orders), use a script with the Shopify Admin GraphQL API.

Prerequisites

Shopify Admin API access with write_orders scope
Order IDs mapped to your attribution data
Attribution data in the supported key format (see table above)

Implementation

mutation orderUpdate($input: OrderInput!) {
  orderUpdate(input: $input) {
    order {
      id
      customAttributes {
        key
        value
      }
    }
    userErrors {
      field
      message
    }
  }
}
Variables:
{
  "input": {
    "id": "gid://shopify/Order/1234567890",
    "customAttributes": [
      { "key": "utm_source", "value": "facebook" },
      { "key": "utm_medium", "value": "cpc" },
      { "key": "utm_campaign", "value": "summer_sale_2025" }
    ]
  }
}

Important Considerations

The orderUpdate mutation replaces all customAttributes on the order. If the order already has attributes you want to keep:
  1. First, fetch existing attributes via order query
  2. Merge your new attributes with existing ones
  3. Send the combined array in the mutation
# Fetch existing attributes first
existing = get_order_attributes(order_id)
merged = {**existing, **new_attribution}  # New values overwrite existing
backfill_order_attribution(order_id, merged)
Shopify’s Admin API has rate limits. For bulk backfills:
  • Use the Bulk Operations API for large datasets
  • Or throttle requests to ~2 per second for standard API calls
  • Consider batching updates overnight
Shopify allows updating orders regardless of age, but consider:
  • Very old orders may already have SourceMedium attribution from other sources
  • Check with SourceMedium support about attribution waterfall priority for your account

Verification

After backfilling:
  1. Verify in Shopify Admin: Orders → [Order] → Additional details should show your attributes
  2. Wait for sync: SourceMedium syncs typically run every 24 hours
  3. Check Orders Deep Dive: Verify the attribution appears in SourceMedium dashboards

Capturing UTMs for Future Orders

To automatically capture UTM parameters at checkout for new orders, use a Shopify Checkout UI Extension.
This approach supplements your existing tracking (GA4, Elevar). It’s particularly useful when cookie-based tracking fails due to ad blockers or cross-domain issues.

How It Works

Prerequisites

Shopify Plus or ability to create Checkout UI Extensions
Access to deploy changes to your Shopify theme/app
Method to persist UTM params across pages (cookies, localStorage, etc.)

Step 1: Persist UTMs on Landing

Before checkout, you need UTM parameters stored somewhere accessible:
// On page load, capture UTMs from URL and store in cookies
const urlParams = new URLSearchParams(window.location.search);
const utmKeys = ['utm_source', 'utm_medium', 'utm_campaign',
                 'utm_content', 'utm_term', 'utm_id', 'gclid', 'fbclid'];

utmKeys.forEach(key => {
  const value = urlParams.get(key);
  if (value) {
    // Set cookie with 30-day expiry, SameSite=Lax for cross-page persistence
    document.cookie = `${key}=${encodeURIComponent(value)}; ` +
                      `max-age=${30 * 24 * 60 * 60}; path=/; SameSite=Lax`;
  }
});

Step 2: Create Checkout UI Extension

// extensions/utm-capture/src/Checkout.tsx
import { useEffect } from 'react';
import {
  reactExtension,
  useApplyAttributeChange,
  useAttributes,
} from '@shopify/ui-extensions-react/checkout';

export default reactExtension('purchase.checkout.block.render', () => <UtmCapture />);

function UtmCapture() {
  const applyAttributeChange = useApplyAttributeChange();
  const currentAttributes = useAttributes();

  useEffect(() => {
    captureUtmAttributes();
  }, []);

  async function captureUtmAttributes() {
    const utmKeys = [
      'utm_source', 'utm_medium', 'utm_campaign',
      'utm_content', 'utm_term', 'utm_id',
      'gclid', 'fbclid', 'referrer',
    ];

    const utmData: Record<string, string> = {};

    for (const key of utmKeys) {
      const value = getCookie(key);
      if (value && value.trim() !== '') {
        utmData[key] = value;
      }
    }

    for (const [key, value] of Object.entries(utmData)) {
      const existing = currentAttributes.find(attr => attr.key === key);
      if (existing) continue;

      try {
        await applyAttributeChange({ type: 'updateAttribute', key, value });
      } catch (err) {
        // Silently fail - accelerated checkout will throw
        console.debug(`[UTM Capture] Error setting ${key}:`, err);
      }
    }
  }

  return null;
}

function getCookie(name: string): string | null {
  const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
  return match ? decodeURIComponent(match[2]) : null;
}

Step 3: Configure and Deploy

# extensions/utm-capture/shopify.extension.toml
api_version = "2024-10"

[[extensions]]
type = "ui_extension"
name = "UTM Attribution Capture"
handle = "utm-capture"

[[extensions.targeting]]
module = "./src/Checkout.tsx"
target = "purchase.checkout.block.render"
shopify app deploy

Limitations

Accelerated Checkout: The applyAttributeChange() method fails for Apple Pay, Google Pay, and Shop Pay express checkouts. This typically affects 10-30% of orders. Use this as a supplement to GA4/Elevar, not a replacement.

Troubleshooting

  1. Key name mismatch: Keys must be exact lowercase (utm_source, not UTM_Source)
  2. Sync timing: Wait 24-48 hours for data to flow through
  3. Connector version: Ensure your Shopify connector supports customAttributes (V3 connector required)
Ensure your API credentials have write_orders scope. For private apps, this must be enabled in the app settings.
The orderUpdate mutation replaces all attributes. Fetch existing attributes first, merge, then update.


FAQ

  • Backfill: You have historical attribution data you want to add to existing orders
  • Capture going forward: You want to automatically capture UTMs for new orders
  • Both: Most brands benefit from backfilling historical data AND capturing future UTMs
SourceMedium uses an attribution waterfall that prioritizes data closest to the transaction. Contact SourceMedium support for your specific configuration.
Yes, but the waterfall priority determines which source wins. Backfilled customAttributes data may or may not override existing attribution depending on your configuration.
SourceMedium’s Shopify V3 connector extracts customAttributes automatically. Contact support if you’re unsure which connector version you’re on.