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 (Normalized)

SourceMedium extracts a specific allowlist of keys from order-level customAttributes. Keys are normalized before matching (snake_case / camelCase / delimiter / case agnostic):
  • utm_source, utmSource, UTM_SOURCE, utm-source → treated as the same key
  • sm_utmParams, smUtmParams → treated as the same key
  • GE_utmParams, ge_utm_params → treated as the same key
To reduce collisions with other checkout apps, prefer the sm_utm_* / sm_utmParams keys for explicit overrides. Standard utm_* keys are also supported.
KeyDescriptionExample
sm_utm_source, sm_utm_medium, sm_utm_campaign, sm_utm_content, sm_utm_term, sm_utm_idSourceMedium override UTMs (recommended)sm_utm_source=facebook
utm_source, utm_medium, utm_campaign, utm_content, utm_term, utm_idStandard UTMsutm_campaign=summer_sale_2025
sm_utmParams, utmParams, GE_utmParamsAggregate UTM query string (parsed)utm_source=google&utm_medium=cpc
sm_referrer, referrerReferring URLhttps://blog.example.com/review

Click IDs (fallback inference)

Click IDs are processed but not stored as raw values. If no explicit utm_source is present, their presence infers a channel-level utm_source (fallback-only).
Click IDInferred utm_source
scclidsnapchat
irclickidimpact
msclkidmicrosoft
ttclidtiktok
fbclidmeta
gclidgoogle
If multiple click IDs exist, the system prioritizes: scclid > irclickid > msclkid > ttclid > fbclid > gclid.

Conflict resolution (deterministic)

If you provide conflicting values (e.g., both sm_utm_source and utm_source, or both direct keys and utmParams), SourceMedium resolves each final field with a deterministic waterfall:
  • UTM fields: direct sm_utm_* → direct utm_* → parsed from sm_utmParams → parsed from utmParams → parsed from GE_utmParams
  • utm_source only: if still missing, infer from click IDs (scclidirclickidmsclkidttclidfbclidgclid)
  • Referrer: sm_referrerreferrer
If the same normalized key appears multiple times at the same level (e.g., utm_source and UTM_SOURCE), SourceMedium de-dupes deterministically using MAX() (lexicographically largest value). To avoid surprises, only set each key once.

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": "sm_utm_source", "value": "facebook" },
      { "key": "sm_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": "sm_utm_source", "value": "facebook" },
      { "key": "sm_utm_medium", "value": "cpc" },
      { "key": "sm_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
  • Order-level customAttributes are treated as an explicit override (see Attribution Source Hierarchy)

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 write cart attributes before checkout (theme JS / Storefront API)

Step 1: Write Attribution to Cart Attributes (Before Checkout)

Checkout UI Extensions are sandboxed and can’t directly access the browser DOM (e.g., document.cookie, localStorage, or window.location). To make UTMs available at checkout, capture them on the storefront and write them into cart attributes before the buyer starts checkout. This example uses the Online Store cart/update endpoint to set SourceMedium override keys (sm_utm_*). These cart attributes flow into checkout attributes and appear on the order as customAttributes.
// theme.js (storefront)
function getParam(name) {
  return new URLSearchParams(window.location.search).get(name);
}

const attributes = {
  sm_utm_source: getParam('utm_source'),
  sm_utm_medium: getParam('utm_medium'),
  sm_utm_campaign: getParam('utm_campaign'),
  sm_utm_content: getParam('utm_content'),
  sm_utm_term: getParam('utm_term'),
  sm_utm_id: getParam('utm_id'),
  // Optional click IDs for fallback inference (not stored as raw values in SourceMedium)
  scclid: getParam('scclid'),
  irclickid: getParam('irclickid'),
  msclkid: getParam('msclkid'),
  ttclid: getParam('ttclid'),
  fbclid: getParam('fbclid'),
  gclid: getParam('gclid'),
  sm_referrer: document.referrer || null,
};

const cleanAttributes = Object.fromEntries(
  Object.entries(attributes).filter(([_, v]) => v != null && v !== '')
);

if (Object.keys(cleanAttributes).length > 0) {
  fetch('/cart/update.js', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ attributes: cleanAttributes }),
  });
}
Set these attributes before the buyer starts checkout. If you update cart attributes after checkout is already open, they might not be reflected unless the buyer refreshes checkout.
If you’re using a headless storefront, use the Storefront API to set cart attributes instead of cart/update.js.

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(() => {
    void ensureSmOverrideKeys();
  }, []);

  function getAttributeValue(key: string): string | null {
    return currentAttributes.find(attr => attr.key === key)?.value ?? null;
  }

  async function ensureSmOverrideKeys() {
    // Checkout UI extensions cannot access cookies or localStorage.
    // This helper maps any existing `utm_*` attributes to `sm_utm_*` override keys.
    const mapping: Record<string, string | null> = {
      sm_utm_source: getAttributeValue('sm_utm_source') ?? getAttributeValue('utm_source'),
      sm_utm_medium: getAttributeValue('sm_utm_medium') ?? getAttributeValue('utm_medium'),
      sm_utm_campaign: getAttributeValue('sm_utm_campaign') ?? getAttributeValue('utm_campaign'),
      sm_utm_content: getAttributeValue('sm_utm_content') ?? getAttributeValue('utm_content'),
      sm_utm_term: getAttributeValue('sm_utm_term') ?? getAttributeValue('utm_term'),
      sm_utm_id: getAttributeValue('sm_utm_id') ?? getAttributeValue('utm_id'),
    };

    for (const [key, value] of Object.entries(mapping)) {
      if (!value) continue;

      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;
}

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. Unsupported key: Only allowlisted keys are extracted (see “Supported Keys”)
  2. Sync timing: Wait 24-48 hours for data to flow through
  3. Connector version: Ensure your Shopify connector supports customAttributes (contact support to verify)
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 source hierarchy. If allowlisted attribution is present in order customAttributes, it is treated as an explicit override and takes top priority.
Yes. If you write allowlisted attribution to order customAttributes, it is treated as an explicit override and will replace the order’s last-touch attribution (unless you only set a subset of fields, in which case missing fields may be filled from lower-priority sources when the channel matches).
SourceMedium’s latest Shopify connector extracts customAttributes automatically. Contact support if you’re unsure whether your connector supports this feature.