StoreKit 2 Purchase Validation and Subscriptions in iOS 16

StoreKit 2 modernizes in-app purchase workflows in iOS 16 by replacing legacy App Store receipts with cryptographically signed JSON Web Signatures (JWS) and Swift concurrency. This article details how StoreKit 2 handles transaction validation directly on-device, tracks auto-renewable subscription statuses using dedicated status models, and monitors real-time entitlement updates without relying on manual receipt-parsing logic.

Cryptographic Purchase Validation with JWS

Legacy StoreKit required retrieving an encrypted receipt file from the application bundle and either decoding it locally via OpenSSL or sending it to Apple's verifyReceipt server. StoreKit 2 eliminates this pattern entirely. Every transaction returned by StoreKit 2 is represented as a signed JWS token.

When your app queries products, transactions, or completes a purchase, the API wraps the result in an enum called VerificationResult<T>.

let result = try await product.purchase()

switch result {
case .success(let verification):
    switch verification {
    case .verified(let transaction):
        // Signature verified by Apple's public key; deliver content
        await transaction.finish()
    case .unverified(let transaction, let verificationError):
        // Signature validation failed; do not unlock content
        break
    }
case .userCancelled, .pending:
    break
}

StoreKit 2 automatically validates the JWS token against Apple’s root certificate authority on the device. If the signature is intact and the payload is authentic, it returns .verified(let transaction). If the payload has been tampered with or fails certificate validation, it returns .unverified. Once content is unlocked, calling await transaction.finish() notifies the App Store that the entitlement has been granted.

Determining Active Entitlements

To grant users access to non-consumable items and active subscriptions, iOS 16 relies on Transaction.currentEntitlements. This asynchronous sequence queries the latest transaction for every product the user is currently entitled to, automatically filtering out expired subscriptions and revoked transactions.

func updateEntitlements() async {
    for await verificationResult in Transaction.currentEntitlements {
        guard case .verified(let transaction) = verificationResult else {
            continue
        }
        
        // Grant access based on transaction.productID
    }
}

If a user revokes access, a subscription lapses, or an item is refunded, the item is removed from Transaction.currentEntitlements.

Querying Subscription Status

For auto-renewable subscriptions, StoreKit 2 introduces Product.SubscriptionInfo.status(for:). This method returns a list of subscription statuses within a given subscription group, accounting for Family Sharing and account changes.

Each subscription status exposes two critical components:

  1. Transaction Information: Provides the latest verified transaction and expiration details.
  2. Renewal Information: An additional JWS-signed payload (RenewalInfo) containing the current state of the subscription.

A subscription's state is evaluated via the Product.SubscriptionInfo.Status.State enum:

  • .subscribed: The subscription is active and in good standing.
  • .expired: The subscription has lapsed without renewing.
  • .inBillingRetryPeriod: Payment failed, and the App Store is actively attempting to collect payment.
  • .inGracePeriod: Payment failed, but the user maintains access under an active Billing Grace Period.
  • .revoked: Apple Customer Support refunded or revoked the subscription.
if let subscription = product.subscription {
    let statuses = try await subscription.status
    for status in statuses {
        guard case .verified(let renewalInfo) = status.renewalInfo,
              case .verified(let transaction) = status.transaction else {
            continue
        }

        switch status.state {
        case .subscribed, .inGracePeriod:
            // Maintain user access
            let willAutoRenew = renewalInfo.willAutoRenew
        case .inBillingRetryPeriod:
            // Notify user to update payment method
        case .expired, .revoked:
            // Revoke access
        default:
            break
        }
    }
}

Listening for Real-Time Lifecycle Updates

StoreKit 2 provides a continuous update stream via Transaction.updates. This allows apps to react immediately to out-of-app events, including external renewals, Ask to Buy approvals, refunds, and subscription changes made via App Store settings.

Task(priority: .background) {
    for await verificationResult in Transaction.updates {
        guard case .verified(let transaction) = verificationResult else {
            continue
        }
        
        // Update app state
        await transaction.finish()
    }
}

By combining automatic JWS verification, clear subscription state models, and reactive transaction streams, StoreKit 2 in iOS 16 removes the historical complexities of client-side StoreKit implementation while maintaining high security.