You stop attributing on the session and start attributing on the identity. Click IDs and UTM tags only ever live in the landing URL, and the hop to a cart, a subdomain checkout or a hosted payment page drops them. What survives is the email and phone on the order.
So the job is to store the source against a durable visitor ID at the first pageview, and join the order back to it afterwards.
Why do WooCommerce orders show as Direct when the checkout is on another page?
Because the thing carrying your attribution is the URL, and the URL changed. A buyer arrives on /offer?utm_source=meta&fbclid=…, clicks through to the cart, and the campaign tags are gone — nobody carried them. The order record that WooCommerce writes minutes later has no memory of them either.
Analytics then does the only thing it can. The checkout page’s referrer is your own site, which is not a traffic source, so the visit gets filed as Direct. This is the same failure mode behind almost every case of ad conversions showing up as Direct, just concentrated at one hop.
Two consequences follow, and both cost money. Paid channels look worse than they are, so you cut budget on campaigns that are working — attributing revenue back to the ad that started it is the whole job, and this hop is where it fails. And the ad platform, told nothing about the sale, keeps optimising against whatever partial signal it has.
What exactly breaks between the landing page and the checkout?
Four separate things, at four different stages. They fail independently, which is why fixing one rarely fixes the report.
- The URL loses its parameters. Internal links do not carry
utm_sourceorfbclidunless someone deliberately appends them. One click into the cart and the campaign context is gone from the address bar. - The referrer becomes your own host. A page-two referrer of
yourstore.comtells a naive classifier nothing, so it falls back to Direct rather than preserving the original source. - The origin changes. A checkout on
checkout.yourstore.comor on a separate domain does not automatically read cookies orlocalStoragewritten on the landing origin. Browser storage is scoped, and scope is the point of it. - The order is written by the server. The record WooCommerce creates contains a customer, line items and a total. It does not contain a click ID, because a click ID was never part of the order.

Read that left to right and the shape of the problem is obvious. Everything the ad platform needs exists at stage one. Everything that identifies the human exists at stage four. Nothing carries all of it end to end unless you build the carry yourself.
Does a payment-gateway redirect lose your attribution too?
Yes, and worse than an internal hop. When the buyer leaves for PayPal, a hosted card page, a bank’s 3-D Secure challenge or a wallet, they are on an origin you do not control. Your tag cannot run there, your cookies are not readable there, and the return URL brings back a browser that may look new.
Three things commonly change across that round trip. The referrer is now the payment provider, which some tools cheerfully classify as a referral channel — an ad-driven sale credited to a gateway. The session may be treated as a fresh visit if it exceeds a timeout. And on mobile, the network can change while the buyer is away, so the IP you would have joined on no longer matches.
There is a structural fix for the sourceless return session: let it inherit from its sibling. If a session with no source shares a device and a short time window with a session that has strong attribution, the two are the same visit. That is the idea behind visit-sibling inheritance, and it is the only thing that reliably heals the return leg of a gateway redirect.
Why doesn’t a checkout on a subdomain share the landing page’s data?
Because browsers scope storage by origin, and a subdomain is a different origin. localStorage set on www.yourstore.com is not visible to checkout.yourstore.com. Cookies are the partial exception: a cookie written at the apex domain is readable on subdomains, but only if it was deliberately scoped that way when set.
That makes the cookie the more portable half of the pair, which matters because cookies are also the half under pressure. Safari’s Intelligent Tracking Prevention caps the lifetime of cookies written by JavaScript, so a script-set attribution cookie can expire well before a considered purchase completes. A cookie set by your server in the HTTP response is not subject to that same script-written cap.
If the checkout is on a genuinely separate domain rather than a subdomain, nothing is shared at all and you are in the same territory as attributing orders across two domains: the join has to happen server-side, on identity, because no browser mechanism spans it.
How do you carry attribution across the hop yourself?
Three approaches, in rough order of how well they hold up.
Persist at first touch, read at order time. On the first pageview, write the UTMs, click IDs, landing page and referrer into first-party storage keyed to a visitor ID you generate once. When the order form posts, read them back and attach them to the order as metadata. This is the standard build and it is sound — its limits are that storage is per-origin, clearable, and vulnerable to the ITP cap above if you write the cookie from script.
Decorate the links to the checkout. Append the visitor ID or the click IDs to internal links crossing to another origin. It works, and it is fragile: it breaks on any redirect you do not control, it does not survive the gateway return, and it puts identifiers into URLs that get pasted, shared and logged.
Rely on the platform’s own cookies plus a server-side event. Real, and necessary, but incomplete on its own. A server event sent without the platform’s browser cookie is an anonymous server event, and its match quality reflects that. Server-side delivery is how the event gets there reliably; it is not a substitute for what the browser collected. That distinction is the whole subject of sending WooCommerce purchases server-side.
All three are worth doing. None of them recovers a buyer whose storage was cleared between the ad click and the purchase, which is why the fourth approach is the one that changes the outcome.
Why is matching on identity more reliable than carrying the session?
Because identity is the one thing the order is guaranteed to have. A WooCommerce order cannot exist without a customer — an email at minimum, usually a phone and a billing address too. The browser state you were trying to carry is optional and destructible. The customer record is neither.
So invert the design. Instead of asking the browser to transport the ad source all the way to checkout, let the order arrive server-side and then ask a different question: which visitor does this person match? Normalise the email, normalise the phone to E.164, and look for a session that saw an ad and left the same identifiers behind.
That join is tiered, because the identifiers differ in strength. A visitor ID echoed back from your own storefront is near-certain. A normalised email is strong. A phone is strong. An IP plus user agent inside a narrow window is a reasonable last resort. Working down that ladder is what visitor identity resolution does, and it is what makes the checkout hop survivable: the hop breaks the session, but it does not break the person.
How does PartialLeads attribute WooCommerce orders when the checkout is elsewhere?
By capturing the source at the first pageview against a durable visitor ID, then matching the order to it server-side after WooCommerce writes it.
The capture side runs on the storefront. One tag records UTMs on the landing URL, falls back to the referrer when there are none, and stores the click IDs each platform uses — fbclid, gclid, gbraid, wbraid, msclkid, ttclid, epik — on the session. The visitor ID lives in localStorage with a first-party cookie backup, and that cookie is set by the server in the response rather than by script, so Safari’s cap on script-written cookies does not apply to it.
The order side runs one pipeline for every inbound purchase, whatever sent it — WooCommerce, Shopify, Stripe, GoHighLevel or a universal webhook:
- Normalise, then deduplicate. Email and phone are normalised before anything else; the record is idempotent on source and order ID, so a redelivered webhook cannot create a second order.
- Match by tier. A visitor ID echoed from the storefront first, then email, then phone, then IP. The order flips the lead to Completed with revenue attached.
- Write both models. Every matched purchase produces a first-touch and a last-touch attribution row, snapshotting the UTMs, click IDs, referrer, landing page and time to purchase.
- Heal the sourceless session. A session with no attribution inherits from a sibling on the same device within a short window — including the mobile case where the network changed and only the user agent and the Meta click cookie still line up.
- Fan the conversion out. The matched purchase is dispatched server-side to Meta, Pinterest, TikTok and Google Ads, with a deterministic event ID so a retry cannot double-count.

You check it in three places. The Purchases ledger lists every order that arrived and whether it matched a session — the reconciliation that matters is that your WooCommerce admin order count equals the ledger’s. The Attribution report shows first touch, last touch and the resolved model side by side, with a recovered-attribution breakdown that itemises which orders moved off Direct and why. The Journey ribbon on the Leads list shows one buyer’s touches in order, so you can read a single order’s path without opening anything.
Two honest constraints. An order that matches nothing still lands, as an unmatched row, and stays recoverable if identity arrives later — it is not silently dropped, but it is also not attributed. And keep your client-side base pixels installed: a _fbc value can be rebuilt from an fbclid in the landing URL, but a _fbp cookie that was never set cannot be invented.
| What breaks | The mechanism | Where you see it in the dashboard |
|---|---|---|
| Click IDs and UTMs dropped at the first internal hop | Attribution captured on the first pageview and stored on the session, with referrer fallback | Journey ribbon on the Leads list |
| The checkout session looks like a new, sourceless visitor | Durable visitor ID in localStorage plus a server-set first-party cookie |
Journey ribbon — one person, N sessions |
| The payment page is on a domain you do not control | The order arrives server-side and is matched on identity, not on the returning browser | Purchases ledger — matched or unmatched |
| The returning session has no source at all | Visit-sibling inheritance from a strong session in the same visit | Attribution report — recovered attribution |
| The mobile network changed during the gateway round trip | Cellular-handoff tier: same user agent and Meta click cookie in a short window, IP allowed to differ | Journey ribbon |
| A redelivered order webhook counted twice | Idempotent dedup on source and order ID, deterministic event ID on dispatch | Purchases ledger, CAPI activity log |
| Refunds left inside attributed revenue | Refund rows in the same pipeline, netted against paid | Purchases ledger, net revenue |
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/parameters/fbp-and-fbc