Why client-side tracking silently under-counts
Client-side GTM/GA4 fires from the browser, which means it loses events to ad-blockers, privacy modes, tracking-prevention, and — critically for payments — 3DS redirects that navigate the user away from your page before the conversion tag runs. None of this shows up as an error; the numbers are just quietly low. When most of your buyers pay via a 3DS redirect, most of your conversions vanish.
Make the server the source of truth
The payment already happens server-side — the PSP tells your backend when a charge succeeds via webhook. That is the moment a conversion is real. So the backend sends the conversion to GA4 over the Measurement Protocol, using the same client id captured earlier, instead of relying on a browser tag that may never fire.
// On verified PSP webhook -> report conversion server-side
await fetch(`https://www.google-analytics.com/mp/collect?measurement_id=${ID}&api_secret=${SECRET}`, {
method: 'POST',
body: JSON.stringify({
client_id: order.gaClientId, // captured at checkout start
events: [{ name: 'purchase', params: {
transaction_id: order.id, // dedupe key
value: order.amountMajor, currency: order.currency,
}}],
}),
});Deduplication: don't double-count the client + server
If the client tag sometimes fires and the server always fires, you can double-count. The fix is a stable transaction_id on both paths so GA4 collapses duplicates into one purchase. The server event is authoritative; the client event, when it survives, just reconciles to the same id.
The 3DS retry flow
3-D Secure adds a redirect and often a retry. The tracking has to survive a user bouncing to the bank's page and back, and it must not count a retried-then-succeeded payment twice. Anchoring the conversion to the server-verified webhook (not the return-to-site page load) handles both: the event fires exactly when the charge is truly settled.
If a large share of your buyers use 3DS or mobile wallets, assume client-side conversion tracking is under-reporting materially. The webhook is the honest signal.
What changed
Moving conversion to a server-side Measurement Protocol path with dedup and 3DS-aware timing recovered visibility into the paying-customer majority that client-side tracking had been dropping — which also unblocked sane PSP-migration planning, because the funnel numbers could finally be trusted.
Key takeaways
- Client-side conversion tags under-count silently; ad-blockers and 3DS redirects eat events.
- Fire the conversion from the server on the verified payment webhook.
- Use a stable transaction_id to deduplicate client and server events.
- Anchor to settlement, not page load, so retries and redirects don't miscount.