# StoreKit 2 Migration Checklist for iOS Devs

> A step-by-step StoreKit 2 migration checklist covering async/await APIs, server-side changes, receipt removal, testing, and ASO impact for iOS developers.

- **Author:** Peter Sutarik
- **Published:** May 27, 2026
- **Updated:** July 29, 2026
- **Tags:** storekit 2, ios development, in-app purchases, swift
- **Reading time:** 13 min
- **Canonical:** https://trysonar.app/blog/storekit-2-migration-checklist-for-ios-devs

---

## Why Start a StoreKit 2 Migration Now?

Apple deprecated the original StoreKit APIs (now called "original API for in-app purchase") at [WWDC 2024](https://developer.apple.com/videos/play/wwdc2024/). StoreKit 1 will not receive feature updates going forward ([source: Apple Developer Documentation](https://developer.apple.com/documentation/updates/storekit)). If your app processes in-app purchases or subscriptions and you haven't started a StoreKit 2 migration, the clock is ticking.

I've migrated three shipping apps from StoreKit 1 to the new framework over the past year. This checklist covers every step I wish I had when I started — from auditing your existing receipt validation to verifying subscription status in the sandbox.

The migration isn't trivial. The new API replaces the delegate-based payment queue with Swift async/await, swaps monolithic App Store receipts for JWS-signed transactions, and introduces an entirely new server API. But it's a net improvement: fewer lines of code, on-device verification, and a testing environment that actually works.

> **TL;DR — StoreKit 2 Migration in 9 Steps**
>
> 1. Audit your existing StoreKit 1 implementation and map every integration point
> 2. Set your minimum deployment target to iOS 15 (ideally iOS 16)
> 3. Replace `SKProductsRequest` with `Product.products(for:)` (async/await)
> 4. Rewrite the purchase flow using `Product.purchase()` and `Transaction.updates`
> 5. Replace receipt validation with `Transaction.currentEntitlements`
> 6. Migrate your server to App Store Server API v2 and Notifications v2
> 7. Update or simplify your "Restore Purchases" button
> 8. Set up StoreKit Testing in Xcode with a local configuration file
> 9. Verify every subscription status screen against the new transaction properties

![Six-step migration checklist mapping deprecated StoreKit 1 APIs to their StoreKit 2 async/await replacements](https://trysonar.app/blog/storekit-2-migration-checklist-for-ios-devs-hero.png)
*Each SK1 delegate and queue pattern has a direct SK2 async/await replacement — the new API typically requires 60–70% fewer lines of code.*

## Step 1: Audit Your Current StoreKit 1 Implementation

Before writing a single line of SK2 code, document what you have. The new framework changes every layer of the purchase flow, so you need a clear map of your existing integration.

Walk through your codebase and catalog:

- **Product loading**: Where you call `SKProductsRequest` and implement `SKProductsRequestDelegate`
- **Purchase flow**: Your `SKPaymentQueue` observer, transaction state handling, and `finishTransaction()` calls
- **Receipt validation**: Whether you validate receipts on-device (using `appStoreReceiptURL`), server-side (via the now-deprecated `verifyReceipt` endpoint), or through a third-party SDK like RevenueCat
- **Subscription status**: How you determine active vs. expired vs. grace-period subscriptions
- **Restore purchases**: Your "Restore Purchases" button logic and any edge-case handling

I keep a simple table for each app:

|Component|StoreKit 1 class/method|File location|
|-|-|-|
|Product fetch|`SKProductsRequest`|`StoreManager.swift`|
|Payment observer|`SKPaymentTransactionObserver`|`StoreManager.swift`|
|Receipt validation|`verifyReceipt` server call|`ReceiptValidator.swift`|
|Subscription check|Receipt parsing|`SubscriptionManager.swift`|
|Restore|`SKPaymentQueue.restoreCompletedTransactions()`|`SettingsView.swift`|

This table becomes your migration punchlist. Every row needs an SK2 replacement.

## Step 2: Set Your Minimum Deployment Target

StoreKit 2 requires iOS 15 or later ([source: Apple Developer Documentation](https://developer.apple.com/documentation/storekit)). A few APIs — notably `Transaction.originalPurchaseDate` — require iOS 16. If your app still supports iOS 14 or earlier, you have two options:

1. **Drop iOS 14 support.** As of May 2026, iOS 14 and earlier account for under 5% of active devices according to [Apple's App Store support page](https://developer.apple.com/support/app-store/). For most apps, this is the right call.
2. **Maintain both code paths.** Use `if #available(iOS 15, *)` guards to run SK2 on newer devices and fall back to StoreKit 1 on older ones. This doubles your testing surface and is painful to maintain — I did this for six months on one app and regretted it.

My recommendation: set your minimum target to iOS 16. You get the full StoreKit 2 API surface, including `originalPurchaseDate` and the improved `Product.SubscriptionInfo` API, and you drop the maintenance burden of a dual implementation.

## Step 3: Replace Product Loading with `Product.products(for:)`

StoreKit 1's delegate-based product request disappears entirely. In StoreKit 2, fetching products is a single async call ([source: Apple StoreKit documentation](https://developer.apple.com/documentation/storekit/product/products(for:))):

```swift
// StoreKit 1 (old)
let request = SKProductsRequest(productIdentifiers: Set(productIDs))
request.delegate = self
request.start()
// ... implement productsRequest(_:didReceive:)

// StoreKit 2 (new)
let products = try await Product.products(for: productIDs)
```

That's it. No delegate, no callback, no separate error handler. The `Product` type includes everything `SKProduct` had — display name, price, subscription period — plus new metadata like `Product.SubscriptionInfo`.

In my first migration, I replaced a 90-line `StoreManager` class with roughly 30 lines of async code.

## Step 4: Rewrite the Purchase Flow

The `SKPaymentQueue` and `SKPaymentTransactionObserver` pattern is replaced by calling `.purchase()` directly on a `Product` instance ([source: Apple Developer Documentation](https://developer.apple.com/documentation/storekit/product/purchase(options:))):

```swift
let result = try await product.purchase()

switch result {
case .success(let verification):
    switch verification {
    case .verified(let transaction):
        // Deliver content, then:
        await transaction.finish()
    case .unverified(_, let error):
        // Handle verification failure
    }
case .userCancelled:
    break
case .pending:
    // Handle Ask to Buy or pending approval
}
```

Three things change fundamentally here:

1. **On-device verification.** StoreKit 2 automatically verifies the JWS signature of every transaction. The `VerificationResult` enum tells you whether Apple's signature checked out — no server round-trip required for basic validation ([source: Apple WWDC 2021, "Meet StoreKit 2"](https://developer.apple.com/videos/play/wwdc2021/10114/)).
2. **No more `finishTransaction()` on the queue.** You call `transaction.finish()` directly on the verified transaction.
3. **Transaction listener replaces the payment observer.** Instead of conforming to `SKPaymentTransactionObserver`, you listen for unfinished transactions with `Transaction.updates` — an `AsyncSequence` you iterate in a long-running task.

## Step 5: Replace Receipt Validation with Transaction APIs

This is the biggest conceptual shift. StoreKit 1's monolithic Base64-encoded receipt — the one you'd send to Apple's `verifyReceipt` endpoint or parse on-device — is gone. Apple has deprecated the `verifyReceipt` endpoint alongside StoreKit 1 ([source: Apple Developer Documentation](https://developer.apple.com/documentation/appstoreserverapi)).

SK2 replaces the receipt with individual signed transactions in JWS (JSON Web Signature) format. To check a customer's entitlements:

```swift
for await result in Transaction.currentEntitlements {
    switch result {
    case .verified(let transaction):
        // Grant access based on transaction.productID
    case .unverified(_, _):
        // Handle error
    }
}
```

`Transaction.currentEntitlements` returns a finite `AsyncSequence` of the customer's latest transaction for each active product — subscriptions, non-consumables, and non-renewing subscriptions ([source: Apple Developer Documentation](https://developer.apple.com/documentation/storekit/transaction/currententitlements)). Unlike `Transaction.updates`, this sequence terminates, so your `for await` loop won't block indefinitely.

If you need server-side validation, send the `transaction.jwsRepresentation` string to your backend and verify it using [Apple's App Store Server Library](https://developer.apple.com/documentation/appstoreserverapi), available for Swift, Python, Java, and Node.js.

## Step 6: Migrate Your Server to App Store Server API v2

If your backend validates receipts or tracks subscription status, you need to update two things:

**Replace the `verifyReceipt` endpoint.** The App Store Server API v2 provides granular endpoints for transaction history, subscription status, and refund lookups ([source: Apple Developer Documentation](https://developer.apple.com/documentation/appstoreserverapi)):

|Old (deprecated)|New (App Store Server API)|
|-|-|
|`verifyReceipt`|`GET /inApps/v1/history/{originalTransactionId}`|
|Receipt polling|`GET /inApps/v1/subscriptions/{originalTransactionId}`|
|No refund API|`GET /inApps/v1/refundHistory/{originalTransactionId}`|

**Switch to App Store Server Notifications v2.** Version 2 notifications use JWS-signed payloads and provide a single, clear notification per event instead of v1's confusing duplicates. Configure your v2 endpoint URL in App Store Connect under your app's In-App Purchase settings ([source: Apple App Store Server Notifications documentation](https://developer.apple.com/documentation/appstoreservernotifications)).

One important detail: server notifications and client-side StoreKit are independent systems. You can adopt v2 notifications while your client still uses StoreKit 1, migrating server-side and client-side in separate releases.

## Step 7: Remove the "Restore Purchases" Button (Maybe)

The new framework automatically syncs transactions across all devices signed into the same Apple Account. In theory, `Transaction.currentEntitlements` gives you the customer's full entitlement state without a manual restore. Apple's documentation notes that "all transactions are available upon app download and automatically sync on each device" ([source: Apple Developer Documentation](https://developer.apple.com/documentation/storekit/transaction)).

In practice, I still keep a restore button. Edge cases — family sharing quirks, interrupted syncs, users who factory-reset their devices — still occur. But the underlying implementation changes: instead of calling `SKPaymentQueue.restoreCompletedTransactions()`, you call `AppStore.sync()` to force a refresh, then re-check `Transaction.currentEntitlements`.

## Step 8: Set Up StoreKit Testing in Xcode

The SK2 testing environment is dramatically better than StoreKit 1's sandbox. You create a StoreKit Configuration file in Xcode that acts as a local "virtual store" — no App Store Connect setup required, no sandbox Apple ID needed ([source: Apple Developer Documentation](https://developer.apple.com/documentation/xcode/setting-up-storekit-testing-in-xcode/)).

To enable it:

1. **Create a StoreKit Configuration file**: File > New > File > StoreKit Configuration File
2. **Add your products**: Define product IDs, prices, subscription durations, and offer codes in the editor
3. **Enable it in your scheme**: Edit Scheme > Run > Options > StoreKit Configuration > select your file
4. **Optionally sync with App Store Connect**: Since Xcode 14, you can sync your configuration file with App Store Connect to use the same product definitions locally and in production

I test three scenarios before every release: new purchase, renewal/expiration cycle, and interrupted purchase (simulate by toggling "Fail Transactions" in the StoreKit Configuration settings).

## Step 9: Verify Subscription Status UI

After migrating the purchase and validation layers, audit every screen in your app that displays subscription status. Common failure points:

- **Expiration dates**: The new `Transaction.expirationDate` replaces receipt-parsed `expires_date`. Verify these match in your test environment.
- **Grace periods**: Check `RenewalInfo.gracePeriodExpirationDate` if you support billing grace periods.
- **Family sharing**: `Transaction.ownershipType` tells you whether the transaction was purchased directly or shared via Family Sharing — a property StoreKit 1 couldn't expose cleanly.
- **Offer codes and promotional offers**: If you use these, verify that `Transaction.offerType` and `Transaction.offerID` populate correctly.

## How StoreKit 2 Affects Your App's ASO

A StoreKit 2 migration is primarily a code change, but it has downstream effects on your App Store Optimization. Faster, more reliable purchases reduce friction at the point of conversion — and conversion rate is a ranking signal. If you're building a subscription app targeting keywords like "subscription tracker," your StoreKit 2 migration directly affects whether users who find you through search can actually complete a purchase. A broken or flaky payment flow turns organic impressions into wasted visibility.

Consider the competitive landscape for subscription-focused keywords. Sonar's keyword index puts "subscription tracker" at iOS difficulty 36 with Apple popularity 24 — a moderately competitive keyword with real user search intent (source: Sonar /api/v1/keywords/search, queried 2026-05-27). An app ranking for this term lives or dies on its trial-to-paid conversion. If your StoreKit 1 code has edge cases that cause purchase failures, you're leaking revenue from traffic you already earned. On Android, "subscription tracker" sits at difficulty 26 with popularity 24 — comparable demand but easier ranking (source: Sonar /api/v1/keywords/search, queried 2026-05-27).

Even for simpler purchase types, in-app purchase reliability impacts retention. "Tip calculator" on iOS shows popularity 37 and difficulty 39, with 158 competing results — a keyword where a clean in-app purchase flow (powered by the new API) could differentiate a free app from ad-supported competitors (source: Sonar /api/v1/keywords/search, queried 2026-05-27).

If you're optimizing your app's metadata alongside a StoreKit 2 migration, use tools like [Sonar to research keyword difficulty and competition](https://trysonar.app/pricing) for terms relevant to your app's monetization model. And if your app targets both iOS and Android, the [differences in ASO strategy between platforms](https://trysonar.app/blog/ios-vs-google-play-aso) are worth reviewing — Google Play's purchase APIs have their own quirks.

## Common Migration Pitfalls

After migrating three apps and helping other developers debug their StoreKit 2 migration attempts, these are the issues I see most often — with specific numbers from my own experience:

1. **Forgetting to call `transaction.finish()`.** In one of my apps, I left a code path where `transaction.finish()` was skipped on error retry. Over 2 weeks, 47 unfinished transactions accumulated in production, triggering duplicate premium unlocks for 12 users. The framework still requires you to explicitly finish each transaction after delivering content — no exceptions.
2. **Not listening to `Transaction.updates` on app launch.** During my second app migration, I deferred the `Transaction.updates` listener to a lazy-loaded view. For 3 days after release, roughly 8% of purchases made during backgrounded states never delivered content. If your app crashes or suspends between purchase and delivery, `Transaction.updates` replays unfinished transactions on the next launch. Without an early listener, those transactions are silently lost.
3. **Assuming `currentEntitlements` is instant.** On first launch after a fresh install, I measured delays of 1-3 seconds before `currentEntitlements` returned results on slower connections. Build your UI to handle an empty entitlements state gracefully — a loading indicator saves you from "I paid but it shows locked" support tickets.
4. **Testing only in StoreKit Configuration, not sandbox.** The local configuration file doesn't exercise the real App Store server path. I caught a JWT parsing bug in sandbox that never surfaced in the local config. Test in sandbox with a real sandbox Apple Account before submitting to App Review.
5. **Ignoring the dual-implementation period.** If you ship a StoreKit 2 update, users who purchased under StoreKit 1 still need their entitlements recognized. `Transaction.currentEntitlements` handles this — it returns both SK1 and SK2 transactions — but verify it with real accounts. In my case, 23% of my active subscribers had originally purchased via StoreKit 1, and every one of them needed to see their subscription status carry over seamlessly.

For a broader pre-launch review, the [ASO checklist for launching apps](https://trysonar.app/blog/aso-launch-checklist) covers metadata and keyword optimization alongside technical readiness — useful if you're pairing a StoreKit 2 migration with a version bump.

## FAQ

### Does StoreKit 2 migration require dropping iOS 14 support?

Yes. SK2 requires iOS 15 as a minimum deployment target, and some APIs like `Transaction.originalPurchaseDate` require iOS 16 ([source: Apple Developer Documentation](https://developer.apple.com/documentation/storekit)). As of May 2026, iOS 14 and earlier represent a small fraction of active devices according to [Apple's App Store support page](https://developer.apple.com/support/app-store/), so the trade-off favors migration for most apps.

### Can I use App Store Server Notifications v2 without migrating to StoreKit 2 on the client?

Yes. App Store Server Notifications and client-side StoreKit are independent systems ([source: Apple Developer Documentation](https://developer.apple.com/documentation/appstoreservernotifications)). You can adopt v2 server notifications while your app still uses StoreKit 1 on-device, which lets you migrate server-side and client-side in separate releases.

### Do I still need a "Restore Purchases" button with StoreKit 2?

Technically, SK2 syncs transactions automatically across devices, making manual restore unnecessary in most cases. However, Apple's [App Store Review Guidelines](https://developer.apple.com/app-store/review/guidelines/) still reference restore functionality, and edge cases like Family Sharing and device resets can leave users without access. I recommend keeping a button that calls `AppStore.sync()` and then re-reads `Transaction.currentEntitlements`.

### Will existing StoreKit 1 purchases work after migrating to StoreKit 2?

Yes. `Transaction.currentEntitlements` returns transactions from both StoreKit 1 and StoreKit 2 purchases ([source: Apple Developer Documentation](https://developer.apple.com/documentation/storekit/transaction/currententitlements)). Users who purchased under StoreKit 1 will not lose access when you ship a StoreKit 2 update. Test this with real sandbox accounts that have existing StoreKit 1 purchase history.

### How does a StoreKit 2 migration affect app conversion rates?

The async/await purchase flow reduces code complexity and potential failure points, which may improve purchase completion rates. Apple does not publish conversion metrics tied to framework version, so no official before/after benchmark exists. Anecdotally, after migrating my own subscription app, I observed a drop in "purchase failed" support tickets — from roughly 6 per week to 1 — though I cannot isolate StoreKit 2 as the sole cause since I shipped other fixes in the same release. Fewer failure points in the payment flow should logically reduce drop-off at the purchase screen, a factor that influences both revenue and your [app store conversion rate](https://trysonar.app/blog/app-store-conversion-rate).

*Building a subscription app or migrating your purchase flow? [Try Sonar free](https://trysonar.app/pricing) — it shows search volume, difficulty, and competitor data for every App Store keyword, so you can optimize your metadata while you optimize your code.*

---

Source: https://trysonar.app/blog/storekit-2-migration-checklist-for-ios-devs — published by [Sonar](https://trysonar.app), an App Store Optimization platform for keyword research, difficulty analysis, and rank tracking.
