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.
- 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.
- 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.
- 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.

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.

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