Draft — awaiting review
Why purchases disappear when customers pay with PayPal
The thank-you page is the least reliable part of a checkout, and most plugins hang the Purchase event on it. Fire from the payment hook instead.
TODO(owner): review before publishing —
this article is written from the plugin project files but has not been signed
off. Until it is, it carries noindex and stays out of the sitemap
and the RSS feed. Flip status to published in its
frontmatter to release it.
Because the Purchase event was attached to the thank-you page, and with an offsite gateway that page frequently never loads. The customer pays on PayPal’s site, closes the tab, loses the redirect, or finishes on a phone that goes to sleep — and the order-received page your tracking depends on is never rendered. The sale happened. The conversion was never recorded.
The fix is not a better thank-you page. It is to stop depending on one.
The thank-you page is the weakest link in the chain
A conversion tracked on the order-received page depends on a sequence of things you do not control:
- the customer completing payment on a third-party domain,
- that third party redirecting them back to you correctly,
- the customer not closing the tab in the seconds that takes,
- and the browser executing your tracking script before navigating anywhere else.
Every one of those is a place the chain breaks, and every break is a sale you made and did not measure. Card checkouts that stay on your domain mostly survive it. Offsite gateways — PayPal in particular, where completion often arrives asynchronously by webhook long after the browser has gone — do not.
Meanwhile your store’s database already knows, for certain, that the order exists. The information was never missing. It was simply never sent.
Server-first: fire from the payment hook instead
The alternative is to trigger Purchase from the event that actually means “this sale happened”: WooCommerce recording the payment.
Purchase fires on the first of these to occur:
- the WooCommerce payment-complete hook, or
- the order moving to
processingorcompleted.
At that moment the event ID is generated and stored on the order, together with a guard flag. No browser is involved and none is needed. If the customer never returns to your site, the conversion is already recorded.
Then what about the browser copy?
It still fires, when there is a browser to fire it in. If the customer does reach the thank-you page, the browser copy fires there — reusing the same event ID that was stored on the order, with its own re-fire guard so a reload cannot send it twice.
That shared ID is what makes the pair deduplicate into one conversion instead of double counting. Server-first tracking without a shared ID is just a second, competing set of numbers. It is the same mechanism described in Processed vs Deduplicated, applied to the one event where the money is.
The guard flag matters more than it sounds. An order can move through several status transitions — pending to processing to completed, plus whatever your fulfilment workflow adds — and a naive implementation fires on each one. The flag on the order means the event can be sent exactly once, no matter how many times the status changes afterwards.
The consent problem nobody mentions
Here is the part that catches people out the moment they switch consent gating on.
A payment webhook is a server-to-server request from the gateway to your store. It carries no visitor cookies, no session, no consent state — there is no browser in the transaction at all. So a system that reads the consent decision at send time finds nothing, and a correctly built fail-closed system then drops every webhook-triggered purchase. You turn on consent compliance and your PayPal sales vanish a second time, for a completely different reason.
The answer is to capture the decision while the visitor is still there: consent is stamped onto the order at checkout and read back from the order when the event is finally sent. The customer’s actual choice is honoured minutes later, with no browser present.
The same “no cookies at send time” constraint shows up elsewhere. Google Analytics identifies a user by a cookie value, so a purchase fired from a payment hook has no client ID to read — which is why one has to be derived deterministically from the order instead of invented per send. Get that wrong and every retry of the same order books a brand-new user.
Tracking must never be in the checkout’s way
One more constraint, because it is the reason this is harder than it looks: none of this may slow down a checkout.
Sends go through an asynchronous queue with retries at one, five and thirty minutes. A platform
API having a bad minute does not hold up a customer’s payment, and it does not silently drop
the event either — it retries, and the row stays visible in the log as queued or retrying
until it is sent. The access token is read at send time rather than at queue time, so
rotating a token never strands the events already waiting.
What to check if purchases are still missing
- Is the order actually reaching a paid status? No payment-complete hook, no event. Check the order status before anything else.
- Are events queued but not sent? Look for rows sitting at
queuedin the plugin log. If they are stuck, the problem is your site’s task scheduling, not the tracking. - Is consent gating dropping them? If the consent source expects a banner that does not exist, everything fails closed — which is correct behaviour and looks identical to being broken.
- Compare orders against events. Your store knows a sale happened; the platform may never have been told. That comparison is exactly what Order-to-Event Reconciliation is being built to automate, and it is the only way to turn “Meta says 40, my store says 52” into a list of twelve order numbers.
The full checklist, with the specific things to look at in each case, is in Purchases missing after PayPal.
Start with what you can see
Before any of this, it is worth knowing what is on your pages at all. The free tracking checker reads your public pages the way a visitor does and reports the pixels it finds and whether any of them is installed twice — read-only, no signup, about ten seconds.
It cannot tell you whether your server-side Purchase arrived. Nothing outside your own server can. But it will rule out the simpler explanations before you go looking for the complicated one.
TODO(owner) —
the author box needs your real details — name, one-line role, two or three sentences
of bio in the first person, and optionally a real photo. They go in
src/data/author.ts. Until then this post is attributed to PixelCapi in its
structured data rather than to an invented person, and no byline is shown.
Where to go next
Related reading
-
draft
How page caching silently breaks your pixel
If a plugin writes the event ID into your HTML, the cache freezes one ID for everybody — and the platform folds thousands of conversions into one.
-
draft
Processed vs Deduplicated in Meta Events Manager
The label that reads like a warning is the one you want. What Meta's two source labels actually mean, and the session trap that fakes a broken pixel.