If you're starting a new iOS or macOS subscription app today, you face a deceptively tricky setup question: Apple's legacy receipt-based StoreKit 1 APIs are being progressively deprecated, StoreKit 2 is the present and future, and the RevenueCat SDK has evolved alongside it. Getting the setup wrong — a missing credential here, a skipped verification step there — means purchases fail silently, security gaps go undetected, or features like win-back offers simply don't work.
This guide covers everything you need to build correctly from scratch with RevenueCat SDK v5 and StoreKit 2 as the default, including the security features most developers overlook.
1. RevenueCat v5 Is SK2-Native by Default
The most important thing to understand: RevenueCat SDK v5.0+ enables the full StoreKit 2 flow on both the SDK and the backend by default. You don't opt into SK2 — you'd have to explicitly opt out.
The older .with(usesStoreKit2IfAvailable: true) configuration option has been completely removed from v5. Remove it if you're carrying it forward from older code. Source
What this means practically: - No more "Missing receipt" sandbox errors that blocked trial eligibility checks in SK1. - Faster validation: Receipt validation runs ~200ms faster at p95 vs. SK1 in RevenueCat's internal benchmarks. - Future-proof: Apple is progressively deprecating SK1 APIs, and all new Apple features (win-back offers, etc.) are SK2-only. - Automatic fallback: The SDK will automatically fall back to StoreKit 1 on older OS versions — macOS 12 or earlier, iOS 15 or earlier, iPadOS 15 or earlier, tvOS 15 or earlier, or watchOS 8 or earlier. Source
⚠️ Third-party analytics SDKs: Most popular analytics SDKs (Facebook, Mixpanel, OneSignal, Segment, Firebase) do not fully support logging purchases made via StoreKit 2. Use RevenueCat's native data integrations instead of relying on these SDKs to auto-track IAP events. Source
Minimum Deployment Targets for v5
SDK v5 raises the minimum targets: - iOS 13.0 - tvOS 13.0 - watchOS 6.2 - macOS 10.15
2. In-App Purchase Key: The SK2 Credential (Not the Shared Secret)
This is the #1 setup mistake that breaks new SK2 apps: developers configure the legacy App-Specific Shared Secret instead of the In-App Purchase Key.
The Shared Secret was mandatory for StoreKit 1 receipt validation. With SK2, Apple no longer uses receipts — it uses signed JWS transactions validated via the App Store Server API. RevenueCat needs a different credential to call that API on your behalf.
⚠️ Critical: When using Purchases v5.x+, transactions will fail to be recorded without the In-App Purchase Key configured. This means users won't get access to purchases they paid for. Source
Credential Comparison
| StoreKit Version | Required Credential | Use Case |
|---|---|---|
| StoreKit 1 (legacy) | App-Specific Shared Secret | Legacy apps, iOS 15 or earlier targets |
| StoreKit 2 (v5+) | In-App Purchase Key | Modern apps, iOS 15+, RevenueCat v5+ |
How to Set It Up (3 Steps)
Step 1 — Generate the key in App Store Connect:
Go to App Store Connect → Users and Access → Integrations → In-App Purchase. Click Generate In-App Purchase Key. Download the .p8 file immediately — you only get one chance. One key works for all apps under the same App Store Connect account. Source
Step 2 — Upload to RevenueCat:
In the RevenueCat dashboard, navigate to Project Settings → Apps & Providers → [Your App] → In-App Purchase Key Configuration. Upload your .p8 file.
Step 3 — Add the Issuer ID:
Find the Issuer ID on the same App Store Connect page (Users and Access → Integrations → In-App Purchase). If you don't see it, generate any App Store Connect API key first — this will expose the shared Issuer ID. Paste it into the RevenueCat field and save. Source
RevenueCat will validate the credentials and show a "Valid credentials" badge once everything checks out. If you see an "Invalid permissions" error, confirm the key is still listed as Active in App Store Connect — a revoked key is the most common cause.
3. Trusted Entitlements: JWS Verification Built In
Even with HTTPS, a determined attacker who controls the device can run a man-in-the-middle (MiTM) attack to intercept RevenueCat's response and inject fake entitlements — granting themselves premium access without paying. Source
Trusted Entitlements is RevenueCat's defense against this. When enabled, the SDK and backend cryptographically sign the entitlement response. Your app can then verify that signature before acting on it.
What "JWS Signature Verification" Actually Means
JWS (JSON Web Signature) is a compact, URL-safe format for representing signed data. RevenueCat's backend signs the CustomerInfo payload using intermediate keys (which are themselves signed by a root key and rotated frequently). The SDK verifies this signature on-device before handing you the data. If the signature doesn't match — because someone tampered with the response — the verificationResult field tells you.
Default Behavior in SDK v5
Starting with iOS SDK 5.15.0+, Trusted Entitlements is enabled by default in Informational mode. This means: - The SDK automatically provides verification data. - Verification results are informational only — your app must check the result and decide what to do. - Unverified entitlements are not automatically blocked. RevenueCat provides the signal; enforcement is your responsibility.
Reading the Verification Result
EntitlementInfo exposes a verificationResult property. You should check it whenever you gate access to premium features:
import RevenueCat
func checkPremiumAccess() async {
do {
let customerInfo = try await Purchases.shared.customerInfo()
guard let entitlement = customerInfo.entitlements["premium"],
entitlement.isActive else {
// No active entitlement
showPaywall()
return
}
// Check the verification result before granting access
switch entitlement.verification {
case .verified:
// Signature verified — safe to grant access
grantPremiumAccess()
case .notRequested:
// Trusted Entitlements not enabled, or cached data pre-dates it
// Grant access but consider enabling verification
grantPremiumAccess()
case .failed:
// Signature check failed — possible tampering
// Do NOT grant access; log for investigation
denyAccess(reason: "Entitlement verification failed")
reportSuspiciousActivity()
case .verifiedOnDevice:
// Verified using on-device StoreKit transaction data
grantPremiumAccess()
}
} catch {
print("Failed to fetch CustomerInfo: \(error)")
}
}
Disabling Trusted Entitlements (If Needed)
For SDK v5.15.0+, Trusted Entitlements is on by default. To explicitly disable it:
Purchases.configure(
with: Configuration.Builder(withAPIKey: "your_api_key")
.with(entitlementVerificationMode: .disabled)
.build()
)
Cache note: If you transition from
.disabledto.informational, cachedCustomerInfowon't be verified until the next network refresh. To avoid stale unverified data, callPurchases.shared.invalidateCustomerInfoCache()after enabling verification. Source
4. Win-Back Offers (iOS 18+)
Win-back offers let you present special pricing to lapsed subscribers — users who previously subscribed but have since churned. This is an iOS 18+ feature, requires SK2, and requires your In-App Purchase Key to be configured (see section 2). Source
Setting Up Win-Back Offers in App Store Connect
Win-back offers are configured as subscription pricing options inside App Store Connect, similar to promotional offers. Navigate to your subscription product, add a pricing option, and select Win-Back Offer. Apple will automatically determine eligibility — the offer only surfaces for users with a qualifying lapse history.
Two Redemption Paths
RevenueCat v5 supports two ways for lapsed users to redeem win-back offers:
Path 1: StoreKit Message (Automatic)
When the app is foregrounded, iOS 18 sends a StoreKit message if the user is eligible for a win-back offer. RevenueCat's SDK automatically listens for this message and presents Apple's win-back offer sheet.
// No extra code needed — RevenueCat handles this automatically in SDK v5.
// The win-back sheet will present itself when iOS sends the StoreKit message.
// Make sure you configure Purchases before the app finishes launching.
@main
struct MyApp: App {
init() {
Purchases.configure(withAPIKey: "your_api_key")
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
This path requires iOS SDK version 5+. Source
Path 2: StoreKit StoreView (Manual)
You can also proactively surface win-back offers in your own paywall UI using a SwiftUI StoreView. This path is useful when you want to show a targeted win-back screen to users you've identified as lapsed through your own logic.
import SwiftUI
import StoreKit
import RevenueCat
struct WinBackPaywallView: View {
// Fetch your subscription product IDs from RevenueCat offerings
let productIDs: [String] = ["com.yourapp.premium.monthly"]
var body: some View {
StoreView(ids: productIDs)
.productViewStyle(.large)
.onInAppPurchaseCompletion { product, result in
// RevenueCat will automatically detect the completed purchase
// via its transaction listener and update CustomerInfo
switch result {
case .success:
print("Win-back offer redeemed for \(product.displayName)")
case .failure(let error):
print("Purchase failed: \(error)")
}
}
}
}
This path also requires iOS SDK version 5+. Source
Note: Win-back offers are exclusively available to users on iOS 18.0+ and are only presented to users Apple determines are eligible (lapsed subscribers). They are not shown to current subscribers or users who have never subscribed. Source
5. macOS Purchase Detection: A Behavior Difference to Know
If you're building a macOS app (or a Mac Catalyst app), there's one important SK2 behavior difference from iOS:
By default, when purchases are completed by your app using StoreKit 2 on macOS, the SDK does not detect the purchase until after the user brings the app back to the foreground.
On iOS, the SK2 transaction listener fires immediately. On macOS, if a purchase is completed while the app is in the background (e.g., via the system purchase sheet that briefly moves focus away), the SDK may not process it until the next foreground event.
Fix: Call recordPurchase Immediately
If you're handling purchases yourself on macOS (using purchasesAreCompletedBy: .myApp, see section 6) and need RevenueCat to detect the purchase immediately, call Purchases.shared.recordPurchase(_:) right after the purchase completes:
import StoreKit
import RevenueCat
func purchase(product: Product) async throws {
let purchaseResult = try await product.purchase()
switch purchaseResult {
case .success(let verificationResult):
// Immediately inform RevenueCat on macOS
// so entitlements are updated without waiting for foreground
let transaction = try verificationResult.payloadValue
await Purchases.shared.recordPurchase(purchaseResult)
await transaction.finish()
case .userCancelled:
break
case .pending:
break
@unknown default:
break
}
}
6. purchasesAreCompletedBy: When RevenueCat Is Not Finishing Transactions
By default, RevenueCat finishes transactions automatically after validating them. This is the right behavior for most apps.
But some apps — particularly those wrapping an existing SK2 implementation, or cross-platform apps with custom purchase flows — need to finish transactions themselves. This pattern was previously called "Observer Mode" in SDK v4. In v5, it's been renamed to purchasesAreCompletedBy to be clearer about what's actually happening. Source
When to Use This
Use purchasesAreCompletedBy: .myApp when:
- You have existing StoreKit purchase code you're not yet ready to replace.
- You're building a hybrid app where another SDK or library makes the actual purchase calls.
- You want RevenueCat to observe and track purchases without taking over the transaction lifecycle.
Configuration
When setting purchasesAreCompletedBy: .myApp, you must also specify which version of StoreKit your app is using so RevenueCat knows how to observe transactions:
import RevenueCat
// Your app handles finishing transactions; RevenueCat observes them.
// Tell RevenueCat which StoreKit version your code uses.
Purchases.configure(
with: Configuration.Builder(withAPIKey: "your_api_key")
.with(purchasesAreCompletedBy: .myApp, storeKitVersion: .storeKit2)
.build()
)
If you're using StoreKit 1 for your own purchase flow:
Purchases.configure(
with: Configuration.Builder(withAPIKey: "your_api_key")
.with(purchasesAreCompletedBy: .myApp, storeKitVersion: .storeKit1)
.build()
)
Syncing Historical Purchases
When using this mode, RevenueCat won't have a record of past purchases until you sync them. Call syncPurchases() after login (not on every launch) to backfill:
// Sync past purchases once after user logs in
try await Purchases.shared.syncPurchases()
Putting It All Together: Complete Setup Checklist
Here's the full initialization for a new iOS/macOS app using all the capabilities above:
import RevenueCat
@main
struct MyApp: App {
init() {
// Standard RevenueCat v5 setup — SK2 is the default.
// Trusted Entitlements (informational mode) is also on by default.
Purchases.configure(withAPIKey: "your_revenuecat_api_key")
// Optional: Enable verbose logging during development
Purchases.logLevel = .debug
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Before shipping, verify:
- [ ] In-App Purchase Key uploaded to RevenueCat dashboard (required for v5/SK2)
- [ ] Issuer ID entered alongside the key
- [ ] RevenueCat dashboard shows "Valid credentials" badge
- [ ] Your app checks
entitlement.verificationbefore granting premium access - [ ] Third-party analytics SDKs replaced with RevenueCat integrations for IAP tracking
- [ ] If building for macOS with custom purchase flow:
recordPurchase()called immediately post-purchase - [ ] If using hybrid/custom IAP:
purchasesAreCompletedBy: .myAppconfigured with correctstoreKitVersion
Sources
- iOS Native 4.x to 5.x Migration Guide — SK2 default behavior, PurchasesAreCompletedBy, Trusted Entitlements in v5, macOS detection behavior, minimum deployment targets
- In-App Purchase Key Configuration — Step-by-step key setup, why it's required for v5, troubleshooting
- App Store Connect App-Specific Shared Secret — SK1 vs SK2 credential comparison
- Trusted Entitlements — JWS verification, VerificationResult values, default behavior by SDK version, cache edge cases
- iOS Subscription Offers — Win-back offers setup, StoreKit message path, StoreView path, eligibility requirements
- Using the SDK with your own IAP Code (formerly Observer Mode) — purchasesAreCompletedBy configuration, syncPurchases guidance
- Getting Subscription Status — CustomerInfo, EntitlementInfo reference