LimitedRound 2 is open. Get your first month free, no extra charge.Join the waitlist ›
Wire in your backend

How to add in-app purchases with StoreKit 2

TL;DR. StoreKit 2 lets you load products, run a purchase, and verify the transaction with modern async Swift instead of the old delegate API. You define products in App Store Connect, load them by ID, and call purchase on a Product. AppFlight wires the StoreKit setup in and keeps your products gated correctly.

What in-app purchases do for an iOS app

In-app purchases are how you sell anything digital inside an iOS app: a one-time unlock, consumable credits, or access to content. Apple handles the payment, the buyer's payment method, taxes, and refunds. Your job is to load the products you defined in App Store Connect, run the purchase, and unlock the right thing once it succeeds. For digital goods consumed in the app, Apple requires this path under guideline 3.1.1, so Stripe and other processors are not an option there.

Adding it with AppFlight, and manually

With AppFlight you describe what you sell, for example a one-time Pro unlock, and the AI generates the StoreKit 2 code to load the product, run the purchase, and gate the feature. You still create the product records in App Store Connect yourself, since those live in Apple's system, and AppFlight references them by product ID.

Manually, you add a StoreKit configuration or create the products in App Store Connect, then write code that calls Product.products(for:) with your IDs, calls purchase() on the chosen product, and checks the VerificationResult. There is no client key to manage here. StoreKit talks to Apple directly using the signed-in Apple Account.

A SwiftUI example

Load a product, run the purchase, and verify the transaction:

import StoreKit
import SwiftUI

struct StoreView: View {
    @State private var product: Product?

    var body: some View {
        VStack {
            if let product {
                Button("Unlock Pro for \(product.displayPrice)") {
                    Task { await buy(product) }
                }
            }
        }
        .task {
            product = try? await Product.products(for: ["com.example.pro"]).first
        }
    }

    func buy(_ product: Product) async {
        guard let result = try? await product.purchase() else { return }
        if case .success(.verified(let transaction)) = result {
            // Unlock the feature, then finish the transaction.
            await transaction.finish()
        }
    }
}

Common pitfalls

Forgetting to call transaction.finish() leaves the purchase pending and can cause repeat prompts. Not listening to Transaction.updates means purchases made on another device, or Ask to Buy approvals, never unlock until the next manual load. Testing only in the simulator misses real edge cases, so use a sandbox Apple Account or a StoreKit configuration file. If you want cross-platform entitlements, server validation, and paywall analytics without building them, RevenueCat sits on top of StoreKit and handles that layer.

FAQ

What is the difference between StoreKit 1 and StoreKit 2?

StoreKit 2 is the modern API introduced at WWDC21. It uses async/await and signed transactions you can verify on device, so you write far less receipt-parsing code than the original delegate-based StoreKit.

Do I need a server to sell in-app purchases?

No. StoreKit 2 verifies transactions on device with VerificationResult, which is enough for many apps. A server with the App Store Server API is only needed for things like cross-platform entitlements or stronger fraud protection.

Can I use Stripe for in-app purchases instead?

No, not for digital goods consumed in the app. Apple guideline 3.1.1 requires StoreKit in-app purchase for digital content. Stripe is for physical goods and services delivered outside the app.

Sources

Build this app without opening Xcode.

AppFlight turns a plain-English prompt into a real native iOS app and ships it to the App Store. Round 2 is open: free for your first month.

Join the waitlist