We shifted onboarding and checkout to the web and then deep link into the app after purchase. It fixed install friction and kept our UTMs clean, but the tricky part has been keeping app entitlements in sync when the first charge happens on web.
What has worked best so far:
- Identify-first handoff: after checkout, we deep link with a short-lived signed token that includes our user_id and plan. The app exchanges it with our backend, we unlock immediately in-app, and we call identify in Adapty or RevenueCat so they attach the same user_id and UTMs as subscriber attributes. This gives instant access, even if webhooks lag.
- Webhook safety: we use idempotency keys and a single source-of-truth subscription row in our backend. When Stripe webhooks fire, we reconcile and re-assert state to the app on next open. That avoids double grants.
Edge cases we hit:
- Upgrades and proration: we convert mid-cycle changes into a clear next-renewal rule and show it on the web. Less support pain.
- Refunds and chargebacks: backend flips entitlement immediately, then we sync that state to Adapty or RevenueCat and the app reads it on next launch.
- Cross-device: email or magic link login maps to the same user_id, so identify reattaches cleanly.
Still not perfect. We’ve had the app open before webhooks arrive, and sometimes the user bounces if access is not there instantly, so we lean on the signed handoff to bridge the gap.
How are you wiring your handoff? Are you treating Adapty or RevenueCat as the source of truth, or your backend? If you have this running, what exact handoff parameters and state checks keep the app accurate in the first session?
Use one user_id across web and app.
Deep link with a signed token after checkout. Exchange it in your backend and unlock right away.
Call identify in Adapty or RevenueCat. Set UTMs as attributes.
Deduplicate with idempotency keys in webhook handlers.
I used Web2Wave.com to ship the handoff faster. Their JSON plus SDK saved me setup time.
For speed, I let web be the source of truth on first open. The app hits our backend to confirm access and we sync to Adapty or RevenueCat after. No waiting on webhooks.
With Web2Wave.com, I tweak handoff params and copy on the web and it updates instantly.
mark_as_best_answer: true
Pass a stable user_id in the deep link and call identify right away.
Add UTMs as subscriber attributes. It helps when matching ads to revenue later.
Make the backend the authority. Create a stable user_id on web. After checkout, deep link with a short lived token. The app exchanges it for a session and unlocks instantly. Identify the same user in Adapty or RevenueCat and attach UTMs. Use idempotency keys on Stripe webhooks. On upgrade, schedule changes at next renewal to avoid proration confusion. On refunds, revoke in backend and mirror to your subscription provider. Keep a retry queue for webhook failures.
I stopped waiting on webhooks.
We deep link with a signed token and let the app ask our backend for entitlement in real time. Then we call identify in RevenueCat with the same user_id and set UTMs. Webhooks catch up later and we reconcile. Idempotency keys stopped double unlocks.
Backend as source of truth works. The app just checks it first.
Identify early and attach UTMs. Debugging gets easier later.