Shop Pay, Apple Pay, PayPal: Why GA4 Misses Revenue

A dark hero graphic. Top left: the Clickport logo with the kicker Shopify Revenue Tracking. Right: the dimmed Clickport Goals panel showing an ecommerce funnel (Viewed product, Added to cart, Started checkout, Completed purchase with EUR 48,920 revenue, Newsletter signup) with Visitors, Clicks, CR, Revenue, and Avg columns. Bottom left, four labels with blue icons: Checkout happens on Shopify's domain, Your GA4 tag can't run there, 1 in 5 orders never shows up, Count orders on the server instead.

Your Shopify admin and GA4 never agree on revenue. Every store owner has seen the gap. Most assume it's GA4 being GA4.

It isn't. The orders go missing at the checkout domain, not in the report.

Shop Pay processes 41% of Shopify's payment volume. Its checkout runs on a domain your analytics can't reach. We crawled 27 top DTC Shopify brands in April 2026, and 78% of them send buyers to shop.app, pay.shopify.com, or checkout.shopify.com. Which means most of the best-known stores on the platform send buyers somewhere the merchant's own tag never loads.

Only one store in the sample had a server-side tracking pipe installed. The rest are in the gap. And the pipe every vendor sells you to close it doesn't close it.

Key Takeaways
  • Shop Pay handles 41% of Shopify's payment volume. Its checkout finishes on shop.app, where your GA4 tag can't run. Shopify staff confirmed the missing purchase events are expected behavior.
  • We audited 27 top Shopify stores in April 2026. 78% send buyers to a Shopify-owned checkout domain, and on those domains the store's own analytics can't run.
  • The usual fix costs $200 to $990 a month for a server-side tracking service. Only 1 of the 27 stores had one installed.
  • Server-side GTM can't close this gap. It forwards events the browser already sent. If the purchase event never fired, there's nothing to forward.
  • The reliable fix is Shopify's orders/paid webhook. It fires on the server for every completed order: Shop Pay, Apple Pay, PayPal, with or without ad blockers or consent.

What we found in 27 stores (and why 78% are in the gap)

The mechanics come later. First, the audit.

On April 14, 2026 we crawled the public HTML of 27 well-known DTC Shopify brands across apparel, beauty, home goods, and food and beverage. Each one is a name you would recognize in its category, with real traffic and an active paid-social presence.

We aren't publishing the store list. The point is a platform-level problem, not a pile-on against any one brand. What matters is the shape of the sample: these are the stores an agency or a DTC founder would point to as best-in-class.

Clickport Audit, n=27, 2026-04-14
What we found in the public HTML of 27 top DTC Shopify brands
78%
route checkout through shop.app, pay.shopify.com, or checkout.shopify.com
74%
have Shopify's Web Pixel sandbox active, restricting third-party tracking access
4%
visibly installed a third-party server-side tracking pipe (Elevar, Stape, Analyzify, Littledata)
Detection is conservative. Tracker IDs hidden inside Shopify's Web Pixel sandbox aren't always visible in SSR HTML. What the crawl sees is what a third-party auditor or ad-blocker heuristic would see. Full methodology below.

The 78% is the number to sit with. That's 21 of the 27 brands whose cart page already points at a Shopify-controlled checkout domain before the buyer does anything. We opened the cart HTML of the four most-trafficked brands by hand to be sure. All four embed shop.app/checkouts/internal/preloads right there in the markup.

So this isn't an edge case that fires only when someone taps Shop Pay. Shopify is pre-warming the shop.app checkout as the default path for these stores.

The 4% is the one that surprised me. Every ecommerce tracking article ranking on Google tells you to install a server-side pipe: Elevar, Stape, Littledata, Analyzify. We found one store out of 27 running any of them. Zero Elevar, zero Stape, zero Analyzify, one Littledata.

Which means 96% of the best-known stores on the platform are ignoring the entire $200 to $990 per month rescue industry. Not because they don't care about their data. Because the pipe doesn't fix the problem people think it fixes. I get to that in section six.

For a deeper view of where Shopify's native analytics fall short, see Shopify Analytics: The Store Owner's Guide to Better Data.

How we ran the 27-store audit

Here's the full method, so you can check the results above and rerun them yourself.

Sample. 27 publicly known DTC Shopify brands, pulled from commonly cited Shopify case studies and industry lists, then filtered down to the ones we could confirm were running on Shopify (by detecting cdn.shopify.com, Shopify.theme, or shopify-section markers in the homepage HTML). The mix spans apparel (9 stores), beauty and personal care (8), food and beverage (7), home goods (2), and one personal-care-adjacent store. No store with under 50k monthly visits made the cut. We leaned toward brands that already show up in industry reporting, so the sample reflects best-in-class DTC setup, not long-tail merchants. We're holding the store list back to keep the finding on the platform-level gap, not on arguments about any one brand's data point.

What we fetched. For each store, three public endpoints:

  • The homepage (/)
  • The cart page (/cart)
  • The Shopify-default /products.json?limit=1 to get one product handle, then the product page (/products/{handle}) where the JSON endpoint returned data

No login. No items added to cart. No checkout started. No personal data touched. Every request used a Safari 17 desktop user-agent string, a 15 to 25 second timeout, and a 1.5 second pause between stores.

What we looked for. The crawler grepped each store's combined HTML for regex signatures of the tracking stack, express checkout path, and third-party pipes:

  • GA4 measurement ID: G-[A-Z0-9]{6,12}, gtag/js?id=G-, "accountID":"G-", google-analytics.com/g/collect, "measurementId":"G-"
  • Google Tag Manager: GTM-[A-Z0-9]{4,10}, googletagmanager.com/(gtm|gtag).js
  • Meta Pixel: connect.facebook.net, fbevents, fbq(, facebook.com/tr?, "pixelId": followed by 10+ digits, facebook-domain-verification
  • TikTok Pixel: analytics.tiktok.com, ttq.load, ttq.page, tiktok.com/i18n/pixel
  • Klaviyo: static.klaviyo.com, klaviyo.com/onsite, _learnq, klaviyoOnsite
  • Shopify native analytics: shopify-analytics, monorail-edge.shopifysvc, trekkie.storefront
  • Web Pixel sandbox active: web-pixels-manager, webPixelsManager, web_pixels_manager
  • Third-party server-side pipes: getelevar.com, shopify.elevar.io, littledata.io, analyzify.com, stape.io, gtm.stape, .stape.net
  • Express checkout domains: shop.app, pay.shopify.com, checkout.shopify.com
  • Express checkout buttons: shopify-payment-button, shop-pay-button, shop_pay, apple-pay-button, ApplePaySession, paypal.com/sdk, paypal-button
  • Headless detection: /products.json?limit=1 returning non-JSON was treated as headless or JSON endpoint blocked

Limits, stated up front. Three of them matter, and I want them on the table before you read the numbers.

  1. Sandboxed pixel IDs don't always show up in SSR HTML. Shopify's Web Pixel sandbox renders the tracking config inside iframed JSON that gets hydrated client-side. A store running GA4 through the sandbox can show web-pixels-manager signatures with no visible G-XXXXXX measurement ID in the static HTML. So our GA4, Meta Pixel, and TikTok counts are floors, not ceilings. The real numbers can only be higher.
  2. Headless storefronts push tracking even further client-side. Four stores in the sample (15%) run Next.js on top of a Shopify backend. Their marketing HTML is hydrated by React, which often loads tracking after the fact. For these stores, the SSR signal is at its weakest.
  3. A third-party pipe can hide when the merchant routes sGTM through a custom subdomain. We searched for vendor-owned domains (stape.io, elevar.io, littledata.io, analyzify.com) and for common sGTM patterns. A merchant who has cleanly CNAMEd their sGTM container to a fully custom subdomain won't match any of these. So the 1-of-27 figure is a floor for third-party sGTM adoption. The true number could be higher. It won't be much higher, because most setups leave at least one vendor-identifiable string in the HTML.

What the crawl tells us reliably. Which domain the checkout points at (shop.app, pay.shopify.com, checkout.shopify.com). Whether the Web Pixel sandbox is active, which is a strong yes-or-no signal. Whether a third-party pipe is visibly branded in the markup. And which express checkout buttons are declared. Those are the four signals the headline findings rest on.

Full results, all signals, n=27
Signal detected in public HTMLCount%
References shop.app / pay.shopify.com / checkout.shopify.com2178%
Web Pixel sandbox active2074%
Shopify native analytics (monorail / trekkie)2074%
PayPal integration detected1867%
Klaviyo1763%
GTM container1556%
Meta Pixel visible in SSR HTML1452%
GA4 measurement ID visible in SSR HTML1141%
Apple Pay button detected622%
Shop Pay button detected622%
Headless storefront (products.json blocked)415%
TikTok Pixel14%
Third-party server-side pipe (Elevar / Stape / Analyzify / Littledata)14%
Only 1 of 27 stores shows a visibly branded third-party server-side pipe (Littledata signature in the HTML). Zero Elevar, Stape, or Analyzify signatures detected. True third-party sGTM adoption could be marginally higher if a merchant has fully CNAMEd their sGTM container, but our detection is deliberately broad.

Spot-check. We opened the cart page HTML of the four most-trafficked brands by hand, to make sure the 78% wasn't just the regex seeing things. All four embed the string shop.app/checkouts/internal/preloads directly in their cart markup. Shopify is pre-warming the off-domain checkout as the default path for these brands. This isn't a corner case that needs a specific buyer to tap Shop Pay first.

Reproducibility. The whole audit is 80 lines of bash and Python. Curl each store's three URLs, pipe the combined HTML through grep -E for each regex above, write a row to CSV, aggregate with value_counts(). Anyone with a developer laptop and two hours can run it against their own sample and compare.

What we'll rerun. October 2026, same 27 stores, same regexes, published as a before-and-after diff. Two numbers I expect to move. The share of stores in full headless mode should go up. So should the share with the Web Pixel sandbox hiding pixel IDs, as more merchants migrate off checkout.liquid.

Shop Pay: where the purchase event goes to die

Shop Pay was 38% of Shopify's payment volume across all of 2024, and it peaked at 41% in Q4, per PYMNTS coverage of Shopify's Q4 2024 earnings call. Shopify's own BFCM 2024 report says Shop Pay sales grew 58% year over year across the BFCM weekend.

In other words: this is the most-used express checkout in ecommerce. It's also the one that breaks your analytics the hardest.

A side-by-side illustration of a Shopify admin Orders page and a GA4 ecommerce purchases report for the same 14-day window. Shopify shows 247 orders worth $42,180.30. GA4 shows 188 purchases worth $33,860.20. Red annotations mark the gap: 247 vs 188 orders, the 24% express-checkout signal loss, Shop Pay at 41% of payment volume completing on shop.app, and $8,320 in revenue missing from analytics.
The shape of the gap. Same store, same 14 days: 247 paid orders in Shopify, 188 purchases in GA4. Illustration; the three annotations are unpacked in this section and the two after it.

Here's what happens when a buyer taps Shop Pay on a product page. Shopify redirects the session to shop.app or pay.shopify.com. The merchant's JavaScript doesn't run there. GA4's tag, the Meta Pixel, any GTM container, the Klaviyo tracker: all of them stop at the domain line.

When the buyer finishes, Shopify sends them back to the merchant's thank-you page. That return trip was the one thing meant to save client-side tracking. It doesn't.

A Shopify staff member, Alan_G, confirmed in the Shopify developer community that the checkout funnel events, checkout_started, checkout_shipping_info_submitted, payment_info_submitted, don't fire in the Shop Pay view. His exact words: "at the moment, it's expected behaviour that the events don't fire in the Shop Pay view." The fix is "on our radar" for "the medium-term." No date. The thread is from June 2025. As I write this, no fix has shipped.

The checkout_completed event is the one that maps to GA4's purchase event and Meta's Purchase. It fires only when the buyer lands back on your thank-you page and your pixel gets its chance to run. For Shop Pay, that has been hit or miss. Merchants in the same thread report the event firing with an order total of zero, or not firing at all, or firing for some orders and dropping others. There's no single number for how often it works. The failure isn't consistent enough to have one.

What the buyer sees vs. what your pixel sees
THE BUYER
Taps Shop Pay, confirms via SMS code, sees a confirmation screen, gets the order email. From their perspective, checkout worked perfectly.
YOUR PIXEL
Saw `add_to_cart` on the product page. Saw nothing else. The buyer left the merchant domain before any checkout events could fire, and the return trip to the thank-you page is either non-deterministic, stripped of session context, or happens after the pixel has already given up.
Source: Shopify dev community (June 2025), Shop Pay and Web Pixel Events thread.

There's a second failure mode, and it's worse because it makes no noise. In October 2025, a merchant named Jason reported that turning on Shop Pay Installments (Pay in 3) silently switched his checkout domain from brownieheaven.co.uk to shop.app. His Meta Pixel was verified on brownieheaven.co.uk, so the check now failed. Purchase events stopped reaching Meta. He burned £175 in ad spend over five days before he worked out why.

Shopify gave him no warning. In the end he turned Pay in 3 off to get his attribution back.

No merchant could have set this up to avoid it. The domain switch was Shopify's product decision. A Shop Pay feature update can break your Meta tracking tomorrow, and you'll hear about it from your ROAS dashboard, not from a Shopify email.

A screenshot of the Google Analytics 4 Traffic acquisition report with the property selector blurred behind a grey redaction bar, three editorial annotations marked up on top. The table shows eight session source / medium rows including google / organic (4,210 sessions), (direct) / (none) (3,024), pay.shopify.com / referral (612), shop.app / referral (488), facebook.com / referral (318), newsletter / email (280), paypal.com / referral (142), and instagram.com / referral (96). Three rows are highlighted in pale yellow: pay.shopify.com, shop.app, and paypal.com. A caption at the top reads 'The three rows your campaigns paid for, stolen by your own checkout domain. pay.shopify.com, shop.app, and paypal.com show up as referral channels and overwrite the original Meta or Google session attribution.' One annotation reads 'pay.shopify.com is the SMS-code authentication step for Shop Pay. It becomes the last referrer on the thank-you page and steals credit from whatever channel actually brought the buyer in.' Another reads 'shop.app is Shopify's checkout pre-warm domain. 78% of the 27 DTC brands we audited route through it. The merchant's GA4 tag does not run there.' A third reads 'paypal.com / referral. The GA4 cross-domain _gl linker is stripped by the HTTP redirect. Without it, GA4 attributes the order to PayPal instead of Meta, Google, or email.'
The three checkout-domain referral rows that quietly overwrite your real attribution. The fix is the GA4 referral exclusion list at Admin → Data Streams → Configure tag settings → List unwanted referrals. The longer-term fix is the architecture in the next section.

Apple Pay: the OS modal your tracker can't see into

Apple Pay on the web isn't an iframe. It isn't a pop-up. It's a browser-native modal that Safari and the OS draw, fully outside the merchant page. Apple's ApplePaySession documentation describes the flow in terms of "the system" showing the payment sheet, not the merchant page. Your page never owns the sheet.

So your JavaScript can't see what the buyer does inside it. The biometric check happens inside the Secure Enclave. The tap on "Pay" happens inside the sheet. Your page gets one callback, paymentauthorized, after the payment is already approved. Everything in between is dark.

Take a visitor who taps Apple Pay, passes Face ID, then closes the tab before the thank-you page loads. Your pixel sees nothing. No add_payment_info, no purchase, no checkout_completed. Shopify's official docs confirm that accelerated checkout buttons can sit right on the product page, skipping the cart entirely. The button renders inside a closed shadow DOM. Any tracking attached to it fails without a sound.

Then ITP stacks on top. Safari caps script-set first-party cookies at 7 days, and at 24 hours if the landing URL carried a click ID like fbclid or gclid. GA4's client ID and Meta's _fbp are both script-set cookies. So a buyer who comes back after 8 days looks brand new to your analytics.

Apple Pay doesn't cause this on its own, but the two feed each other. The people most likely to tap Apple Pay are mobile Safari users, which is exactly where ITP bites hardest. Per StatCounter (March 2026), Safari holds about 55% of US mobile browser share and 43.86% of UK mobile share.

One number to correct while we're here. The "53% Safari UK mobile" figure you see quoted everywhere is wrong. StatCounter's own dashboard shows 43.86%. Use the real one.

Warning
A cookie consent banner cannot gate the Apple Pay sheet. The sheet is a browser-native OS modal, not merchant-side JavaScript. Your CMP cannot block it, your visitor cannot be asked about it before it appears, and Apple Pay payment data flows regardless of the consent state your merchant site is in. This is the only part of the ecommerce stack that operates outside GDPR's client-side consent model by design.

PayPal Express: redirects, popups, and the 15% who never come back

PayPal's Smart Buttons run three different flows, depending on the device and browser. On desktop with popups allowed, checkout opens in a pop-up. On mobile, or when the popup is blocked, PayPal falls back to a full-page redirect to paypal.com. PayPal's own documentation calls the popup the main mode and the redirect "older." In practice, every mobile Shopify checkout takes the redirect.

Here's why the redirect hurts. When PayPal sends the buyer back to your thank-you page, the redirect happens at the HTTP level. GA4's cross-domain linker parameter (_gl) gets stripped on the way. Google's own documentation admits the parameter "may end up being removed from the URL." Seresa.io put it plainly: "JavaScript cannot intercept an HTTP server redirect. That's the fatal gap."

Without _gl, GA4 starts a fresh session and credits paypal.com as the referrer. Whatever Meta, Google, or email campaign really brought the buyer in gets overwritten.

Then there are the 10 to 15% who never come back at all. MarketLytics measured this across their own GA setups: "In about 85-90% of cases, user returns to the site successfully and we can trigger a transaction. But those 10-15% of times a user closes the page after completing the order, is where the trouble lies." The payment cleared. The order is in Shopify's admin. The purchase event never fired, because there was no thank-you page load to fire on. Put another way: roughly one PayPal order in ten, sometimes more, never gets counted.

Venmo through PayPal's PPCP is worse still. Braintree's own documentation says the mobile fallback "currently requires a full page redirect," and that after the app-switch "some mobile browsers can return customers to the same tab, while others relaunch your checkout page in a new tab." So your session cookie, your linker parameter, and your client ID all have to survive a server redirect or a native app hop. Often they don't.

PayPal Smart Button behavior by context
Desktop, popup OK
Popup window, session stays on merchant domain, `purchase` event fires on return with referrer largely intact
Mobile
Full-page redirect to paypal.com, `_gl` linker stripped, 10-15% never return to thank-you page, original attribution overwritten to paypal.com referral
Venmo (PPCP)
App-switch to Venmo iOS/Android app or Mobile Web Fallback full redirect, then browser return, tab handling varies by browser so session restoration is required
Safari popup blocked
Documented cases of the PayPal popup failing to open in Safari, leaving the buyer unable to complete checkout at all (BigCommerce Checkout SDK issue #944)
Return-failure rate of 10-15% is MarketLytics' own instrumented data. PPCP Venmo behavior is from Braintree's client-side JS documentation.

Before checkout even starts, a real share of your visitors have already said no to cookies. That's GDPR working as designed, and it isn't going away.

The USENIX Security 2024 paper by Bielova et al. ran controlled banner tests on French users. When accept and reject got equal prominence, 34% rejected. When the banner spelled out the consequences, that climbed to 47%. In plain English: give people a fair choice and a third to nearly half opt out.

etracker's 2025 consent benchmark for Germany measured 60% data loss under a legally compliant banner. Weight that for the EU share of a typical DTC audience and your GA4 and Meta Pixel coverage is already down 15 to 30% before a single express checkout enters the picture.

The SHEIN fine makes the legal stakes real. CNIL issued a €150 million penalty on September 1, 2025. The violation wasn't a missing reject button. The reject button was there. It just didn't work. Cookies still landed after "Refuse all." So "has a reject button" is now the floor. The bar regulators enforce is "does the reject button do anything."

On a Shopify store, rejecting the banner does more than hide analytics data. Shopify's Web Pixels API documentation says the pixel manager won't load a pixel that requires consent when that consent is missing. The pixel never loads. The cookieless modeling pings that Google's Consent Mode leans on can't fire from inside the sandbox either. That visitor isn't a 50% signal you can model your way back to. They are a zero.

The stacked funnel: 100 EU visitors, one Shopify store with compliant consent + express checkout
100 visitors
Real traffic
After consent rejection
~40 tracked
After ad blockers
~30
After express checkout
~21 in GA4
Consent rejection ~60% per etracker 2025 (Germany). Ad blockers 29.5% global per Backlinko (Q2 2025). Express checkout loss ~30% per Littledata (Feb 2025). Illustrative compounding, not a controlled study.

For more on why cookie-banner-free tracking matters architecturally, see What Cookie-Banner-Free Analytics Actually Means. For the full picture on GA4 gaps beyond checkout, see Why GA4 Is Not Showing Data, or use the GA4 Data Loss Estimator to score the general accuracy gap by ad blockers, consent, Safari ITP, and GA4's internal limits.

Why the $450/month server-side pipe barely moved the numbers

Server-side tracking is the answer every article ranking for this topic gives you. Here's how it works. sGTM (server-side Google Tag Manager) runs on infrastructure you control. It takes hits from the browser and forwards them to GA4, Meta, and the rest. In theory it gets around ad blockers, resets cookie expiry, and recovers lost events.

In practice it does some of that, and the price isn't what the blogs let on. Elevar charges $200/month for 1,000 orders, $450/month for 10,000, $950/month for 50,000, plus a $1,000 install. Littledata starts at $199/month and climbs to $990/month at 10,000 orders. Analyzify lists $109 to $206/month plus a one-time setup of $1,490 to $2,790. Stape is the infrastructure-only pick at $17 to $167/month, plus Google Cloud hosting that realistically adds $60 to $240/month. The real bill is higher than the headline number, every time.

Now the upside. Stape's own published case study recovered 20.71% of previously lost requests over a 10-day test, and 30.67% of purchase events specifically. That means about one in five lost requests came back, and nearly one in three lost purchase events. Genuinely good numbers for what sGTM does: recovering requests the browser made but that got blocked on the way out.

They tell you nothing about what sGTM can't do. And what it can't do is make an event that never happened. If Shop Pay doesn't fire checkout_completed, there's no hit to relay. sGTM is a better pipe, not a power source. It moves events that exist. It doesn't create the ones that don't.

Our audit found 1 store in 27 with a visible third-party pipe. I'm not saying these tools are useless. They really do recover 20 to 30% of otherwise-lost events. I'm saying 96% of this sample decided that recovery wasn't worth $200 to $990 a month. Looking at where the gap comes from, they have a point.

What server-side GTM does and does not fix
ProblemsGTM / CAPI
Ad blocker blocks browser requestFixed (routes via first-party domain)
Safari ITP caps cookie to 7 daysPartially fixed (server-set cookies can be longer-lived)
Shop Pay `checkout_completed` event never firesNot fixed (nothing to relay)
Apple Pay sheet, buyer closes tab pre-thank-youNot fixed (no client hit)
PayPal mobile redirect, `_gl` strippedNot fixed (attribution lost before server relay)
Consent banner rejection (EU)Not fixed (sandbox suppresses firing entirely)
Shop App upsell on `shop.app/thankyou`Not fixed (order never recorded on merchant domain)
Pixel + CAPI event deduplicationWorks if `event_id` matched on both sides; frequently broken
Pipes relay. They do not resurrect events that never existed.

Revenue Gap Calculator

Plug in your own numbers. The math is open and the source stats are linked right below the widget.

Revenue Gap Calculator
Monthly Shopify revenue$100,000
EU visitors (consent banner region)30%
Express checkout usage (Shop Pay + Apple Pay + PayPal)35%
Safari mobile traffic share25%
Visitors with ad blockers23%
Estimated monthly revenue invisible to GA4 / Meta Pixel
$42,380
/month your analytics can't see
Consent rejection loss
Express checkout event loss
Browser tracking loss (ITP + blockers)
Consent 45% loss on EU share: etracker 2025. Express checkout 30% loss: Littledata Feb 2025. Ad blocker 50% signal loss: Backlinko Q2 2025. Clickport captures this via server-side order webhooks. See how it works.

The two jobs you're accidentally asking one tool to do

Here's the reframe. Your Shopify admin is the source of truth for revenue. Every completed order is in it. The orders/paid webhook fires for every one, whether the buyer used Shop Pay, Apple Pay, PayPal, or the plain checkout. Shopify doesn't lose orders. Shopify loses analytics events.

Your analytics, done right, is the source of truth for one thing: which channel earned each order. Not whether the order happened. Not how much it was worth. Those are Shopify's job.

GA4 tries to do both jobs at once. It counts orders and attributes channels with the same client-side purchase event. So when that event fails at the payment sheet, both jobs fail in the same instant. The revenue count and the channel attribution die together, because the one event that carried both is gone.

The fix is to split the jobs. Count orders where orders get counted: in the Shopify admin. Capture the channel where the channel is knowable: at landing, before the payment sheet even exists. Then join the two by order ID the moment the webhook fires. The payment sheet drops out of the critical path entirely.

The industry already agrees on the diagnosis. Brad Redding of Elevar said it bluntly on the Honest Ecommerce podcast: "I don't think the vast majority of any site out there is going to say, 'Yes, my Google Analytics is 100% accurate.'" Edward Upton, Littledata's CEO, has been writing about this gap for years. Rand Fishkin put the bigger version on the SparkToro blog: "If you try to prove acquisition via traffic referral data you will become Google's fool."

The prescription the industry keeps writing is "ship more events, faster." That's the one thing the payment sheet refuses to go along with.

Decoupling attribution from the payment sheet: the architecture

Here's what it looks like in practice, on a Shopify store, with any analytics tool that has a server-side custom event API.

Step one: capture the session source at landing. When a visitor arrives, a first-party script reads the UTM parameters, the referrer, and any click IDs. It stores them with the session. The channel is locked in before checkout matters at all. (This only works if the inbound tags are clean; the UTM builder grades yours and shows the channel they'll classify into.)

Step two: at add-to-cart, the storefront writes that session context into Shopify's note_attributes on the cart. This is the move the whole thing turns on. note_attributes is a key-value list that Shopify keeps with the order. Whatever you put there rides through the Shop Pay redirect, the Apple Pay sheet, the PayPal popup, and the trip back. When the order shows up in the admin, your session context is sitting right there on it.

Step three: Shopify fires the orders/paid webhook. Not orders/create, which fires for unpaid orders too. orders/paid is the real "money received" event. Your handler checks the signature, pulls the session context out of note_attributes, and fires one server-side event to your analytics tool: revenue, currency, order ID, channel.

import crypto from 'crypto';

export async function handleShopifyWebhook(req) {
  const rawBody = await req.text();
  const hmacHeader = req.headers.get('x-shopify-hmac-sha256');

  const digest = crypto
    .createHmac('sha256', process.env.SHOPIFY_CLIENT_SECRET)
    .update(rawBody)
    .digest('base64');

  if (!crypto.timingSafeEqual(Buffer.from(digest), Buffer.from(hmacHeader))) {
    return new Response('Unauthorized', { status: 401 });
  }

  const order = JSON.parse(rawBody);
  const ctx = Object.fromEntries(
    (order.note_attributes || []).map(a => [a.name, a.value])
  );

  await fetch('https://clickport.io/api/event', {
    method: 'POST',
    headers: {
      'X-API-Key': process.env.CLICKPORT_API_KEY,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      type: 'custom',
      name: 'Purchase',
      url: order.landing_site || `https://${order.domain}/orders/${order.id}`,
      revenue_amount: parseFloat(order.total_price),
      revenue_currency: order.currency,
      meta_keys: ['order_id', 'utm_source', 'utm_medium', 'utm_campaign', 'payment_method'],
      meta_values: [
        String(order.id),
        ctx.utm_source || '',
        ctx.utm_medium || '',
        ctx.utm_campaign || '',
        order.payment_gateway_names?.[0] || ''
      ]
    })
  });

  return new Response('OK', { status: 200 });
}

That's 30 lines, HMAC check included. It runs as a Cloudflare Worker, a Vercel function, a Shopify App webhook handler, or a plain Express route. The /api/event endpoint is documented in Clickport's custom events documentation. The Shopify webhook registration and field reference live in Shopify's Admin API docs.

A screenshot of a Shopify admin Order detail page for order #SH1247 with three editorial annotations marked up on top. The order shows a 'Paid' green badge and a 'Fulfilled' green badge, placed on May 14, 2026 at 11:42 via Shop Pay for $156.40 USD. The right sidebar's Additional details card contains a Note attributes block listing six key-value pairs: utm_source = facebook, utm_medium = paid_social, utm_campaign = spring_apparel_2026, utm_content = wool_throw_carousel_v3, landing_page = /products/wool-throw-charcoal, cp_session_id = 8f3a-2b1d-44e9-7c2a. A caption at the top reads 'The fix the article describes. The session's UTM context written into note_attributes at add-to-cart survived the Shop Pay redirect, the express checkout sheet, and the return trip. When the orders/paid webhook fires, the channel attribution is right there in the payload.' One annotation reads 'Shop Pay completed off-domain. The merchant's GA4 tag did not fire on this order. Without note_attributes, this Purchase would arrive in Shopify with no channel context attached.' Another reads 'utm_source=facebook, utm_campaign=spring_apparel_2026. Written into the cart at add-to-cart, persisted through the Shop Pay redirect, returned in the orders/paid webhook payload.' A third reads 'When orders/paid fires, the webhook handler reads note_attributes, fires a server-side Purchase event with the original channel, and joins the revenue to the session by order ID instead of by cookie.'
What the fix looks like inside Shopify. The session's UTMs and the Clickport session ID land in note_attributes when the cart is built, ride through the Shop Pay redirect inside Shopify's own order object, and come out the other side as part of the orders/paid webhook payload. The channel attribution survived the payment sheet.

Here's what this buys you that server-side GTM can't. The server-side event doesn't wait on any browser event. If Shop Pay fires nothing, if Apple Pay suspended the tab, if PayPal never came back: no difference. The order landed in Shopify's admin. The webhook fired. The join happened on order ID, not on a cookie.

And here's what it doesn't buy you. It doesn't fix Meta Ads Manager's ROAS dashboard. Meta's optimization engine still wants Pixel and CAPI events, and that's a separate problem with a separate fix. What this architecture gives you is a true internal picture of which channel earned which revenue. That's the picture you make decisions on.

What this looks like in Clickport (and what it doesn't)

Clickport is a cookieless analytics product. The tracker is 5.6 KB and keeps nothing past the tab. No persistent cookies. No consent banner needed. The 16-channel classifier runs server-side and catches AI Search, social referrers, paid click IDs, and affiliate parameters that GA4 keeps dumping into "Direct." The custom events API takes the webhook payload above as-is.

Here's what the architecture gives you inside Clickport. Every completed Shopify order shows up as a Purchase event with the original session's channel attached. The $47,000 GA4 filed under "Direct" last month breaks out into 31% Meta paid social, 22% Google organic, 18% email, and 11% AI Search referrals from ChatGPT and Perplexity. The numbers hold up because the channel was captured before the payment sheet had a chance to break anything.

What it doesn't do, where I want to be straight with you:

  1. Clickport doesn't replace Shopify's admin as the revenue ledger. We read your orders. Shopify still owns them. If Shopify says $120,000 this month and Clickport says $119,800, Shopify wins. Every time.
  2. Shop App upsell orders completed on shop.app/thankyou can't be recovered by any analytics vendor, us included. The upsell happens entirely inside Shopify's domain, so its channel context is gone before anyone can grab it. That's a platform-level limit, not a tool problem.
  3. We don't do Meta CAPI forwarding or pixel deduplication out of the box. If you need your Meta Ads Manager ROAS to reflect the recovered events, that takes a separate CAPI setup. Shopify's native Meta channel is the easiest option there, Stape's sGTM the more robust one. Clickport gives you your internal attribution picture. It doesn't feed Meta's bidding signal.
  4. Our Shopify integration is a documented pattern, not an installed app. The code above is how real customers wire it up. There's no one-click app store install today. If you're a technical team or an agency, this is an afternoon of work. If you need a click-install, I'm not going to pretend we're there yet.

So here's the honest summary. Clickport solves the part of this problem the rest of the ecosystem is worst at: attributing revenue to the right channel after the payment sheet has broken the event chain. It doesn't solve the parts the ecosystem already does well, like Meta ROAS dashboards. Different tools for different jobs. I think that split holds for a lot of Shopify stores, not just ours.

Start a 30-day free trial. No credit card. The tracker installs in 60 seconds, and the webhook handler above is the second thing you ship.

Frequently asked questions

Why is my GA4 showing fewer purchases than Shopify?

Shopify's admin counts every server-confirmed order. GA4's purchase event fires only when the thank-you page loads in the buyer's browser and the script gets to run. Ad blockers, consent rejections, Safari ITP, Shop Pay domain hops, Apple Pay's OS sheet, and PayPal's mobile redirect all break that chain. A 10 to 20% gap is the industry baseline. Above 30% usually means express checkouts are a big share of how your customers pay.

Why is Shop Pay not tracking in GA4?

Shop Pay's express path finishes on shop.app or pay.shopify.com, domains where your GA4 tag doesn't run. Shopify's developer staff confirmed in June 2025 that checkout_started, checkout_shipping_info_submitted, and payment_info_submitted events "don't fire in the Shop Pay view" as "expected behaviour." The thank-you page can still get a checkout_completed event if the buyer comes back to your domain, but that's hit or miss. Add shopify.com and shop.app to your GA4 referral exclusion list to limit the attribution damage, and fire a server-side event from the orders/paid webhook if you want a revenue count you can trust.

Does Apple Pay break GA4 tracking?

Apple Pay's payment sheet is an OS-level modal that Safari and the operating system draw, outside the merchant page's document. Your JavaScript can't see the biometric check or the tap on "Pay." If the buyer closes the tab before the thank-you page loads, the purchase event never fires. ITP's 7-day cookie cap makes it worse for returning visitors. Apple Pay on mobile Safari is one of the highest-volume, least-trackable payment paths in ecommerce.

Why does GA4 show Direct for purchases that came from my Meta ads?

Two causes stack here. First, the checkout flow hops through payment.shopify.com or paypal.com, and those become the last referrer on the thank-you page, overwriting your original Meta attribution. Second, GA4's cross-domain _gl linker parameter gets stripped by HTTP-level server redirects, so session continuity snaps at the domain hop. Any session with no UTMs and no valid referrer falls into direct / (none). Two fixes: add shopify.com, shop.app, and paypal.com to your GA4 referral exclusions, and capture UTMs into Shopify's note_attributes before the buyer ever leaves your storefront.

What does pay.shopify.com mean in my GA4 referral sources?

When a buyer confirms Shop Pay through the SMS code modal on pay.shopify.com, that domain becomes the last touchpoint before your thank-you page loads. Brad Redding of Elevar documented this pattern years ago: pay.shopify.com turns up as its own referral channel and steals credit from the campaign that really earned the sale. Fix: GA4 Admin → Data Streams → Configure tag settings → List unwanted referrals → add shopify.com and shop.app.

Is it normal for GA4 to be 20% short?

Littledata's February 2025 benchmark says 20 of every 100 Shopify orders never show up in GA4, on average. That's with a standard client-side setup. Individual merchants have reported gaps as high as 84% (Shopify community thread, January 2025). A 10 to 20% gap you can live with. A 30%-plus gap means express checkouts are a real chunk of your payment volume, and you need a server-side purchase event fired from the Shopify webhook.

Keep reading

On WooCommerce instead of Shopify? The same purchase event goes missing, but the fix is closer because the checkout runs on your own server: WooCommerce Sales Not Tracking in GA4? We Audited 75 Stores.

For the wider Shopify picture, start with Shopify Analytics: The Store Owner's Guide to Better Data. For what GA4 hides past checkout, Why GA4 Is Not Showing Data is the companion piece. For the channel problem across all your traffic, Attribution Modeling: The Six Models with Actual Math shows why the "Direct" bucket is the worst lie in your dashboard. And for the cookie-banner argument under all of this, there's What Cookie-Banner-Free Analytics Actually Means.

We'll rerun the 27-store audit in October 2026 and publish the diff.

David Karpik

David Karpik

Founder of Clickport Analytics
Building privacy-focused analytics for website owners who respect their visitors.

Comments

Loading comments...

Leave a comment