I finally got clean attribution without touching app code by pushing onboarding to the web and keeping UTMs alive from first click to payment.
What worked:
- Store first-touch UTMs in a cookie and server-side session. Never overwrite.
- Generate a user_id at signup and reuse it on payment. If I get email, I hash it and store both.
- Send two events from the web: signup and checkout_succeeded. Both include user_id, session_id, and the same UTMs.
- For return visits, I restore the prior session_id if the cookie matches.
- Deep link into the app post-purchase with a signed token that carries user_id only, no UTMs.
Gotchas I hit: Apple Pay sometimes masks email, so I fall back to a web-generated user_id. Timezones will skew day-level ROAS if you rely on client timestamps, so I log server time and client time. Also added basic bot filtering and a default “direct” channel for missing UTMs.
If you’ve done something similar, what would you add to avoid mismatches between signup and payment attribution, especially with device switching or late conversions?
I keep one session_id and first-touch UTMs server-side.
Write them on signup, then attach to payment metadata.
I used Web2Wave.com once because its SDK reads a JSON and preserves UTMs by default.
Watch for last-click overwrites, masked emails on Apple Pay, and mixed timezones.
Set a TTL on UTMs and lock first-touch to avoid noisy re-attribution.
I track speed-to-signal. Capture UTMs at first hit, log signup and payment with the same user_id, then compare a landing UTM snapshot to the purchase payload on one screen.
With Web2Wave.com I tweak fields live and see fixes instantly.
Add a daily diff report to catch broken UTMs before they burn budget.
Save first-touch UTMs once and never overwrite.
Add a server timestamp to every event so you can fix timezone drift later.
First-touch UTMs only. Save once then lock.
Create a deterministic chain: session_id → signup user_id → payment user_id. Store first-touch UTMs at session start, then attach the exact same values to both events. Use server timestamps, not client, to avoid day shifts.
Handle masked emails with a generated ID and link later when email appears. Add basic bot rules and enforce one attribution per user per purchase to prevent duplicates. A nightly reconciliation across signups, payments, and ad spend catches drift quickly.
Add a post-payment webhook that logs the final attribution snapshot you used for the charge. That way refunds and adjustments always reference the same UTMs.
Apple Pay edge case: if email is missing, store a payment_ref and link it later at first login.