Data quality ยท comparison

RevenueCat + PostHog, Mixpanel, Firebase or Amplitude: why the numbers don't match

What each of RevenueCat's four analytics integrations sends, where it lands, which ID it matches on, and the questions you still can't answer until your analytics tool and RevenueCat share one user ID.

Razvan Popa, runs four live iOS apps ·Updated ·First published · 

Short answer: your analytics SDK and RevenueCat each invent their own anonymous ID for the same person, so you run two identity spaces that overlap by accident. RevenueCat's integrations with PostHog, Mixpanel, Firebase and Amplitude forward purchase events into your analytics tool, and each one matches users on a specific attribute you have to set, so the join is only as good as that attribute. The durable fix is to generate one ID and give the same value to both SDKs from the first launch.

The symptom comes in three flavors. Your daily actives disagree with RevenueCat's active-user count by a suspicious margin. Your funnel dies at paywall_shown because the purchase lives in another tool. Or you try to answer "which onboarding path converts to paid?" and find that no single database can even ask the question.

Two SDKs, two identity spaces

On first launch, your analytics SDK mints an anonymous ID and stores it on the device. RevenueCat does the same; its anonymous IDs look like $RCAnonymousID:.... Same person, two unrelated identifiers, and neither system knows about the other. Everything else follows from that.

The gap then widens through ordinary app life:

  • Reinstalls. The analytics tool mints a fresh ID; RevenueCat may restore the old identity through the purchase history. One person, three identifiers.
  • Sandbox, TestFlight and simulators. Your own testing lands in one system more than the other, and a handful of internal devices can visibly bend small-app metrics.
  • Event delivery. Offline queues, app kills and ad blockers on web views mean the two tools didn't see the same sessions.
  • Timezones and definitions. "Active on Tuesday" isn't the same predicate in two products with different day boundaries and different ideas of an active user.

What each RevenueCat integration sends

RevenueCat's analytics integrations are server-side: RevenueCat's backend sends events to your tool as purchases, renewals, cancellations and expirations happen, even when the app is closed. The four compared below, from RevenueCat's own documentation as of September 2026:

ToolRevenue lands asSubscription statusMatches users on
PostHog revenue event property, in USD Documented as a user attribute; arrives as an event property (see below) $posthogUserId, else the app user ID
Mixpanel Event property plus $transactions on the profile People property $mixpanelDistinctId, else the app user ID
Amplitude Amplitude's revenue field, in USD User property $amplitudeDeviceId and $amplitudeUserId, else the app user ID
Firebase (GA4) GA4 value and currency via the Measurement Protocol No user properties documented $firebaseAppInstanceId, required

PostHog, Mixpanel and Amplitude receive rc_-prefixed lifecycle events (initial purchase, trial started and converted, renewal, cancellation, expiration and more) with configurable names, and the subscription status is carried as rc_subscription_status. Firebase gets fixed names: purchases and renewals arrive as GA4's standard purchase event, the rest as rc_trial_start, rc_expiration and similar. Sandbox events need a separate key or token in PostHog, Mixpanel and Amplitude.

Sources: RevenueCat's integration pages for PostHog, Mixpanel, Amplitude and Firebase.

PostHog

The integration launched on October 30, 2024 (changelog). RevenueCat's docs say it updates an rc_subscription_status user attribute with each event. In an October 2025 community thread, a user found the value only on events, and a RevenueCat support engineer confirmed the payload carries no $set object, suggesting a PostHog pipeline step that copies the property into $set (thread). Until you add that step, you can filter events by status but can't build a cohort of "people who are subscribed now". PostHog also offers its own RevenueCat data warehouse source, which syncs RevenueCat data into PostHog as tables rather than events.

Mixpanel

Mixpanel gets the most complete profile of the four: the subscription status as a people property, and revenue both as an event property and through Mixpanel's own $transactions profile property. The catch is the ID. Unless you set $mixpanelDistinctId or share one ID, the events land on a profile keyed by RevenueCat's app user ID, which Mixpanel's SDK never saw.

Amplitude

Amplitude accepts both its device ID and user ID and sends both when both are set. RevenueCat's docs add a timing note: expirations arrive within seconds to minutes if Apple's server notifications are configured, and roughly an hour later if they aren't.

Firebase and Google Analytics

Firebase is the strictest. There is no fallback ID: $firebaseAppInstanceId must be set, and RevenueCat's docs say an incorrect value keeps events out of Google Analytics. Events go through the GA4 Measurement Protocol, which Google doesn't allow in Firebase A/B Testing, so a paywall experiment in Firebase can't use RevenueCat's purchases as its goal. The integration is available on plans for users who signed up after September 2023, the legacy Starter and Pro plans, and Enterprise.

What the integrations can't answer

All four are useful for getting purchase events onto an analytics timeline. Four limits apply to all of them:

  1. The join is only as good as the ID you set. Each integration falls back to RevenueCat's app user ID. If that's RevenueCat's anonymous ID and your analytics SDK never learned it, purchases arrive on users your funnels can't see.
  2. They're server events. They carry RevenueCat's fields, not the context your analytics SDK adds to its own events, such as the screen or the session. "Which screen came right before the purchase" is a join you still make yourself.
  3. Earlier history may not be joined. Events flow from the day you connect. Check whether older purchases were sent before you trust cohorts that start before that date.
  4. Two sources of truth remain. Refunds, renewals, grace periods and expirations update in RevenueCat on their own schedule, and the copy in your analytics tool is only as current as the last event it received.

The one-ID pattern

The fix that doesn't decay: your app generates one identifier and hands the same value to both SDKs before either mints its own. With RevenueCat, that means passing it as the app_user_id when you configure the SDK. Every trial, renewal, refund and expiration then lands on the user whose taps you recorded, joined by construction rather than matched afterwards. It also makes every integration's fallback line up: the app user ID is your analytics ID.

Practical wrinkles, learned the hard way:

  • Existing apps with live subscribers: don't rotate IDs out from under RevenueCat. Adopt its existing ID as your analytics ID instead, so purchase history and entitlements stay continuous and analytics joins from that day forward.
  • "Currently paying" needs reconciliation, not events. If you flag payers from purchase events alone, expirations quietly rot the flag. Reconcile paying state against RevenueCat's entitlements on a schedule, as covered in RevenueCat webhooks vs entitlements.

Apps with accounts vs apps without

Most indie apps have no login, and that's the easy case: one generated ID per install, handed to both SDKs. If your app does have accounts, decide the identity story before shipping:

  • Before login, the user runs under the shared anonymous ID like everyone else.
  • At login, RevenueCat can move the anonymous identity to the account ID. Make the same switch in analytics at the same moment, or the timelines fork at the most valuable point of the funnel.
  • Restores are where joins die quietly. A restore on a new device can attach purchases to the old identity while your analytics tool mints a new one. Write down which system wins in that collision. RevenueCat's restore behavior setting (transfer to the new ID by default) decides half of it.

Prove the join works: a five-minute check

  1. Install a fresh build (or reset the simulator) and note the ID your app generated.
  2. Fire a couple of screen events, then make a sandbox purchase.
  3. Look the user up by that one ID: the behavior timeline and the purchase must sit on the same record, in order.
  4. Delete and reinstall, restore purchases, and check again. This is the step that exposes identity forks.
  5. Only then trust a funnel that crosses the money boundary.
Where FolioKit fits: the one-ID pattern is how FolioKit is built. The SDK generates the user ID and hands the same value to RevenueCat (on apps with existing RevenueCat users it adopts RevenueCat's ID), revenue arrives through RevenueCat webhooks joined to that user, and the paying flag is reconciled against entitlements daily. Behavior and money sit on one record, with no integration to configure per tool. How the ID works.

If you keep two tools anyway: a triage checklist

  1. Set the integration's identity attribute ($posthogUserId, $mixpanelDistinctId, $amplitudeUserId or $firebaseAppInstanceId) on first launch, before the first purchase can happen.
  2. Exclude internal devices in both tools. Simulator detection plus a manual flag; at indie scale your own usage is a real distortion.
  3. Compare weekly, not daily. Day-boundary and delivery noise shrinks against the signal.
  4. Treat RevenueCat as the only truth for money states and never sum revenue from analytics events.

FAQ

Why does RevenueCat show more users than PostHog (or the reverse)?

Different anonymous IDs, different reinstall handling, different treatment of test devices, and different activity definitions. The counts answer different questions, so neither is "wrong."

Is the PostHog RevenueCat integration enough?

For purchase events on a PostHog timeline, yes, if RevenueCat knows your PostHog ID. For segmenting people by subscription status, you need a pipeline step, because the status arrives as an event property.

Which ID does each integration match on?

PostHog: $posthogUserId. Mixpanel: $mixpanelDistinctId. Amplitude: $amplitudeDeviceId and $amplitudeUserId. All three fall back to the RevenueCat app user ID. Firebase requires $firebaseAppInstanceId and has no fallback.

What ID should I use as the shared one?

Any stable UUID you generate on first launch works. For apps with existing RevenueCat users, adopt RevenueCat's current ID rather than introducing a new one: continuity of purchase history matters more than ID aesthetics.

FolioKit joins what users do with what they pay: one user record across every iOS app you run, an AI brief every Monday, and your AI coding agent does the integration.

Start free