← All posts
Revenue attribution5 min read

RevenueCat + MMP: How Subscription Revenue Should Flow Back to Campaigns

A working integration pattern for tying RevenueCat trial, purchase, renewal, and refund events to acquisition campaigns, with the subscriber-attribute model that scales past 100k MAU.

Most subscription apps end up with two dashboards: one for revenue (RevenueCat), one for acquisition (the MMP). They never quite match. Trials count differently. Refunds show up in one and not the other. The growth team builds a spreadsheet that reconciles them every Monday morning until someone leaves and the spreadsheet rots.

The fix is structural: RevenueCat and the MMP need to share an identity, not exchange CSVs. Done properly, every subscription event lands in the MMP with a campaign, a creative, and a keyword already attached. This post is the working pattern.

The identity problem in one paragraph

RevenueCat thinks in terms of app_user_id. Your MMP thinks in terms of an install identity it generates when the SDK first runs (typically a UUID). Apple thinks in terms of original purchase ID. Your auth system thinks in terms of a user record. None of these are the same identifier, and none of them are immediately available at install time. The integration's job is to glue them together at the right moments without forcing your app to keep four ID maps in sync forever.

The pattern

Three steps, performed by the app at install time.

Step 1: Configure the MMP SDK first, before RevenueCat. This matters because the MMP needs to capture install context (the ad click that produced this install, the campaign, the deferred deep link payload) before any other SDK fires. If RevenueCat starts a session first, its anonymous user ID gets associated with no acquisition context and you'll spend the next week debugging why your "organic" bucket grew 40%.

// At app start, root layout, App.tsx — wherever your bootstrap lives
await AppSprint.configure({ apiKey: process.env.APPSPRINT_API_KEY })
const installId = await AppSprint.getInstallId()
await Purchases.configure({ apiKey: REVENUECAT_KEY })
await Purchases.setAttributes({ appsprint_install_id: installId })

Step 2: Set the install ID as a RevenueCat subscriber attribute. RevenueCat fires every webhook event with the subscriber's attributes attached. Once appsprint_install_id is on the subscriber, every INITIAL_PURCHASE, RENEWAL, CANCELLATION, and BILLING_ISSUE event will carry it. The MMP can match the event back to the original install without any other plumbing.

Step 3: Treat the RevenueCat webhook as the revenue source of truth. The MMP receives the webhook (either directly or via your backend forwarding it), looks up the install by appsprint_install_id, and writes a revenue event against that install's campaign attribution. Now the MMP dashboard knows: this campaign, this creative, this keyword produced $X in trial conversions and $Y in renewals.

That's the whole pattern. Three lines of glue code at startup, plus the webhook.

What the MMP should do with each event

Different RevenueCat events deserve different treatment in the MMP report. The mapping that actually drives decisions:

RevenueCat eventWhat it means for the campaign
INITIAL_PURCHASE (free trial)Trial start. Useful as a leading signal but rarely the right ROAS target.
INITIAL_PURCHASE (paid)Paid acquisition the campaign actually wanted. Strong signal.
RENEWALThe campaign is producing durable revenue. Highest-value event for LTV modeling.
CANCELLATIONA cohort signal, not an individual signal. Useful aggregated at the campaign level.
BILLING_ISSUEMostly noise at the campaign level; watch in aggregate.
REFUNDShould subtract from the campaign's revenue. Many teams forget this.
PRODUCT_CHANGEPlan upgrades and downgrades — re-attribute revenue at the new price.

A subscription app at $50k+ MRR usually has more renewal revenue than initial-purchase revenue. If your MMP is showing campaign ROAS based on initial purchases only, you're under-counting the winners by 3–5× and the losers by however much they're churning. The renewal event is the one that earns the line item.

What breaks at scale, and how to handle it

App reinstalls. When a user deletes the app and reinstalls a week later, the MMP issues a new install ID. RevenueCat issues a new anonymous user ID, then merges it back to the original subscriber when the user signs in. The appsprint_install_id attribute now points to the new install. The right fix is to keep the original attribute (appsprint_install_id) as the lifetime acquisition pointer, and add a second attribute (appsprint_current_install_id) that updates on each install. Most teams won't need this, but the apps that do need it discover it late.

Backend-side identity migration. If your app issues its own user ID once the user signs in, attach the MMP install ID to the user record server-side. That lets you reconcile attribution against your own data warehouse later without depending on RevenueCat as the only ID broker.

Subscriber attributes get truncated. RevenueCat truncates very long attribute values. UUIDs are fine; long opaque tokens are not. Keep appsprint_install_id to under 100 characters and you're safe.

Webhook ordering. RevenueCat webhooks can arrive out of order during retries. The MMP needs to be idempotent on event ID — if RENEWAL arrives before its preceding INITIAL_PURCHASE, the install should still attribute correctly. AppSprint dedupes on RevenueCat's event ID and reorders by event timestamp before writing to the campaign report.

The mistake to avoid

Many teams set up RevenueCat → MMP forwarding through Zapier or a custom worker that converts webhook events into MMP "purchase" events. That works for the first 90 days. Then you realise refunds aren't subtracting, plan changes aren't re-attributing, and the cancellation cohort view doesn't exist because cancellations were never forwarded.

The right shape is: the MMP receives the raw RevenueCat webhook and handles each event type natively. Anything less means you're rebuilding RevenueCat's event model inside your MMP setup, badly.

Where AppSprint fits

AppSprint accepts the RevenueCat webhook directly. The appsprint_install_id subscriber attribute is the only piece of glue your app sets. Every RevenueCat event type listed above maps to a typed event in the AppSprint reporting layer, including refunds (subtractive) and plan changes (re-attributive). The dashboard shows trial start, paid conversion, renewal revenue, and refund-adjusted net revenue by campaign and by keyword.

For a subscription app at $50k+ MRR, the combined system is the difference between two dashboards that disagree every week and one report that the growth team actually reads. The integration takes about 20 minutes once the apps are set up; the value compounds for every subsequent campaign decision.

You might also like

See all posts →