Tracking & Attribution

Why Does Attribution Break Between Your Landing Page and Shopify Checkout?

Attribution breaks at the Shopify checkout hop because the landing session and the order session are separate. Here's why, and how to re-stitch them.

Quick answer

Because the visit that carried the ad click and the visit that places the order are recorded as two different sessions. Your landing page runs your own tag with your own storage; Shopify's checkout runs in its own sandboxed context, and the identifier your tag wrote on the landing page is not reliably the identifier available at checkout. The click ID stays behind on the landing page, the order arrives with no source, and your report calls it Direct. Fixing it means re-joining the two sessions to one person after the fact, not hoping the browser carries the ID across.

Tell us what's broken. We'll fix your tracking — free.

Describe the tracking/attribution problem you're stuck on and we'll map it to a fix: server-side conversions to Meta, Google, TikTok and Pinterest, plus first-party tracking that survives Safari. No code required.

Attribution breaks between your landing page and Shopify checkout because the visit that carried the ad click and the visit that places the order are two different sessions. Your landing page runs your tag in your page’s context. Checkout runs in Shopify’s, with its own sandbox and its own storage rules. The identifier written on the landing page does not reliably survive the hop, so the order arrives with nothing tying it to the click.

The symptom is familiar: Meta says it drove 40 purchases, Shopify’s own report says most orders were Direct, and nobody can reconcile the two. The cause is not a broken pixel. It is a broken join.

What actually happens when a shopper moves from your landing page to checkout?

Three things change at once: the page, the execution context, and often the host. Your theme pages run your scripts directly. Checkout does not — it is served by Shopify and third-party code there runs inside a restricted sandbox rather than alongside your theme’s JavaScript.

So the shopper experiences one continuous visit. Your data records two: a landing session with fbclid or gclid in the URL, UTMs, referrer and a visitor ID, then a checkout session with an order, a total, an email — and no click ID at all.

Nothing errors. Both halves are recorded correctly. They just do not know about each other, which is the same structural failure behind ad conversions showing as Direct everywhere else — the identifier that would have joined the records was not present in the second one.

Why does the checkout hop create a new visitor?

Because the sandboxed pixel environment does not give third-party code the same durable storage your theme has, and in practice the visitor identifier it can read regenerates. PartialLeads sees this directly: the Customer Events sandbox regenerates visitor IDs frequently, which is why our matching never assumes the ID survives the hop.

Add the browser-side pressure on top. Safari’s tracking prevention caps script-written cookies. Ad blockers strip requests. A shopper who opened your ad in an in-app browser and finished in Safari has changed browsers mid-purchase. Any one of those turns “same person, one ID” into “two anonymous visitors.”

There is a second, quieter version of this: the checkout itself is fine, but the shopper’s network changed. A phone drops from wifi to LTE between the landing page and the payment step, the IP changes, and an IP-based join silently fails.

And one adjacent case worth naming so you do not misdiagnose: if your landing page is on a separate domain from your store, you have a cross-domain problem on top of this one, and it is its own problem with its own fix. Everything below assumes the landing page and the store share a domain and the break is the checkout hop itself.

What does the break actually cost you?

Four things, in rough order of how much money they represent.

Your channel report lies. Orders with no resolvable source pile into Direct. Direct then looks like your best “channel,” and the campaigns that actually produced those buyers look unprofitable next to it. Scale decisions get made off that table.

The ad platform stops learning. Conversion events with no identifiers, or no event at all, mean the algorithm gets a thinner signal than it needs. Optimisation degrades — not because your creative changed, but because your feedback loop did.

Upper-funnel events go unattributed too. Checkout-started and payment-info events fired in the sandbox carry the same missing identifier, so abandoned checkouts can’t be traced back to the source either.

Your first-touch view collapses entirely. Last-click can sometimes limp along on a referrer. A first-touch model needs the whole journey stitched, and a journey that is two disconnected fragments cannot produce one.

How do you tell whether this is happening to you?

Run three checks. None needs engineering help.

  1. Count orders against attributed orders. Open your store admin for the last 30 days and note the order count. Open your attribution report for the same window and timezone and note how many orders have a resolvable source. The gap is your break, quantified.
  2. Buy from your own ad. Click your live ad on a phone, complete a real order, then look at how that order is labelled. One test purchase tells you more than a week of theorising, because you know the ground truth.
  3. Compare session counts to order counts. If nearly every order has its own fresh visitor with a session length of seconds and no prior pageviews, those are checkout-only sessions — the landing half never got attached.

A useful tell: orders that show a source are disproportionately the ones from shoppers who did not leave and come back. Same-visit, same-device buyers survive the hop more often. Returning buyers, cross-device buyers and anyone who bought two days later are where the loss concentrates.

What can you do about it without a vendor?

Four generic fixes, each real, each with a limit worth knowing before you commit an afternoon to it.

Propagate parameters onto every link that leaves the landing page. If UTMs and click IDs are in the landing URL, carry them onto your cart and checkout links so the next request still has them.

// Carry attribution params onto every internal link before the shopper clicks.
const KEEP = ['utm_source','utm_medium','utm_campaign','utm_content','utm_term',
              'fbclid','gclid','gbraid','wbraid','msclkid','ttclid','epik'];
const here = new URLSearchParams(location.search);
const carry = new URLSearchParams();
KEEP.forEach(k => here.has(k) && carry.set(k, here.get(k)));

if ([...carry].length) {
  document.querySelectorAll('a[href]').forEach(a => {
    const url = new URL(a.href, location.href);
    if (url.host !== location.host) return;        // internal links only
    carry.forEach((v, k) => url.searchParams.set(k, v));
    a.href = url.toString();
  });
}

Limit: it only helps shoppers who go straight from the landing page to checkout in one unbroken run. Anyone who leaves and returns by typing your URL arrives with a clean address bar, and the parameters are gone.

Store the attribution yourself on first arrival. Write UTMs and click IDs to first-party storage the moment the landing page loads, so a later step can read them back rather than re-deriving them from a URL that no longer has them.

Limit: whatever you write from client-side script is subject to browser lifetime caps, and the checkout sandbox may not be able to read it anyway. This buys you durability inside your own pages, not across the hop.

Attach the source to the order server-side. If your stack can write a note or a custom attribute onto the order as it is created, you can carry your own session reference into the order record.

Limit: it requires the checkout to have received that reference in the first place, which is the problem you started with.

Match on the order’s email instead of on cookies. The order always has an email address. If your landing-page record also has one, you can join them without any browser identifier surviving anything.

Limit: this works beautifully for shoppers who typed an email before checkout, and not at all for shoppers who first give you an email at checkout — which is most of them, unless something is capturing email earlier in the funnel.

That last limit is the one that decides the outcome. Every durable fix for the checkout hop is a matching problem, and matching needs at least one identifier that exists on both sides of the break.

How does PartialLeads keep the session intact across the checkout hop?

By assuming the hop breaks the identifier and re-joining the two sessions afterwards, rather than betting that the ID survives. The landing session and the checkout session are stitched into one person by visitor identity resolution: a six-tier cluster over visitor ID, email, phone, IP plus user agent, device fingerprint and click ID, each tier carrying its own confidence.

Dark Lead Intelligence identity view of the Shopify checkout hop: a landing session card with a Meta Ads badge, an fbclid and a visitor ID, a dashed arrow labelled checkout hop to a checkout session card whose visitor ID reads regenerated with no click ID, and a panel below listing the join tiers — visitor ID echo missed, email matched at 95, phone matched, IP plus user agent weak

The Shopify path contributes to that from both ends. The Customer Events pixel carries the visitor ID forward where it can, and when the sandbox regenerates it, the cluster re-stitches the session by the identifiers that remain. Same-visit breaks are healed by visit-sibling inheritance: a session sharing client, IP and user agent within ±30 minutes inherits the strong attribution — UTMs and click IDs — from its sibling. A narrower tier covers the cellular handoff case, matching on user agent and the Meta fbc cookie within ±10 minutes while allowing the IP to differ, which is exactly the phone that switched from wifi to LTE at the payment step.

Then the order itself arrives on the orders/create webhook, server-side, and goes through one matching pipeline: normalise the email and phone, dedup so a redelivered webhook cannot double-count, then match to a session in tiers — a visitor ID echo first, then email, then phone, then IP. Once it matches, the lead flips to Completed with revenue attached and both a first-touch and a last-touch attribution row are written, snapshotting the UTMs, click IDs, referrer, landing page and time-to-purchase. The same pass fans the conversion back out server-side to Meta, Pinterest, TikTok and Google Ads with a deterministic event_id and a UNIQUE dedup table behind it.

Because the order arrives by webhook rather than by browser, every order in your store admin produces a conversion event — closed tabs, blockers and sandbox failures included. That is the honest version of the “never miss a conversion” claim: we control dispatch, the platform controls whether it can match the event to a user. The full ecommerce funnel goes the same way, not just Purchase — ViewContent, AddToCart, Search, InitiateCheckout and AddPaymentInfo — so the algorithm sees the steps before the sale, which is where ecommerce revenue attribution usually loses its signal.

Dark Leads list with three placeholder rows whose journey ribbons span the checkout hop — Meta or Google, landing, checkout, order — two marked Completed with revenue and per-lead API chips, one marked Partial at the cart step, above Attribution report tiles for attributed revenue, attributed purchases and time to purchase

Three honest constraints. An order whose email and phone match nothing and whose visitor ID regenerated still lands as unmatched — it is visible in the Purchases ledger and recoverable later when identity arrives, but it is not magic. Match quality on the platform side still depends on the platform’s own cookie being captured client-side, so keep your base pixel installed; PartialLeads reconstructs Meta’s _fbc from an fbclid in the URL but does not invent _fbp from nothing. And the checkout sandbox has its own reporting quirks that no vendor removes — the empty user agent Shopify’s pixel sends with some events is the best-known one.

What breaks The mechanism Where you see it in the dashboard
The checkout session has no click ID, so the order reads Direct Six-tier identity cluster re-joins the checkout session to the landing session Customer Journey timeline, Attribution report
The sandbox regenerates the visitor ID at the hop Purchase matched in tiers — visitor ID echo, then email, then phone, then IP Purchases ledger, matched vs unmatched wedge
The shopper’s IP changes mid-purchase on mobile Visit-sibling inheritance, including the user-agent plus fbc cellular-handoff tier Journey ribbon on the Leads list
The browser never fires the conversion Order arrives on the orders/create webhook and dispatches server-side CAPI activity log
A redelivered order webhook double-counts Deterministic event_id with UNIQUE dedup tables CAPI activity log
Only Purchase reaches the ad platform Ecommerce fan-out of ViewContent, AddToCart, Search, InitiateCheckout, AddPaymentInfo CAPI activity log, per-lead API column

Tell us what's broken. We'll fix your tracking — free.

Describe the tracking/attribution problem you're stuck on and we'll map it to a fix: server-side conversions to Meta, Google, TikTok and Pinterest, plus first-party tracking that survives Safari. No code required.

Sources

https://developers.facebook.com/docs/marketing-api/conversions-api

https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/customer-information-parameters

https://developers.facebook.com/docs/marketing-api/conversions-api/deduplicate-pixel-and-server-events


Frequently asked questions

QWhy does Shopify say my orders are Direct when my ads are clearly working?
Because the order session and the ad-click session are recorded separately. The click ID lived on your landing page; the checkout session that produced the order does not carry it. With nothing to join the two records, the order has no resolvable source and falls into Direct. The ads may be working exactly as the platform reports — the join is what failed.
QWill adding UTMs to my checkout links fix it?
Partly. Carrying parameters onto internal links helps shoppers who move straight from the landing page to checkout in one run. It does nothing for a shopper who leaves and comes back by typing your URL, or who buys two days later on another device. Parameter passthrough is worth doing, but treat it as one layer, not the fix.
QIs this the same as having my landing page on a different domain?
No, though they look identical in a report. A separate landing domain is a cross-domain identity problem that starts before checkout. The checkout hop breaks attribution even when everything is on one domain, because the execution context and storage change at checkout. If you have both, you have two breaks to fix, not one.
QDoes the Shopify Customer Events pixel solve this on its own?
It gives you events from checkout, which is necessary but not sufficient. The events still need an identifier that ties them to the session that carried the ad click, and in practice the visitor identifier available in that sandbox regenerates. Without a stitching layer behind it, you get accurate checkout events attached to an anonymous visitor.
QCan I just match orders to sessions on email?
It is the strongest single join you have, and it is what most server-side matching leans on. The catch is that it only works if you already had that email before checkout. If the first time you learn a shopper's email is on the order itself, there is nothing on the landing-page side to match it against — which is the argument for capturing email earlier in the funnel.
QHow do I know how much revenue I'm losing to this?
Compare order counts. Take the order count from your store admin for a 30-day window, then count orders with a resolvable source in your attribution report over the same window and timezone. The difference is revenue you cannot assign to a channel. Do it before changing anything, so you have a baseline to measure the fix against.
QIf I fix the stitching, will my numbers match Meta's exactly?
No, and be suspicious of anyone promising that. You can control what gets dispatched — every order in your admin can produce an event. Whether the platform resolves that event to a user and a click depends on its own graph, consent and opt-out rates. Expect the gap to shrink substantially and the direction of your channel report to change; do not expect two systems with different models to print the same number.

Find the qualified leads your forms are currently throwing away.

Install PartialLeads on one landing page, send traffic, and compare what your CRM captured against what PartialLeads recovered and qualified.