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
.disabledto.informational, cachedCustomerInfowill have a verification result of.notRequesteduntil the cache is refreshed. CallPurchases.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. CallPurchases.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
verificationResulton active entitlements — Trusted Entitlements is on, but.failedwon't auto-block in Informational mode - [ ] If building for Mac Catalyst with your own SK2 code, you're calling
recordPurchase()after each transaction - [ ]
purchasesAreCompletedByis set to.myApp(withstoreKitVersion) 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
- iOS Native 4.x to 5.x Migration Guide — SK2 as default, fallback OS versions, Trusted Entitlements default mode, macOS foreground gotcha, PurchasesAreCompletedBy
- In-App Purchase Key Configuration — Required credential for v5, step-by-step setup, SDK version matrix
- App Store Connect App-Specific Shared Secret — SK1 vs SK2 credential comparison, SK1 deprecation notice
- Trusted Entitlements — JWS verification, VerificationResult cases, Informational vs Enforced modes, cache edge case
- iOS Subscription Offers — Win-back offers iOS 18, redemption paths, In-App Purchase Key requirement
- Using the SDK with your own IAP Code (formerly Observer Mode) — purchasesAreCompletedBy, storeKitVersion requirement, syncPurchases guidance