Skip to main content
ZeroSettle works alongside Apple’s StoreKit 2. You can use web checkout as your primary payment method and StoreKit as a fallback, or offer both options side-by-side.
Building for Android? See Play Billing Integration for the equivalent guide using Google Play Billing.

How It Works

When syncStoreKitTransactions is enabled (the default), the SDK’s StoreKitManager automatically:
  1. Listens for StoreKit transaction updates in the background
  2. Syncs completed StoreKit transactions to your ZeroSettle backend
  3. Merges StoreKit entitlements with web checkout entitlements
This means a user who purchases via StoreKit on one device will have their entitlements available via restoreEntitlements() on any device — alongside their web checkout purchases.

Configuration

StoreKit sync is enabled by default:
import ZeroSettleKit

// StoreKit sync enabled (default)
ZeroSettle.shared.configure(.init(
    publishableKey: "your_key"
))

// StoreKit sync disabled (use with RevenueCat)
ZeroSettle.shared.configure(.init(
    publishableKey: "your_key",
    syncStoreKitTransactions: false
))
If you use RevenueCat, set syncStoreKitTransactions: false to avoid conflicts. RevenueCat manages its own StoreKit listener. See RevenueCat Integration.

Purchasing via StoreKit

Use purchaseViaStoreKit() to trigger a native StoreKit 2 purchase:
do {
    let transaction = try await ZeroSettle.shared.purchaseViaStoreKit(
        productId: "premium_monthly",
        userId: currentUser.id
    )
    // transaction is a StoreKit.Transaction
    print("StoreKit purchase completed: \(transaction.id)")
} catch let error as StoreKitPurchaseError {
    switch error {
    case .userCancelled:
        // User tapped Cancel in the StoreKit sheet
        break
    case .pending:
        // Purchase requires approval (e.g., Ask to Buy)
        showMessage("Purchase pending approval")
    case .productNotFound(let id):
        showError("Product \(id) not found in App Store")
    case .verificationFailed:
        showError("Transaction verification failed")
    case .unknown:
        showError("An unexpected error occurred")
    }
}

Hybrid Purchase Flow

A common pattern is to offer web checkout as the primary option with StoreKit as a fallback:
struct ProductView: View {
    @State private var selectedProduct: ZSProduct?
    let product: ZSProduct

    var body: some View {
        VStack(spacing: 16) {
            // Primary: Web checkout (lower fees)
            Button("Buy — \(product.webPrice.formatted)") {
                selectedProduct = product
            }
            .checkoutSheet(
                item: $selectedProduct,
                userId: currentUser.id
            ) { result in
                handleResult(result)
            }

            // Fallback: StoreKit (if user prefers App Store)
            if product.storeKitAvailable, let skPrice = product.storeKitPrice {
                Button("Buy with App Store — \(skPrice.formatted)") {
                    Task {
                        try? await ZeroSettle.shared.purchaseViaStoreKit(
                            productId: product.id,
                            userId: currentUser.id
                        )
                    }
                }
                .foregroundStyle(.secondary)
            }
        }
    }
}
The Product model tells you whether a StoreKit product is available:
  • product.storeKitAvailabletrue if the product is synced to App Store Connect
  • product.storeKitPrice — the App Store price (may differ from webPrice)
  • product.appStorePrice — same as storeKitPrice

StoreKit Sync Delegate

When syncStoreKitTransactions is enabled, the SDK automatically syncs StoreKit purchases to your ZeroSettle backend. You can listen for these sync events:
class PurchaseManager: ZeroSettleDelegate {
    func zeroSettleDidSyncStoreKitTransaction(productId: String, transactionId: UInt64) {
        // A StoreKit transaction was synced to ZeroSettle
        print("Synced StoreKit transaction \(transactionId) for \(productId)")
    }

    func zeroSettleStoreKitSyncFailed(error: Error) {
        // StoreKit sync failed — purchase still went through StoreKit
        // The sync will be retried automatically
        print("StoreKit sync failed: \(error)")
    }
}
Sync failures don’t affect the StoreKit purchase itself — the user gets their purchase through Apple immediately. Failed syncs are handled by a persistent retry queue:
  • Automatic retries: Failed syncs are enqueued to StoreKitSyncQueue with exponential backoff (1s → 5s → 30s → 5min delays between attempts)
  • Persistence: The retry queue is stored in UserDefaults, so pending syncs survive app restarts and device reboots
  • Max attempts: Each sync is retried up to 5 times before being abandoned
  • Transaction safety: On sync failure, transaction.finish() is not called — StoreKit will redeliver the unfinished transaction on the next app launch, providing an additional safety net beyond the retry queue
  • No user impact: The purchase succeeds immediately through Apple; only the backend sync (entitlement creation, revenue tracking) is retried in the background

Entitlement Sources

Entitlements track where each purchase came from:
let entitlements = try await ZeroSettle.shared.restoreEntitlements(userId: currentUser.id)

for entitlement in entitlements {
    switch entitlement.source {
    case .webCheckout:
        print("\(entitlement.productId): purchased via ZeroSettle")
    case .storeKit:
        print("\(entitlement.productId): purchased via App Store")
    }
}
Both sources are merged into a single entitlements list. Your app doesn’t need to treat them differently.

Error Types

StoreKitPurchaseError

ErrorDescription
.productNotFound(String)Product ID not found in App Store Connect
.verificationFailed(Error)StoreKit transaction verification failed
.userCancelledUser tapped Cancel in the StoreKit purchase dialog
.pendingPurchase requires approval (Ask to Buy)
.unknownUnexpected error

Requirements

RequirementVersion
iOS17.0+
StoreKitStoreKit 2
Xcode15.0+
App Store ConnectProducts must be configured and approved