RevenueCat Integration: Subscription Management for Mobile Apps

Implement in-app purchases, subscription analytics, and webhook handling with RevenueCat for iOS and Android.

RevenueCat Integration: Subscription Management for Mobile Apps

What Is RevenueCat and Why Does It Matter?

Managing in-app subscriptions across iOS, Android, and Flutter without a dedicated layer is a recipe for inconsistency. RevenueCat abstracts Apple's StoreKit, Google Play Billing, and Stripe into a single API so you can focus on product logic rather than receipt validation nightmares. This guide walks through a production-grade RevenueCat integration — from SDK setup to webhook-driven backend logic, grace period handling, and pricing experiments.

SDK Setup Across Platforms

Start by creating a RevenueCat project in the dashboard and noting your public API keys for iOS and Android. For Flutter, add the purchases_flutter package:

flutter pub add purchases_flutter

Initialize the SDK early in your app lifecycle — in main.dart before runApp:

import 'package:purchases_flutter/purchases_flutter.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Purchases.setLogLevel(LogLevel.debug);
  PurchasesConfiguration config;
  if (Platform.isAndroid) {
    config = PurchasesConfiguration('goog_XXXX');
  } else {
    config = PurchasesConfiguration('appl_XXXX');
  }
  await Purchases.configure(config);
  runApp(MyApp());
}

For React Native and native iOS/Android, the initialization pattern is similar — supply the platform-specific key and call configure once at app start.

Entitlements, Products, and Offerings

RevenueCat's mental model separates what a user can access (Entitlements) from what you sell (Products) and how you present them (Offerings). Create an entitlement called pro_access in the dashboard, then attach your App Store and Play Store product IDs to it. An Offering bundles one or more packages — monthly, annual, lifetime — into a paywall context.

Fetch the current offering at runtime and display it to users without hardcoding prices:

final offerings = await Purchases.getOfferings();
if (offerings.current != null) {
  final packages = offerings.current!.availablePackages;
  // Render packages dynamically
}

This approach lets you run pricing experiments from the dashboard without shipping a new app version.

Executing the Purchase Flow

Purchasing is a single async call:

try {
  final customerInfo = await Purchases.purchasePackage(selectedPackage);
  if (customerInfo.entitlements.active.containsKey('pro_access')) {
    // Unlock features
  }
} on PurchasesErrorCode catch (e) {
  if (e != PurchasesErrorCode.purchaseCancelledError) {
    // Show error UI
  }
}

Never gate features purely on local state. Always verify customerInfo.entitlements.active on each app resume. RevenueCat caches this data and refreshes it from Apple/Google servers periodically.

Webhook Integration with Your Backend

For server-side access control — particularly important for multi-platform apps or web dashboards — configure RevenueCat webhooks to POST events to your backend. Create an endpoint in Next.js:

// pages/api/revenuecat-webhook.ts
import { NextApiRequest, NextApiResponse } from 'next';

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  const authHeader = req.headers.authorization;
  if (authHeader !== `Bearer ${process.env.RC_WEBHOOK_SECRET}`) {
    return res.status(401).end();
  }
  const { event } = req.body;
  if (event.type === 'INITIAL_PURCHASE' || event.type === 'RENEWAL') {
    await db.subscriptions.upsert({
      userId: event.app_user_id,
      status: 'active',
      expiresAt: new Date(event.expiration_at_ms),
    });
  }
  if (event.type === 'EXPIRATION') {
    await db.subscriptions.update({ userId: event.app_user_id, status: 'expired' });
  }
  res.status(200).end();
}

Handling Grace Periods and Billing Retry

Both Apple and Google offer billing grace periods — typically 6–16 days — where a subscription has failed to renew but the user still has access. RevenueCat surfaces this via the BILLING_ISSUE event and marks entitlements with a billingIssueDetectedAt timestamp. In your UI, show a soft warning banner rather than hard-locking the user:

final info = await Purchases.getCustomerInfo();
final entitlement = info.entitlements.active['pro_access'];
if (entitlement?.billingIssueDetectedAt != null) {
  showBanner('Update your payment method to maintain access');
}

Stripe vs In-App Purchases

For web purchases, RevenueCat supports Stripe-billed subscriptions that are linked to the same entitlement system as mobile IAPs. This is powerful for SaaS products with both web and mobile access. The key trade-off: Apple and Google take 15–30% of IAP revenue whereas Stripe charges ~2.9% + $0.30. Where platform rules permit — typically B2B contexts — routing users to web checkout via Stripe dramatically improves margins.

Analytics Dashboard and Pricing Experiments

RevenueCat's Charts provide cohort-level retention, MRR, churn, and trial conversion data without requiring a separate analytics integration. Use Experiments to A/B test price points or package configurations — RevenueCat handles the random assignment and statistical significance calculations. A common test pattern is comparing a monthly-first flow against an annual-first flow to measure LTV differences.

Key Production Checklist

  • Always validate purchases server-side via webhook, not client-side alone
  • Set up Sandbox test accounts in both App Store Connect and Google Play Console
  • Configure grace period handling before launch
  • Enable Stripe as a web payment source if you have a web product
  • Review RevenueCat's data retention and privacy policies for GDPR compliance

RevenueCat removes weeks of subscription infrastructure work. With the patterns above, you can ship a robust subscription system across all platforms, backed by server-side enforcement and data-driven pricing iteration from day one.

Want this for your business?
See the SaaS Build service