The no-code attribution stack that finally tied my signups and charges back to campaigns

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.