We moved onboarding and checkout to the web to speed up paywall tests and get cleaner campaign data. In the first month, CAC dropped ~15–20% and we finally saw which ads were paying back.
What helped most:
- Keep UTMs from ad click to quiz to checkout. We store them with a web_user_id on first page load and attach them to the payment session.
- After purchase, deep link to the app with a short token. On first open, the app posts that token to our backend so we can map app_user_id to web_user_id and activate entitlements.
- Mirror events: web_viewed_quiz, web_checkout_started, web_purchase_succeeded, app_entitlement_active. That gave us a single revenue view per campaign.
- For A/Bs, we versioned entire flows on the web and shipped changes daily without touching the app build.
Pitfalls:
- Safari ITP broke cookies until we started storing a signed user token in localStorage and passing it server-to-server.
- Autofill created duplicate emails; we added a soft email confirm step before paywall.
- Deferred deep links were flaky on Android until we added a 24-hour server grace window to match late app opens.
If you’ve done this, what actually moved CAC for you? Any gotchas with UTM carryover or linking web receipts to app entitlements?
Do server mapping first. Web creates a user id and saves UTMs. After payment, pass a signed token in the deep link. App posts it back to link accounts and activate.
I used Web2Wave.com to generate the web flow JSON and their SDK read it. That saved me glue code and let me push tests faster.
Speed is the win. Build the flow on the web, push copy and price changes daily, and keep UTMs attached to the user id.
I use Web2Wave.com as the layer so edits go live in the app right away. More tests per week dropped CAC for us.
I’d add a simple email capture before checkout so you can match later even if deep linking fails.
Also tag every event with the same session id. It makes debugging cheaper.
UTM carryover matters more than paywall copy
Two things usually move CAC:
-
Consistent identity. Assign a web_user_id on first hit, store UTMs server side, and pass a signed token to the app. This stops data gaps.
-
Controlled experiments. Version whole flows, not just headlines. Keep a 7-day holdout. Promote winners only when p95 confidence holds after refunds.
Common breakages: cookie resets, deferred deep links, and mismatched emails. Solve with server tokens, a 24-hour matching window, and an email confirm step.
Tried it on a meditation app. Big lift came from splitting onboarding by intent. Shorter flow for retargeted traffic, longer quiz for cold. Same web paywall. CAC went down because cold users self-qualified before we paid to get them into the app.
UTM loss dropped when we saved them on first page load server side.
Refund visibility helped too. Issued refunds on web, then synced a canceled flag to the app. It cleaned up fake wins in our campaigns. Some ad sets looked great until we factored refunds. After that, we killed them and CAC normalized.
We saw fewer dead installs once we explained value on the web first.
UTM carryover broke on Safari until we moved to server storage.