You're starting a new iOS subscription app in 2025. You add RevenueCat v5, write a couple of lines to configure the SDK, and ship. But what exactly just happened under the hood? What security guarantees did you get for free? Which legacy APIs did you silently stop using? And where are the landmines — the things that seem like they work but quietly fail in specific environments?

This guide is a capabilities tour: exactly what you get when you pair RevenueCat iOS SDK v5 with StoreKit 2 (SK2) natively — no legacy configuration, no migration baggage. We'll cover SK2 as the new default, the credential swap you must make, the cryptographic verification that ships on day one, win-back offer support on iOS 18, a Mac Catalyst gotcha, and the purchasesAreCompletedBy decision most developers don't think about until something breaks.


1. SK2 Is the Default — No Configuration Flag Needed

The most important thing to understand about RevenueCat v5: StoreKit 2 is enabled by default, full-stack. The old .with(usesStoreKit2IfAvailable: true) configuration option has been removed from the SDK entirely. You don't opt in to SK2 — it's simply what you get. Source

What does "full SK2 flow" actually mean?

With v5, both the SDK and the RevenueCat backend use the StoreKit 2 code path. This matters because SK2 unlocks real fixes to annoying edge cases that were fundamentally broken in SK1:

  • No more "Missing receipt" errors in Sandbox that caused failures when restoring purchases or checking trial eligibility status.
  • No more "The purchased product was missing in the receipt" error that caused invalid receipt errors at purchase time.
  • ~200ms faster receipt validation at the p95 level versus SK1, because SK2 uses the App Store Server API directly rather than receipt files. Source

Beyond fixing existing bugs, Apple is actively deprecating SK1 APIs. New features — like win-back offers — are SK2-only. The longer you stay on SK1, the more you're swimming against the current. Source

Automatic fallback to SK1

SK2 requires iOS 16+, macOS 13+. RevenueCat handles the fallback automatically — you don't write any conditional logic. The SDK will use StoreKit 1 automatically on: macOS 12 or earlier, iOS 15 or earlier, iPadOS 15 or earlier, tvOS 15 or earlier, or watchOS 8 or earlier. Source

One caveat for 3rd party analytics SDKs: If you're using Facebook, Mixpanel, OneSignal, Segment, or Firebase to auto-track IAPs, be aware that most of them don't fully support SK2. RevenueCat recommends using data integrations instead. Source


2. The Credential Swap: In-App Purchase Key Replaces the Shared Secret

This is the most common mistake new v5 apps make. The classic App-Specific Shared Secret that SK1 used for receipt validation? It does nothing for SK2. The new required credential is an In-App Purchase Key (comprising a .p8 private key file, a Key ID, and an Issuer ID).

The hard rule: When using Purchases v5.x+, transactions will fail to be recorded without the In-App Purchase Key configured. This can result in users not accessing the purchases they are entitled to. Source

Why the swap?

StoreKit 2 validates transactions via Apple's App Store Server API — a server-to-server communication channel that requires cryptographic authentication. The In-App Purchase Key is the credential that authenticates those server-to-server calls. The shared secret was only ever meaningful for receipt validation in SK1's receipt-file-based flow. Source

Step-by-step: Setting up your In-App Purchase Key

Step 1 — Generate the key in App Store Connect: 1. Log into App Store Connect 2. Navigate to Users and Access → Integrations → In-App Purchase 3. Click Generate In-App Purchase Key (or "+" next to Active) 4. Give it a name, then click Download API Key — you get one shot to download the .p8 file. Store it somewhere safe.

You can reuse the same In-App Purchase Key across all apps in the same App Store Connect account. Source

Step 2 — Upload to RevenueCat: 1. In the RevenueCat dashboard, open your App Store app from Apps & Providers → Project Settings 2. Under the In-app purchase key configuration tab, upload the .p8 file 3. Paste the Issuer ID from App Store Connect (same page as above — if it's missing, create any App Store Connect API key to trigger it appearing) 4. Click Save Changes

Once saved, RevenueCat will validate the credentials. Look for the "Valid credentials" message with all permissions checked before proceeding. Source


3. Trusted Entitlements: Cryptographic Receipt Verification, Enabled by Default

Here's a security capability that ships silently in v5 — you likely have it active right now without realizing it.

Trusted Entitlements is RevenueCat's defense against man-in-the-middle (MiTM) attacks. Even with strong SSL, a user who controls their device can intercept the network response from RevenueCat and inject fake entitlements to grant themselves premium access without paying. Trusted Entitlements counters this by attaching a cryptographic signature (JWS) to entitlement data. The SDK verifies this signature locally before surfacing CustomerInfo to your app. Source

What you get in v5 by default

As of iOS SDK v5, Trusted Entitlements is enabled in Informational mode by default. This means: - The SDK provides verification data on every CustomerInfo response - Verification errors are logged automatically - Your app code must check the result — the SDK does not automatically block access Source

The three VerificationResult cases

Every EntitlementInfo object carries a verificationResult. There are three possible values:

Case Meaning
.verified Signature checked and valid. This customer legitimately has this entitlement.
.notRequested Trusted Entitlements was not enabled (or cached data from before it was enabled). No signature available.
.failed Signature check failed. The response may have been tampered with.

Here's how you check and respond to verification in Swift:

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

for (_, entitlement) in entitlements {
    switch entitlement.verification {
    case .verified:
        // Trust this entitlement — grant access
        grantPremiumAccess()
    case .notRequested:
        // No signature available — decide based on your risk tolerance
        // (happens with cached data or if verification was previously disabled)
        grantPremiumAccess() // or restrict, your call
    case .failed:
        // 🚨 Potential tampering detected — log it and restrict access
        logger.warning("Entitlement verification failed for: \(entitlement.identifier)")
        denyPremiumAccess()
    @unknown default:
        break
    }
}

What to do on .failed

The RevenueCat docs are clear: in Informational mode (the v5 default), a .failed result will not automatically block access — your app remains fully functional, and it's up to you to act on the result. If you want the SDK to enforce verification and automatically block unverified entitlements, you must explicitly enable Enforced mode. The practical recommendation: at minimum, log .failed events to your analytics pipeline. At maximum, use Enforced mode for high-value entitlements. Source

// To DISABLE Trusted Entitlements entirely (not recommended for new apps):
let configuration = Configuration.Builder(withAPIKey: "YOUR_API_KEY")
    .with(entitlementVerificationMode: .disabled)
    .build()
Purchases.configure(with: configuration)

// Default in v5 — informational mode (no code needed, this is what you already have):
// .with(entitlementVerificationMode: .informational)

Cache note: If you're transitioning an existing app from .disabled to .informational, cached CustomerInfo will have a verification result of .notRequested until the cache is refreshed. Call Purchases.shared.invalidateCustomerInfoCache() if you need immediate coverage. Source


4. Win-Back Offers (iOS 18 + SDK v5): Two Redemption Paths

Apple introduced win-back offers in iOS 18 — custom pricing and trial periods targeted at lapsed subscribers who have canceled. RevenueCat SDK v5 supports them, but they require SK2 (obviously) and your In-App Purchase Key must already be uploaded to work. Source

The two redemption paths

Path 1 — StoreKit Message in the App Delegate (in-app notification)

On iOS 18+, StoreKit will proactively send your app a StoreKit.Message when a subscriber is eligible to redeem a win-back offer. You handle this in your app delegate or scene delegate by listening for pending StoreKit messages and presenting the redemption UI.

This path requires iOS SDK v5 and gives you control over when and how the offer sheet is presented inside your app. It's best suited when you want to intercept the moment a user is offered the win-back and potentially track the conversion.

// In your AppDelegate or SceneDelegate (iOS 18+)
import StoreKit
import RevenueCat

// Listen for StoreKit messages — call this when your app becomes active
func applicationDidBecomeActive(_ application: UIApplication) {
    Task {
        for await message in StoreKit.Message.messages {
            // Let RevenueCat handle win-back offer messages
            // The SDK will present the appropriate UI
            if case .winBackOffer = message.reason {
                try? await message.display(in: windowScene)
            }
        }
    }
}

Path 2 — Embedding a StoreKit StoreView

Alternatively, you can embed Apple's native StoreView in SwiftUI, which handles win-back offer display automatically when the user is eligible. This is the lower-lift integration — no message listening required. Source

import SwiftUI
import StoreKit

// In your paywall or subscription view (iOS 17+)
struct WinBackPaywallView: View {
    let productIDs: [String]

    var body: some View {
        StoreView(ids: productIDs)
            // StoreView automatically handles win-back offer eligibility
            // and applies the correct offer pricing for lapsed subscribers
    }
}

Key requirement for both paths: The In-App Purchase Key must be uploaded to your RevenueCat dashboard. Win-back offers are a type of subscription offer, and all subscription offers (Promotional Offers, Offer Codes, Win-Back Offers) require the In-App Purchase Key for RevenueCat to authenticate with Apple. Source

Also note: win-back offers can also be redeemed directly in the App Store via Streamlined Purchasing — a flow Apple manages completely, where the subscriber completes the entire redemption without ever entering your app. RevenueCat will record this transaction normally once the purchase is processed. Source


5. The macOS / Mac Catalyst Gotcha: Purchases Aren't Detected Until Foreground

If you're building a Mac Catalyst app (or a native macOS app) and you're using purchasesAreCompletedBy = .myApp with SK2, there is a critical behavioral difference you must know:

By default, when purchases are completed by your app using StoreKit 2 on macOS, the SDK does not detect a user's purchase until after the user foregrounds the app after the purchase has been made. Source

This means a user could complete a purchase, switch away from the app, and return to find their entitlements haven't updated yet. For a subscription app, this is a material UX problem.

The fix: Call Purchases.shared.recordPurchase(_:) explicitly after each new purchase in your own SK2 code. This tells RevenueCat about the transaction immediately, without waiting for an app foreground event.

import StoreKit
import RevenueCat

// When your app's own SK2 code completes a purchase:
func handlePurchaseResult(_ result: Product.PurchaseResult) async throws {
    switch result {
    case .success(let verificationResult):
        // Record the purchase immediately so RevenueCat detects it on macOS
        let transaction = try verificationResult.payloadValue
        await Purchases.shared.recordPurchase(verificationResult)
        await transaction.finish()
    case .userCancelled:
        break
    case .pending:
        break
    @unknown default:
        break
    }
}

This only applies when purchasesAreCompletedBy is set to .myApp. If you're using .revenueCat (the default), RevenueCat handles transaction detection for you and this issue does not apply. Source


6. purchasesAreCompletedBy: The Decision You Need to Make at Configuration Time

RevenueCat v5 replaces the old "Observer Mode" concept with a cleaner API: purchasesAreCompletedBy. Understanding it correctly determines whether you need to write any extra purchase-handling code at all.

The decision guide

Use .revenueCat (the default) if: - You don't have your own StoreKit implementation - You're building a new app from scratch - RevenueCat is your IAP layer

Use .myApp (with explicit storeKitVersion) if: - You already have your own SK2 or SK1 implementation and want to add RevenueCat for analytics/entitlements only - You're integrating RevenueCat into an existing app that already processes purchases

When using .myApp, you must specify the storeKitVersion matching your implementation. This is not optional — the SDK needs to know which StoreKit API your app is using to correctly observe transactions. Source

import RevenueCat

// Option A: Default — RevenueCat handles everything (recommended for new apps)
Purchases.configure(withAPIKey: "YOUR_PUBLIC_SDK_KEY")

// Option B: Your app completes purchases using StoreKit 2
let configuration = Configuration.Builder(withAPIKey: "YOUR_PUBLIC_SDK_KEY")
    .with(purchasesAreCompletedBy: .myApp, storeKitVersion: .storeKit2)
    .build()
Purchases.configure(with: configuration)

// Option C: Your app completes purchases using StoreKit 1 (legacy)
let legacyConfiguration = Configuration.Builder(withAPIKey: "YOUR_PUBLIC_SDK_KEY")
    .with(purchasesAreCompletedBy: .myApp, storeKitVersion: .storeKit1)
    .build()
Purchases.configure(with: legacyConfiguration)

Note: If you've used .myApp, historical subscriptions won't automatically appear in RevenueCat. Call Purchases.shared.syncPurchases() after login to backfill them — but not on every launch, as this adds latency. Source


Putting It All Together: The v5 Startup Checklist

Before you ship a new iOS subscription app using RevenueCat v5 in 2025, verify:

  • [ ] In-App Purchase Key is uploaded to RevenueCat dashboard (not the Shared Secret — that's SK1 only)
  • [ ] RevenueCat dashboard shows "Valid credentials" for the key
  • [ ] You have NOT used .with(usesStoreKit2IfAvailable: true) — it's been removed and will cause a compile error
  • [ ] You're checking verificationResult on active entitlements — Trusted Entitlements is on, but .failed won't auto-block in Informational mode
  • [ ] If building for Mac Catalyst with your own SK2 code, you're calling recordPurchase() after each transaction
  • [ ] purchasesAreCompletedBy is set to .myApp (with storeKitVersion) only if you have your own IAP implementation
  • [ ] Win-back offer handling (Path 1 or Path 2) is wired up if you're targeting iOS 18

Sources