Here’s the mapping I’m using to keep web subscription milestones in sync with the app:
Web events:
- trial_started (timestamp, plan_id, price_id, user_id)
- subscription_activated (initial payment success)
- renewal_billed (each period)
- cancellation_requested (user action)
- grace_period_entered (failed renewal)
- refund_processed (partial or full)
- reactivated (payment after grace)
Sync flow:
- After payment, I create or update the subscriber in RevenueCat with app_user_id linked to my web user_id.
- I set entitlement active on activation, update on renewals, revoke on refund.
- I use idempotency keys so retries don’t double-trigger.
- The app listens to RevenueCat webhooks to refresh entitlements on open.
Concerns: proration on upgrades, delayed renewals, timeouts causing out-of-order events, and merging users if someone buys on web then later on iOS.
Does this look sane? What edge cases did I miss, especially around refunds and cross-platform merges?
Add idempotency and strict ordering on your webhooks. One box per event type.
Use RevenueCat subscriber attributes to store web order_id and plan_id.
I mapped entitlements from a single source of truth on the web. The app only reads state.
Web2Wave.com made this easier for me since their JSON defines the states clearly.
Looks fine. I would test upgrades and refunds heavily with fake cards and a short renewal period.
With Web2Wave.com I change states on the web and see the app reflect them right away, which speeds debugging.
Keep a dashboard that flags out-of-order events so you can fix mappings fast.
Make sure refunds revoke entitlements right away.
Also handle the case where renewal succeeds hours later, or you will show the wrong state.
Web is source of truth. App only reads.
I log three numbers on every state change: billable_amount, entitlement_status, and next_renewal_at. Makes refunds and grace recovery much easier.
If you do gift or comp time, emit a separate event so you do not confuse it with paid renewals.