Claude Code Plugins

Community-maintained marketplace

Feedback
56
0

Reference — Complete StoreKit 2 API guide covering Product, Transaction, AppTransaction, RenewalInfo, SubscriptionStatus, StoreKit Views, purchase options, server APIs, and all iOS 18.4 enhancements with WWDC 2025 code examples

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name storekit-ref
description Reference — Complete StoreKit 2 API guide covering Product, Transaction, AppTransaction, RenewalInfo, SubscriptionStatus, StoreKit Views, purchase options, server APIs, and all iOS 18.4 enhancements with WWDC 2025 code examples
skill_type reference
version 1.0.0
last_updated Mon Dec 08 2025 00:00:00 GMT+0000 (Coordinated Universal Time)
apple_platforms iOS 15+ (iOS 18.4+ for latest features)

StoreKit 2 — Complete API Reference

Overview

StoreKit 2 is Apple's modern in-app purchase framework with async/await APIs, automatic receipt validation, and SwiftUI integration. This reference covers every API, iOS 18.4 enhancements, and comprehensive WWDC 2025 code examples.

Product Types Supported

Consumable:

  • Products that can be purchased multiple times
  • Examples: coins, hints, temporary boosts
  • Do NOT restore on new devices

Non-Consumable:

  • Products purchased once, owned forever
  • Examples: premium features, level packs, remove ads
  • MUST restore on new devices

Auto-Renewable Subscription:

  • Subscriptions that renew automatically
  • Organized into subscription groups
  • MUST restore on new devices
  • Support: free trials, intro offers, promotional offers, win-back offers

Non-Renewing Subscription:

  • Fixed duration subscriptions (no auto-renewal)
  • Examples: seasonal passes
  • MUST restore on new devices

Key Improvements Over StoreKit 1

  • Async/Await: Modern concurrency instead of delegates/closures
  • Automatic Verification: JSON Web Signature (JWS) verification built-in
  • Transaction Types: Strong Swift types instead of SKPaymentTransaction
  • Testing: StoreKit configuration files for local testing
  • SwiftUI Views: Pre-built purchase UIs (ProductView, SubscriptionStoreView)
  • Server APIs: App Store Server API and Server Notifications

When to Use This Reference

Use this reference when:

  • Implementing in-app purchases with StoreKit 2
  • Understanding new iOS 18.4 fields (appTransactionID, offerPeriod, etc.)
  • Looking up specific API signatures and parameters
  • Planning subscription architecture
  • Debugging transaction issues
  • Implementing StoreKit Views
  • Integrating with App Store Server APIs

Related Skills:

  • in-app-purchases — Discipline skill with testing-first workflow, architecture patterns
  • (Future: iap-auditor agent for auditing existing IAP code)
  • (Future: iap-implementation agent for implementing IAP from scratch)

Product

Overview

Product represents an in-app purchase item configured in App Store Connect or StoreKit configuration file.

Loading Products

Basic Loading:

import StoreKit

let productIDs = [
    "com.app.coins_100",
    "com.app.premium",
    "com.app.pro_monthly"
]

let products = try await Product.products(for: productIDs)

From WWDC 2021-10114

Handling Missing Products:

let products = try await Product.products(for: productIDs)

// Check what loaded
let loadedIDs = Set(products.map { $0.id })
let missingIDs = Set(productIDs).subtracting(loadedIDs)

if !missingIDs.isEmpty {
    print("Missing products: \(missingIDs)")
    // Products not configured in App Store Connect or .storekit file
}

Product Properties

Basic Properties:

let product: Product

product.id // "com.app.premium"
product.displayName // "Premium Upgrade"
product.description // "Unlock all features"
product.displayPrice // "$4.99"
product.price // Decimal(4.99)
product.type // .nonConsumable

Product Type Enum:

switch product.type {
case .consumable:
    // Coins, hints, boosts
case .nonConsumable:
    // Premium features, level packs
case .autoRenewable:
    // Monthly/annual subscriptions
case .nonRenewing:
    // Seasonal passes
@unknown default:
    break
}

Subscription-Specific Properties

Check if Product is Subscription:

if let subscriptionInfo = product.subscription {
    // Product is auto-renewable subscription
    let groupID = subscriptionInfo.subscriptionGroupID
    let period = subscriptionInfo.subscriptionPeriod
}

Subscription Period:

let period = product.subscription?.subscriptionPeriod

switch period?.unit {
case .day:
    print("\(period?.value ?? 0) days")
case .week:
    print("\(period?.value ?? 0) weeks")
case .month:
    print("\(period?.value ?? 0) months")
case .year:
    print("\(period?.value ?? 0) years")
default:
    break
}

Introductory Offer:

if let introOffer = product.subscription?.introductoryOffer {
    print("Free trial: \(introOffer.period.value) \(introOffer.period.unit)")
    print("Price: \(introOffer.displayPrice)")

    switch introOffer.paymentMode {
    case .freeTrial:
        print("Free trial - no charge")
    case .payAsYouGo:
        print("Discounted price per period")
    case .payUpFront:
        print("One-time discounted price")
    @unknown default:
        break
    }
}

Promotional Offers:

let offers = product.subscription?.promotionalOffers ?? []

for offer in offers {
    print("Offer ID: \(offer.id)")
    print("Price: \(offer.displayPrice)")
    print("Period: \(offer.period.value) \(offer.period.unit)")
}

Purchase Methods

Purchase with UI Context (iOS 18.2+):

let product: Product
let scene: UIWindowScene

let result = try await product.purchase(confirmIn: scene)

From WWDC 2025-241:9:32

Purchase with Options:

let accountToken = UUID()

let result = try await product.purchase(
    confirmIn: scene,
    options: [
        .appAccountToken(accountToken)
    ]
)

From WWDC 2025-241:11:01

Purchase with Promotional Offer (JWS Format):

let jwsSignature: String // From your server

let result = try await product.purchase(
    confirmIn: scene,
    options: [
        .promotionalOffer(offerID: "promo_winback", signature: jwsSignature)
    ]
)

From WWDC 2025-241:10:55

Purchase with Custom Intro Eligibility:

let jwsSignature: String // From your server

let result = try await product.purchase(
    confirmIn: scene,
    options: [
        .introductoryOfferEligibility(signature: jwsSignature)
    ]
)

From WWDC 2025-241:10:42

SwiftUI Purchase (Using Environment):

struct ProductView: View {
    let product: Product
    @Environment(\.purchase) private var purchase

    var body: some View {
        Button("Buy \(product.displayPrice)") {
            Task {
                do {
                    let result = try await purchase(product)
                    // Handle result
                } catch {
                    print("Purchase failed: \(error)")
                }
            }
        }
    }
}

From WWDC 2025-241:9:50

PurchaseResult

Handling Purchase Results:

let result = try await product.purchase(confirmIn: scene)

switch result {
case .success(let verificationResult):
    // Purchase succeeded - verify transaction
    guard let transaction = try? verificationResult.payloadValue else {
        print("Transaction verification failed")
        return
    }

    // Grant entitlement
    await grantEntitlement(for: transaction)
    await transaction.finish()

case .userCancelled:
    // User tapped "Cancel" in payment sheet
    print("User cancelled purchase")

case .pending:
    // Purchase requires action (Ask to Buy, payment issue)
    // Transaction will arrive via Transaction.updates when approved
    print("Purchase pending approval")

@unknown default:
    break
}

From WWDC 2025-241


Transaction

Overview

Transaction represents a successful in-app purchase. Contains purchase metadata, product ID, purchase date, and for subscriptions, expiration date.

New Fields (iOS 18.4)

appTransactionID:

let transaction: Transaction
let appTransactionID = transaction.appTransactionID
// Unique ID for app download (same across all purchases by same Apple Account)

From WWDC 2025-241:4:13

offerPeriod:

if let offerPeriod = transaction.offer?.period {
    print("Offer duration: \(offerPeriod)")
    // ISO 8601 duration format (e.g., "P1M" for 1 month)
}

From WWDC 2025-249:3:11

advancedCommerceInfo:

if let advancedInfo = transaction.advancedCommerceInfo {
    // Only present for Advanced Commerce API purchases
    // nil for standard IAP
}

From WWDC 2025-241:4:42

Essential Properties

Basic Fields:

let transaction: Transaction

transaction.id // Unique transaction ID
transaction.originalID // Original transaction ID (consistent across renewals)
transaction.productID // "com.app.pro_monthly"
transaction.productType // .autoRenewable
transaction.purchaseDate // Date of purchase
transaction.appAccountToken // UUID set at purchase time (if provided)

Subscription Fields:

transaction.expirationDate // When subscription expires
transaction.isUpgraded // true if user upgraded to higher tier
transaction.revocationDate // Date of refund (nil if not refunded)
transaction.revocationReason // .developerIssue or .other

Offer Fields:

if let offer = transaction.offer {
    offer.type // .introductory or .promotional or .code
    offer.id // Offer identifier from App Store Connect
    offer.paymentMode // .freeTrial, .payAsYouGo, .payUpFront, .oneTime
}

From WWDC 2025-241:8:00

Current Entitlements

Get All Current Entitlements:

var purchasedProductIDs: Set<String> = []

for await result in Transaction.currentEntitlements {
    guard let transaction = try? result.payloadValue else {
        continue
    }

    // Only include non-refunded transactions
    if transaction.revocationDate == nil {
        purchasedProductIDs.insert(transaction.productID)
    }
}

From WWDC 2025-241

Get Entitlements for Specific Product (iOS 18.4+):

let productID = "com.app.premium"

for await result in Transaction.currentEntitlements(for: productID) {
    if let transaction = try? result.payloadValue,
       transaction.revocationDate == nil {
        // User owns this product
        return true
    }
}

From WWDC 2025-241:3:31

Deprecated API (iOS 18.4):

// ❌ Deprecated in iOS 18.4
let entitlement = await Transaction.currentEntitlement(for: productID)

// ✅ Use this instead (returns sequence, handles Family Sharing)
for await result in Transaction.currentEntitlements(for: productID) {
    // ...
}

From WWDC 2025-241:3:31

Transaction History

Get All Transactions:

for await result in Transaction.all {
    guard let transaction = try? result.payloadValue else {
        continue
    }

    print("Transaction: \(transaction.productID) on \(transaction.purchaseDate)")
}

Get Transactions for Product:

for await result in Transaction.all(matching: productID) {
    guard let transaction = try? result.payloadValue else {
        continue
    }

    // All transactions for this product
}

Transaction Listener

Listen for Real-Time Updates (REQUIRED):

func listenForTransactions() -> Task<Void, Never> {
    Task.detached {
        for await verificationResult in Transaction.updates {
            await handleTransaction(verificationResult)
        }
    }
}

func handleTransaction(_ result: VerificationResult<Transaction>) async {
    guard let transaction = try? result.payloadValue else {
        return
    }

    // Grant or revoke entitlement
    if transaction.revocationDate != nil {
        await revokeEntitlement(for: transaction.productID)
    } else {
        await grantEntitlement(for: transaction)
    }

    // CRITICAL: Always finish transaction
    await transaction.finish()
}

From WWDC 2021-10114

Transaction Sources:

  • In-app purchases
  • Purchases from App Store (promoted IAP)
  • Offer code redemptions
  • Subscription renewals
  • Family Sharing transactions
  • Pending purchases (Ask to Buy) that complete
  • Refund notifications

Verification

VerificationResult:

let result: VerificationResult<Transaction>

switch result {
case .verified(let transaction):
    // ✅ Transaction signed by App Store
    await grantEntitlement(for: transaction)
    await transaction.finish()

case .unverified(let transaction, let error):
    // ❌ Transaction signature invalid
    print("Unverified: \(error)")
    // DO NOT grant entitlement
    await transaction.finish() // Still finish to clear queue
}

What Verification Checks:

  • Transaction signed by App Store (not fraudulent)
  • Transaction belongs to this app (bundle ID match)
  • Transaction belongs to this device

Finishing Transactions

Always Call finish():

await transaction.finish()

When to finish:

  • ✅ After granting entitlement to user
  • ✅ After storing transaction receipt/ID
  • ✅ Even for unverified transactions (to clear queue)
  • ✅ Even for refunded transactions

What happens if you don't finish:

  • Transaction redelivered on next app launch
  • Transaction.updates re-emits transaction
  • Queue builds up over time

AppTransaction

Overview

AppTransaction represents the original app download. Available via AppTransaction.shared.

New Fields (iOS 18.4)

appTransactionID:

let appTransaction = try await AppTransaction.shared

switch appTransaction {
case .verified(let transaction):
    let appTransactionID = transaction.appTransactionID
    // Globally unique ID for this Apple Account + app
    // Same value appears in Transaction and RenewalInfo

case .unverified(_, let error):
    print("AppTransaction verification failed: \(error)")
}

From WWDC 2025-241:1:42

originalPlatform:

if let appTransaction = try? await AppTransaction.shared.payloadValue {
    let platform = appTransaction.originalPlatform

    switch platform {
    case .iOS:
        print("Originally downloaded on iPhone/iPad")
    case .macOS:
        print("Originally downloaded on Mac")
    case .tvOS:
        print("Originally downloaded on Apple TV")
    case .visionOS:
        print("Originally downloaded on Vision Pro")
    @unknown default:
        break
    }
}

From WWDC 2025-241:2:11

Note: Apps downloaded on watchOS show originalPlatform = .iOS

Essential Properties

let appTransaction: AppTransaction

appTransaction.appVersion // "1.2.3"
appTransaction.originalAppVersion // "1.0.0"
appTransaction.originalPurchaseDate // First download date
appTransaction.bundleID // "com.company.app"
appTransaction.deviceVerification // UUID for device
appTransaction.deviceVerificationNonce // Nonce for verification

Use Cases

Check App Version:

if let appTransaction = try? await AppTransaction.shared.payloadValue {
    if appTransaction.appVersion != currentVersion {
        // Prompt user to update
    }
}

From WWDC 2025-241:0:51

Business Model Migration:

// Moving from paid app to free app with IAP
if appTransaction.originalPlatform == .iOS,
   appTransaction.originalPurchaseDate < migrationDate {
    // User paid for app before migration - grant premium
    await grantPremiumAccess()
}

From WWDC 2025-241:2:32


Product.SubscriptionInfo.RenewalInfo

Overview

RenewalInfo provides information about auto-renewable subscription renewal state, including whether it will renew, expiration reason, and upcoming offers.

New Fields (iOS 18.4)

appTransactionID:

let renewalInfo: RenewalInfo
let appTransactionID = renewalInfo.appTransactionID

From WWDC 2025-241:6:40

offerPeriod:

if let offerPeriod = renewalInfo.offerPeriod {
    print("Next renewal offer period: \(offerPeriod)")
    // ISO 8601 duration (applies at next renewal)
}

From WWDC 2025-249:3:11

appAccountToken:

if let token = renewalInfo.appAccountToken {
    // UUID associating subscription with your server account
}

From WWDC 2025-241:6:56

advancedCommerceInfo:

if let advancedInfo = renewalInfo.advancedCommerceInfo {
    // Only for Advanced Commerce API subscriptions
}

From WWDC 2025-241:6:50

Essential Properties

Renewal State:

let renewalInfo: RenewalInfo

renewalInfo.willAutoRenew // true if subscription will renew
renewalInfo.autoRenewPreference // Product ID customer will renew to
renewalInfo.expirationReason // Why subscription expired (if expired)

Expiration Reasons:

switch renewalInfo.expirationReason {
case .autoRenewDisabled:
    // User turned off auto-renewal
case .billingError:
    // Payment method issue
case .didNotConsentToPriceIncrease:
    // User didn't accept price increase - show win-back offer!
case .productUnavailable:
    // Product no longer available
case .unknown:
    // Unknown reason
@unknown default:
    break
}

From WWDC 2025-241:5:38

Grace Period:

if let gracePeriodExpiration = renewalInfo.gracePeriodExpirationDate {
    // Subscription in grace period - billing issue
    // Show update payment method UI
}

Price Increase Consent:

if let consentStatus = renewalInfo.priceIncreaseStatus {
    switch consentStatus {
    case .agreed:
        // User accepted price increase
    case .notYetResponded:
        // User hasn't responded - show consent UI
    @unknown default:
        break
    }
}

Accessing RenewalInfo

From SubscriptionStatus:

let statuses = try await Product.SubscriptionInfo.status(for: groupID)

for status in statuses {
    switch status.renewalInfo {
    case .verified(let renewalInfo):
        print("Will renew: \(renewalInfo.willAutoRenew)")
    case .unverified(_, let error):
        print("Renewal info verification failed: \(error)")
    }
}

Product.SubscriptionInfo.Status

Overview

SubscriptionStatus represents the current state of an auto-renewable subscription, including whether it's active, expired, in grace period, or in billing retry.

Subscription States

State Enum:

let status: Product.SubscriptionInfo.Status

switch status.state {
case .subscribed:
    // User has active subscription - full access

case .expired:
    // Subscription expired - show resubscribe/win-back offer

case .inGracePeriod:
    // Billing issue but access maintained - show update payment UI

case .inBillingRetryPeriod:
    // Apple retrying payment - maintain access

case .revoked:
    // Family Sharing access removed - revoke access

@unknown default:
    break
}

From WWDC 2025-241

Getting Subscription Status

For Subscription Group:

let groupID = "pro_tier"

let statuses = try await Product.SubscriptionInfo.status(for: groupID)

// Find highest service level
let activeStatus = statuses
    .filter { $0.state == .subscribed }
    .max { $0.transaction.productID < $1.transaction.productID }

From WWDC 2025-241:6:22

For Specific Transaction (iOS 18.4+):

let transactionID = transaction.id

let status = try await Product.SubscriptionInfo.status(for: transactionID)

From WWDC 2025-241:6:40

Listen for Status Updates:

for await statuses in Product.SubscriptionInfo.Status.updates(for: groupID) {
    // Process updated statuses
    for status in statuses {
        print("Status: \(status.state)")
    }
}

Status Properties

let status: Product.SubscriptionInfo.Status

status.state // .subscribed, .expired, etc.
status.transaction // VerificationResult<Transaction>
status.renewalInfo // VerificationResult<RenewalInfo>

StoreKit Views

ProductView (iOS 17+)

Basic Usage:

import StoreKit

struct ContentView: View {
    let productID = "com.app.premium"

    var body: some View {
        ProductView(id: productID)
    }
}

From WWDC 2023-10013

With Loaded Product:

struct ContentView: View {
    let product: Product

    var body: some View {
        ProductView(for: product)
    }
}

Custom Icon:

ProductView(id: productID) {
    Image(systemName: "star.fill")
        .foregroundStyle(.yellow)
}

Control Styles:

ProductView(id: productID)
    .productViewStyle(.regular)  // Default

ProductView(id: productID)
    .productViewStyle(.compact)  // Smaller

ProductView(id: productID)
    .productViewStyle(.large)  // Prominent

StoreView (iOS 17+)

Basic Store:

struct ContentView: View {
    let productIDs = [
        "com.app.coins_100",
        "com.app.coins_500",
        "com.app.coins_1000"
    ]

    var body: some View {
        StoreView(ids: productIDs)
    }
}

From WWDC 2023-10013

With Loaded Products:

struct ContentView: View {
    let products: [Product]

    var body: some View {
        StoreView(products: products)
    }
}

SubscriptionStoreView (iOS 17+)

Basic Subscription Store:

struct SubscriptionView: View {
    let groupID = "pro_tier"

    var body: some View {
        SubscriptionStoreView(groupID: groupID) {
            // Marketing content above subscription options
            VStack {
                Image("app-icon")
                Text("Go Pro")
                    .font(.largeTitle.bold())
                Text("Unlock all features")
            }
        }
    }
}

From WWDC 2023-10013

Control Style:

SubscriptionStoreView(groupID: groupID) {
    // Marketing content
}
.subscriptionStoreControlStyle(.automatic)    // Default
.subscriptionStoreControlStyle(.picker)       // Horizontal picker
.subscriptionStoreControlStyle(.buttons)      // Stacked buttons
.subscriptionStoreControlStyle(.prominentPicker) // Large picker (iOS 18.4+)

From WWDC 2025-241

SubscriptionOfferView (iOS 18.4+)

Basic Offer View:

struct ContentView: View {
    let productID = "com.app.pro_monthly"

    var body: some View {
        SubscriptionOfferView(id: productID)
    }
}

From WWDC 2025-241:14:27

With Promotional Icon:

SubscriptionOfferView(
    id: productID,
    prefersPromotionalIcon: true
)

With Custom Icon:

SubscriptionOfferView(id: productID) {
    Image("custom-icon")
        .resizable()
        .frame(width: 60, height: 60)
} placeholder: {
    Image(systemName: "photo")
        .foregroundStyle(.gray)
}

From WWDC 2025-241:15:14

With Detail Action:

@State private var showStore = false

var body: some View {
    SubscriptionOfferView(id: productID)
        .subscriptionOfferViewDetailAction {
            showStore = true
        }
        .sheet(isPresented: $showStore) {
            SubscriptionStoreView(groupID: "pro_tier")
        }
}

From WWDC 2025-241:15:38

Visible Relationship:

// Only show if customer can upgrade
SubscriptionOfferView(
    groupID: "pro_tier",
    visibleRelationship: .upgrade
)

// Only show if customer can downgrade
SubscriptionOfferView(
    groupID: "pro_tier",
    visibleRelationship: .downgrade
)

// Show crossgrade options (same tier, different billing period)
SubscriptionOfferView(
    groupID: "pro_tier",
    visibleRelationship: .crossgrade
)

// Show current subscription (only if offer available)
SubscriptionOfferView(
    groupID: "pro_tier",
    visibleRelationship: .current
)

// Show any plan in group
SubscriptionOfferView(
    groupID: "pro_tier",
    visibleRelationship: .all
)

From WWDC 2025-241:17:44

With App Icon:

SubscriptionOfferView(
    groupID: groupID,
    visibleRelationship: .all,
    useAppIcon: true
)

From WWDC 2025-241:19:06

Offer Modifiers

Promotional Offer (JWS):

SubscriptionStoreView(groupID: groupID)
    .subscriptionPromotionalOffer(
        for: { subscription in
            // Return offer for this subscription
            return subscription.promotionalOffers.first
        },
        signature: { subscription, offer in
            // Get JWS signature from server
            let signature = try await server.signOffer(
                productID: subscription.id,
                offerID: offer.id
            )
            return signature
        }
    )

From WWDC 2025-241:12:17


Offer Codes (iOS 18.2+)

Overview

Offer codes now support all product types (previously subscription-only):

  • Consumables
  • Non-consumables
  • Non-renewing subscriptions
  • Auto-renewable subscriptions

Redeem in App

UIKit:

func showOfferCodeSheet() {
    guard let scene = view.window?.windowScene else { return }

    StoreKit.AppStore.presentOfferCodeRedeemSheet(in: scene)
}

From WWDC 2025-241:7:38

SwiftUI:

.offerCodeRedemption(isPresented: $showRedeemSheet)

Payment Mode

New: .oneTime:

let transaction: Transaction

if let offer = transaction.offer {
    switch offer.paymentMode {
    case .freeTrial:
        // No charge during offer period
    case .payAsYouGo:
        // Discounted price per billing period
    case .payUpFront:
        // One-time discounted price for entire duration
    case .oneTime:
        // ✨ New: One-time offer code redemption (iOS 17.2+)
    @unknown default:
        break
    }
}

From WWDC 2025-241:8:17

Legacy Access (iOS 15-17.1):

if let offerMode = transaction.offerPaymentModeStringRepresentation {
    // String representation for older OS versions
    print(offerMode) // "oneTime"
}

From WWDC 2025-241:8:49


App Store Server Library

Overview

Open-source library for signing IAP requests and decoding server API responses. Available in Swift, Java, Python, Node.js.

Create Promotional Offer Signature

Swift Example:

import AppStoreServerLibrary

// Configure signing
let signingKey = "YOUR_PRIVATE_KEY"
let keyID = "YOUR_KEY_ID"
let issuerID = "YOUR_ISSUER_ID"
let bundleID = "com.app.bundle"

let creator = PromotionalOfferV2SignatureCreator(
    privateKey: signingKey,
    keyID: keyID,
    issuerID: issuerID,
    bundleID: bundleID
)

// Create signature
let productID = "com.app.pro_monthly"
let offerID = "promo_winback"
let transactionID = transaction.id // Optional but recommended

let signature = try creator.createSignature(
    productIdentifier: productID,
    subscriptionOfferIdentifier: offerID,
    applicationUsername: nil,
    nonce: UUID(),
    timestamp: Date().timeIntervalSince1970,
    transactionIdentifier: transactionID
)

// Send signature to app
return signature // Compact JWS string

From WWDC 2025-241:12:44, 2025-249

Server Endpoint Example:

app.get("promo-offer") { req async throws -> String in
    let productID = try req.query.get(String.self, at: "productID")
    let offerID = try req.query.get(String.self, at: "offerID")

    let signature = try creator.createSignature(
        productIdentifier: productID,
        subscriptionOfferIdentifier: offerID,
        transactionIdentifier: nil
    )

    return signature
}

From WWDC 2025-241:12:52


App Store Server API

Set App Account Token

Endpoint:

PATCH /inApps/v1/transactions/{originalTransactionId}

Request Body:

{
  "appAccountToken": "550e8400-e29b-41d4-a716-446655440000"
}

Usage:

  • Set appAccountToken for purchases made outside your app (offer codes, App Store)
  • Update appAccountToken when account ownership changes
  • Associates transaction with customer account on your server

From WWDC 2025-249:5:19

Get App Transaction Info

Endpoint:

GET /inApps/v2/appTransaction/{transactionId}

Response:

{
  "signedAppTransactionInfo": "eyJhbGc..."
}

Usage:

  • Get app download information on server
  • Check app version, platform, environment
  • Available later in 2025

From WWDC 2025-249:10:48

Send Consumption Information V2

Endpoint:

PUT /inApps/v2/transactions/consumption/{transactionId}

Request Body:

{
  "customerConsented": true,
  "sampleContentProvided": false,
  "deliveryStatus": "DELIVERED",
  "refundPreference": "GRANT_PRORATED",
  "consumptionPercentage": 25000
}

Fields:

  • customerConsented (required): User consented to send consumption data
  • sampleContentProvided (optional): Sample provided before purchase
  • deliveryStatus (required): "DELIVERED" or various UNDELIVERED statuses
  • refundPreference (optional): "NO_REFUND", "GRANT_REFUND", "GRANT_PRORATED"
  • consumptionPercentage (optional): 0-100000 (millipercent, e.g., 25000 = 25%)

Prorated Refund:

  • New in 2025
  • Supports partial consumption (consumables, non-consumables, non-renewing)
  • For auto-renewable subscriptions, App Store calculates based on time remaining

From WWDC 2025-249:16:09

Refund Notifications

REFUND Notification:

{
  "notificationType": "REFUND",
  "data": {
    "signedTransactionInfo": "...",
    "refundPercentage": 75,
    "revocationType": "REFUND_PRORATED"
  }
}

revocationType Values:

  • REFUND_FULL: 100% refund - revoke all access
  • REFUND_PRORATED: Partial refund - revoke proportional access
  • FAMILY_REVOKE: Family Sharing removed - revoke access

From WWDC 2025-249:20:17


Edge Cases

Family Sharing

Detect Family Shared Transactions:

// appAccountToken is NOT available for family shared transactions
let transaction: Transaction

if transaction.appAccountToken == nil {
    // Might be family shared (or appAccountToken not set)
    // Check ownershipType (if available)
}

Subscription Status for Family Sharing:

// Each family member has unique appTransactionID
// Use appTransactionID to identify individual family members

From WWDC 2025-241:1:54

Refunds

Handle Refund:

func handleTransaction(_ transaction: Transaction) async {
    if let revocationDate = transaction.revocationDate {
        // Transaction was refunded
        print("Refunded on \(revocationDate)")

        switch transaction.revocationReason {
        case .developerIssue:
            // Refund due to app issue
        case .other:
            // Other refund reason
        @unknown default:
            break
        }

        // Revoke entitlement
        await revokeEntitlement(for: transaction.productID)
    }
}

Advanced Commerce API

Check if Transaction Uses Advanced Commerce:

if transaction.advancedCommerceInfo != nil {
    // Transaction from Advanced Commerce API
    // Large catalogs, creator experiences, subscriptions with add-ons
}

More Info: Visit Advanced Commerce API documentation

From WWDC 2025-241:4:51

Win-Back Offers

Show Win-Back for Expired Subscription:

let renewalInfo: RenewalInfo

if renewalInfo.expirationReason == .didNotConsentToPriceIncrease {
    // Perfect time for win-back offer!
    SubscriptionOfferView(
        groupID: groupID,
        visibleRelationship: .current
    )
    .preferredSubscriptionOffer(offer: winBackOffer)
}

From WWDC 2025-241:5:38


Testing

StoreKit Configuration File

Create:

  1. Xcode → File → New → StoreKit Configuration File
  2. Add products (consumables, non-consumables, subscriptions)
  3. Configure prices, images, descriptions

Enable in Scheme:

  1. Scheme → Edit Scheme → Run → Options
  2. StoreKit Configuration: Select .storekit file

Test Scenarios:

  • Successful purchases
  • Cancelled purchases
  • Subscription renewals (accelerated time)
  • Subscription expirations
  • Upgrades/downgrades
  • Offer code redemptions
  • Family Sharing (enable in config file)

Sandbox Testing

Create Sandbox Account:

  1. App Store Connect → Users and Access → Sandbox Testers
  2. Create test Apple ID
  3. Sign in on device Settings → App Store → Sandbox Account

Clear Purchase History:

  • Settings → App Store → Sandbox Account → Clear Purchase History

Migration from StoreKit 1

Key Changes

Delegates → Async/Await:

// StoreKit 1
class StoreObserver: NSObject, SKPaymentTransactionObserver {
    func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
        // Handle transactions
    }
}

// StoreKit 2
for await result in Transaction.updates {
    // Handle transactions
}

Receipt → Transaction:

// StoreKit 1
let receiptURL = Bundle.main.appStoreReceiptURL
let receipt = try Data(contentsOf: receiptURL!)

// StoreKit 2
let transaction: Transaction // Automatically verified!

Products → Product.products(for:):

// StoreKit 1
let request = SKProductsRequest(productIdentifiers: Set(productIDs))
request.delegate = self
request.start()

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

WWDC Sessions

  • WWDC 2025-241: What's new in StoreKit and In-App Purchase
  • WWDC 2025-249: Dive into App Store server APIs for In-App Purchase
  • WWDC 2024-10061: What's new in StoreKit and In-App Purchase
  • WWDC 2024-10062: Explore App Store server APIs for In-App Purchase
  • WWDC 2024-10110: Implement App Store Offers
  • WWDC 2023-10013: Meet StoreKit for SwiftUI
  • WWDC 2023-10140: What's new in StoreKit 2 and StoreKit Testing in Xcode
  • WWDC 2022-10007: What's new with in-app purchase
  • WWDC 2022-110404: Implement proactive in-app purchase restore
  • WWDC 2021-10114: Meet StoreKit 2

Quick Reference

Product Types

  • .consumable - Can purchase multiple times (coins, boosts)
  • .nonConsumable - Purchase once, own forever (premium, level packs)
  • .autoRenewable - Auto-renewing subscriptions
  • .nonRenewing - Fixed duration subscriptions

Transaction States

  • success - Purchase completed
  • userCancelled - User tapped cancel
  • pending - Requires action (Ask to Buy)

Subscription States

  • .subscribed - Active subscription
  • .expired - Subscription ended
  • .inGracePeriod - Billing issue, access maintained
  • .inBillingRetryPeriod - Apple retrying payment
  • .revoked - Family Sharing removed

Essential Calls

// Load products
try await Product.products(for: productIDs)

// Purchase
try await product.purchase(confirmIn: scene)

// Current entitlements
Transaction.currentEntitlements(for: productID)

// Transaction listener
Transaction.updates

// Subscription status
Product.SubscriptionInfo.status(for: groupID)

// Restore purchases
try await AppStore.sync()

// Finish transaction (REQUIRED)
await transaction.finish()