GA4 Purchase Tracking for Nepali Checkouts: Fonepay, ConnectIPS and eSewa Redirects Done Right
Nepali checkouts break the textbook GA4 setup. The buyer picks eSewa, Khalti, Fonepay or ConnectIPS, leaves your domain, pays elsewhere, and may never load your thank-you page. A purchase event living only on that page undercounts revenue — and every ROAS number downstream inherits the error. For the strategy context, see server-side tracking in Nepal (the overview; this post is the implementation) and the COD attribution playbook (the business reconciliation this feeds).
1. Why the redirect breaks measurement
Three failure modes, all normal traffic:
- No return: buyer pays, closes the tab on the provider page. No thank-you view, no client-side event. Ever.
- Session break: buyer returns but the client ID did not survive the round trip — purchase lands in a new session, unattributed to the campaign that earned it.
- Double count: thank-you page fires on load and a retry or refresh fires it again, or both client and server paths fire without a shared key.
Design for all three from the start rather than debugging them one by one in production.
2. The correct pattern: server-verified callback as source of truth
Order of operations for every wallet/bank checkout:
-
Create the order first, in your system, with your own order ID, amount, currency and status
pending. Everything downstream keys off this ID — it is the one identifier no provider can take from you. - Send the buyer off with the order ID attached per the provider’s integration (reference field, custom parameter — whatever the docs specify for that rail).
- On callback, verify server-to-server before believing anything: confirm the payment status through the provider’s verification mechanism (status query, signature check — per that provider’s current docs), not merely that the buyer landed on your return URL. Front-channel redirects can be faked or replayed; verification cannot.
-
Fire the GA4 purchase event from your server (Measurement Protocol or your server GTM container) only after verification passes, with
transaction_idset to your order ID and the final settled amount. - Thank-you page as fallback only: render the receipt from your verified order record. If the server path already fired for that order ID, the page must not fire again.
3. Kill the self-referral: unwanted referrals plus cross-domain config
If your reports show esewa.com.np / referral (or khalti/fonepay equivalents) as a top “source” of purchases, the provider domain is stealing credit from the campaign that earned the click. Two admin settings fix different halves:
- Cross-domain measurement (Admin → Data Streams → Configure tag settings → Configure your domains): list your payment return domains so the client ID survives the round trip. Without this, every returning buyer starts a fresh session and the original Google Ads click loses the conversion.
- Unwanted referrals (same settings area): list the exact provider domains observed in your own checkout flow. Traffic from listed domains no longer starts new sessions, so credit stays with the original source. Copy the domains from your live flow, not from any article — providers add regional and failover hosts without notice.
One correction to a common recipe: the Data Layer alone cannot fix this. dataLayer lives in the page — it does not survive the trip to the provider’s domain and back. Use dataLayer for what it is good at (on-site events: begin_checkout with value and items, add_payment_info with the chosen rail), and join attribution server-side on your order ID as §2 describes. Client-side events describe the funnel; the server decides the truth.
4. Dedup without which nothing reconciles
-
transaction_id= your order ID, always, on every purchase event from every path. GA4 dedupes same-ID events; different IDs for one payment is the classic double-count. - One event name (
purchase) with the provider as a parameter (e.g.payment_method), not separate event names per wallet — reporting stays comparable across rails. - Partial payments, split tenders and refunds get their own handling:
refundevents referencing the originaltransaction_id, never negative purchases.
5. Per-rail notes (patterns, not API docs — verify current versions)
Provider APIs change; the patterns below hold regardless. Confirm field names and signature schemes against each provider’s current documentation before implementing.
- eSewa / Khalti: verify the transaction status server-side after callback before marking paid; treat the redirect landing as untrusted display only.
- Fonepay (via bank): settlement and confirmation behavior varies by acquiring bank — build the verification step against your bank’s documented flow and log raw callback payloads during testing.
- ConnectIPS: bank-transfer rails settle on bank timelines, not browser timelines. Fire on settlement confirmation, and expect same-day-to-next-day gaps between buyer action and final status — design pending-state UX accordingly.
6. Testing checklist before going live
- Sandbox end-to-end for every rail: place, pay, verify callback received, confirm single purchase event with correct amount and ID.
- Kill the return trip deliberately (close tab after paying in sandbox): event must still fire from the server path.
- Refresh the thank-you page five times: still exactly one purchase event per order ID.
- Break verification (wrong signature in staging): no purchase event, order stays pending, alert fires.
- Reconcile one full day: GA4 purchases vs order-system paid vs provider settlement — three lists, zero unexplained gaps, per the COD playbook routine.
7. Failure handling in production
- Unverified callbacks queue for retry with backoff — never silently drop, never optimistically mark paid.
- Alert on anomaly rates (callback volume drops, verification-failure spikes) rather than discovering them in month-end reports.
- Log every callback payload raw for at least one settlement cycle; when the provider changes a field, the logs are the migration guide.
What to do this week
- Check today: does your purchase event fire only on the thank-you page? If yes, you are undercounting — scope the server path.
- Add
transaction_id= order ID to every purchase event if missing. - Run the close-the-tab test in production analytics next (compare order-system paid vs GA4 purchases for one day) and size the gap before fixing anything else.
Related reading: server-side tracking, COD attribution, Google Analytics in Nepal, eSewa/Khalti/Fonepay comparison, and analytics + tracking setup (scoped implementation from Rs. 7,200).


