The Problem You're Facing

You have a live iOS app with paying subscribers. Your in-app purchase code runs on StoreKit 1 (the receipt-based model), and your RevenueCat integration is on SDK v4 or earlier. Apple has progressively deprecated StoreKit 1 APIs, new features like Win-Back Offers only work with StoreKit 2, and your analytics tools increasingly flag receipt validation gaps.

The question isn't if you need to migrate — it's how to do it safely without breaking subscription access for existing users or losing transaction history.

This tutorial walks you through every migration pattern RevenueCat supports, covering SDK upgrades, credential changes, legacy subscriber handling, and the edge cases that trip teams up in production.


Why StoreKit 2 Now?

StoreKit 2 (SK2), introduced in iOS 15, replaces the opaque base64 receipt blob with individually signed, verifiable transactions. The concrete developer benefits when moving to RevenueCat SDK v5 include:

  • Eliminated receipt errors — "Missing receipt" errors in Sandbox that could break trial eligibility checks, and "The purchased product was missing in the receipt" purchase errors, are both eliminated by SK2's per-transaction model. Source
  • ~200ms faster validation — RevenueCat found that receipt validation is ~200ms faster at the p95 level compared to the SK1 implementation. Source
  • Future-proofing — StoreKit 1 APIs are being progressively deprecated by Apple; all new capabilities (Win-Back Offers, improved promotional offers) live in SK2. Source
  • Unlocked features — Win-Back Offers, Introductory Offer eligibility with In-App Purchase Keys, and improved pricing accuracy all require SK2. Source

Pre-Migration Checklist

Before touching a single line of code, verify two things in your RevenueCat dashboard:

1. Configure an In-App Purchase Key (Critical)

This is the most commonly missed step and causes the most post-deployment incidents.

⚠️ When upgrading to RevenueCat SDK v5, you must configure your In-App Purchase Key in the RevenueCat dashboard. Purchases will fail to be recorded without this key being set. Source

SK2 uses the App Store Server API (not the legacy receipt-validation endpoint), and RevenueCat needs this key to call it on your behalf. This affects all cross-platform SDKs at their equivalent major versions:

SDK Minimum version requiring the key
iOS native 5.0.0+
Flutter 7.0.0+
React Native 8.0.0+
Capacitor 9.0.0+
Cordova 6.0.0+
Unity 7.0.0+

Source

How to generate and upload it:

  1. In App Store Connect, go to Users and Access → Integrations → In-App Purchase.
  2. Select Generate In-App Purchase Key (or click "+" next to Active). Name it, then download the .p8 file immediately — you only get one chance.
  3. In the RevenueCat dashboard, go to Project Settings → Apps → [Your App] → In-app purchase key configuration.
  4. Upload the .p8 file, enter the Issuer ID (found on the same App Store Connect page), and click Save Changes.

Source

💡 If you already have an App-Specific Shared Secret configured for SK1, you can keep it during a staged rollout. For full SK2 apps, the In-App Purchase Key replaces it. Source

2. Confirm Deployment Targets

SDK v5 raises minimum deployment targets. Check your Xcode project before bumping the SDK version:

Platform Minimum (SDK v4) Minimum (SDK v5)
iOS 11.0 13.0
tvOS 11.0 13.0
watchOS 6.2 6.2
macOS 10.13 10.15

Source


This is the standard path for apps where RevenueCat handles all purchases. Bump the SDK, remove deprecated config options, and everything switches to SK2 automatically.

Step 1: Update the SDK

Update RevenueCat to 5.x in your Package.swift, Podfile, or Gradle file.

Step 2: Remove the Deprecated Configuration Flag

In SDK v4, you could opt into SK2 with:

// SDK v4 — REMOVE THIS
Purchases.configure(
    with: Configuration.Builder(withAPIKey: "your_api_key")
        .with(usesStoreKit2IfAvailable: true) // deprecated, now removed
        .build()
)

In SDK v5, StoreKit 2 is the default. Simply remove the .with(usesStoreKit2IfAvailable:) call:

// SDK v5 — SK2 is on by default
Purchases.configure(
    with: Configuration.Builder(withAPIKey: "your_api_key")
        .build()
)

Source

Step 3: Handle the SK1 Fallback (It's Automatic)

On older OS versions where SK2 isn't available, the SDK automatically falls back to SK1 — no code required from you:

The SDK will automatically use StoreKit 1 on macOS 12 or earlier, iOS 15 or earlier, iPadOS 15 or earlier, tvOS 15 or earlier, or watchOS 8 or earlier. Source

Step 4: Force SK1 If You Must (Escape Hatch)

If you have a critical blocker preventing SK2 adoption, you can force SK1 globally using the configuration API. This is a temporary escape hatch, not a long-term strategy, since SK1 APIs are being deprecated by Apple.

// Force SK1 — only use this as a temporary measure
Purchases.configure(
    with: Configuration.Builder(withAPIKey: "your_api_key")
        .with(storeKitVersion: .storeKit1)
        .build()
)

Source


Migration Pattern 2: Observer Mode / PurchasesAreCompletedBy

If your app has its own StoreKit purchase handling code and you're only using RevenueCat for tracking, SDK v5 renames and restructures the old "observer mode" concept.

Version 5.0 deprecates "Observer Mode" and replaces it with PurchasesAreCompletedBy (either RevenueCat or your app). Source

The Pattern (Before → After)

// SDK v4 (Observer Mode)
Purchases.configure(
    with: Configuration.Builder(withAPIKey: "your_api_key")
        .with(observerMode: true) // deprecated
        .build()
)

// SDK v5 (PurchasesAreCompletedBy)
Purchases.configure(
    with: Configuration.Builder(withAPIKey: "your_api_key")
        .with(purchasesAreCompletedBy: .myApp, storeKitVersion: .storeKit2)
        .build()
)

Source

You must specify the storeKitVersion explicitly when using purchasesAreCompletedBy: .myApp, so RevenueCat knows which transaction format to expect. Source

macOS Edge Case: recordPurchase

On macOS, when your app completes a SK2 purchase (not RevenueCat), the SDK doesn't detect the purchase until the user foregrounds the app. To get immediate detection:

// Call this right after your own SK2 purchase succeeds on macOS
let result = try await product.purchase()
await Purchases.shared.recordPurchase(result)

Source

Syncing Historical Purchases in Observer Mode

When you first add RevenueCat to an app with existing subscribers, historical purchases won't appear automatically. Call syncPurchases() once per user on their first launch with the new SDK:

// Pseudo-pattern: sync once, not on every launch
if !userDefaults.bool(forKey: "rcPurchasesSynced") {
    do {
        let customerInfo = try await Purchases.shared.syncPurchases()
        userDefaults.set(true, forKey: "rcPurchasesSynced")
        // Check entitlements from customerInfo
    } catch {
        print("Sync error: \(error)")
    }
}

Do not call syncPurchases() on every app launch. This increases latency and can unintentionally alias customers together. Source


Migration Pattern 3: Migrating Existing Subscribers (Historical Data)

Bumping the SDK handles new transactions. But what about subscribers who won't update their app immediately? You have two paths:

Path A: Server-Side Import (Preferred)

If you have stored Apple receipts or SK2 signed transaction JWTs on your server, POST them directly to RevenueCat's API before or during rollout:

POST https://api.revenuecat.com/v1/receipts
Authorization: Bearer YOUR_SECRET_API_KEY

{
  "app_user_id": "user_123",
  "fetch_token": "<base64_receipt_or_SK2_jwt>"
}

RevenueCat automatically deduplicates receipts, so you can safely forward new receipts immediately and then batch-process historical ones. Source

For extraordinarily large imports, RevenueCat offers bulk import as a service. Contact their sales team to discuss options. Source

Path B: Client-Side SDK Import

If you don't have receipts server-side, rely on the SDK to sync when users open the updated app. This is simpler but means historical data only appears as users launch the new version:

A client-side migration is best used when you don't have access to the raw base64 Apple receipts, StoreKit 2 signed transactions, or Google purchase tokens needed for a full receipt import. Source

The trigger: check your existing entitlement source vs. RevenueCat, and call syncPurchases() once per user when there's a mismatch.


Migration Pattern 4: Trusted Entitlements (New in SDK v5)

SDK v5 enables Trusted Entitlements in Informational mode by default. This uses cryptographic signatures on entitlement responses to detect man-in-the-middle attacks.

Informational mode logs verification errors and allows you to check customerInfo.entitlements.verificationResult to protect your purchases from attackers. Source

Checking the result in code:

let customerInfo = try await Purchases.shared.customerInfo()
let entitlements = customerInfo.entitlements.active

if customerInfo.entitlements.verificationResult == .verified {
    // Full trust — unlock premium features
    unlockPremium()
} else if customerInfo.entitlements.verificationResult == .failed {
    // Possible tampering — log and handle defensively
    logSecurityEvent()
    // You decide: block access or allow with a warning
}

Important: Enabling Trusted Entitlements does not automatically protect your app. The SDK provides verification data, but it's your responsibility to check the verification result and decide how to handle unverified entitlements. Source

If you're upgrading from an older SDK version where cached CustomerInfo was unverified, you can clear the cache to avoid stale results:

Purchases.shared.invalidateCustomerInfoCache()

Source


Common Gotchas

Third-Party Analytics SDKs Break with SK2

This catches many teams off-guard post-migration:

Most third-party analytics SDKs do not completely support logging purchases made with StoreKit 2. This is the case for popular SDKs like Facebook, Mixpanel, OneSignal, Segment, and Firebase. Source

Fix: Use RevenueCat's data integrations to forward purchase events to these services instead of relying on the analytics SDKs to auto-detect them. For Firebase specifically, follow Google's instructions to manually log SK2 purchases.

Historical Data May Change After Adding the In-App Purchase Key

If you're adding an In-App Purchase Key to an app with existing transactions (common when migrating apps from older SDK versions), expect some historical chart data to update:

Adding an in-app purchase key to an app with existing transactions may change historic data, as RevenueCat updates previously estimated data with corrected data from Apple. You may see an increased number of updated transactions in Scheduled Data Exports. Source

This is expected behavior — the data is becoming more accurate, not corrupt.

Win-Back Offers Require SK2

If Win-Back Offers are on your roadmap, note they have a hard SK2 dependency:

Since Win-Back Offers use StoreKit 2 under the hood, you must upload an In-App Purchase Key to RevenueCat to use Win-Back Offers. Source

This is another reason to complete the migration sooner rather than later.


Migration Verification Checklist

Run through this before releasing to production:

  • [ ] In-App Purchase Key uploaded to RevenueCat dashboard and shows "Valid credentials"
  • [ ] SDK bumped to v5.x (or platform-equivalent major version)
  • [ ] .with(usesStoreKit2IfAvailable: true) removed from SDK configuration
  • [ ] observerMode: true replaced with .with(purchasesAreCompletedBy: .myApp, storeKitVersion: .storeKit2) if applicable
  • [ ] recordPurchase() called on macOS for self-managed SK2 purchases
  • [ ] Third-party analytics switched to RevenueCat data integrations (not auto-detect)
  • [ ] syncPurchases() implemented once-per-user for existing subscribers (not on every launch)
  • [ ] verificationResult checked in entitlement-gating code
  • [ ] Sandbox purchase tested end-to-end and transaction visible in RevenueCat dashboard

Sources