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
Installation
Details
Usage
After installing, this skill will be available to your AI coding assistant.
Verify installation:
skills listSkill Instructions
name: axiom-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: 2025-12-08 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:
axiom-in-app-purchases— Discipline skill with testing-first workflow, architecture patterns- (Future:
iap-auditoragent for auditing existing IAP code) - (Future:
iap-implementationagent 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.updatesre-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 datasampleContentProvided(optional): Sample provided before purchasedeliveryStatus(required): "DELIVERED" or various UNDELIVERED statusesrefundPreference(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 accessREFUND_PRORATED: Partial refund - revoke proportional accessFAMILY_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:
- Xcode → File → New → StoreKit Configuration File
- Add products (consumables, non-consumables, subscriptions)
- Configure prices, images, descriptions
Enable in Scheme:
- Scheme → Edit Scheme → Run → Options
- 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:
- App Store Connect → Users and Access → Sandbox Testers
- Create test Apple ID
- 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)
Resources
WWDC: 2025-241, 2025-249, 2024-10061, 2024-10062, 2024-10110, 2023-10013, 2023-10140, 2022-10007, 2022-110404, 2021-10114
Docs: /storekit
Skills: axiom-in-app-purchases
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 completeduserCancelled- User tapped cancelpending- 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()
More by CharlesWiltgen
View allUse when porting OpenGL/DirectX to Metal - translation layer vs native rewrite decisions, migration planning, anti-patterns
Use when UI is slow, scrolling lags, animations stutter, or when asking 'why is my SwiftUI view slow', 'how do I optimize List performance', 'my app drops frames', 'view body is called too often', 'List is laggy' - SwiftUI performance optimization with Instruments 26 and WWDC 2025 patterns
Use for Core Location troubleshooting - no location updates, background location broken, authorization denied, geofence not triggering
Reference — Comprehensive SwiftUI navigation guide covering NavigationStack (iOS 16+), NavigationSplitView (iOS 16+), NavigationPath, deep linking, state restoration, Tab+Navigation integration (iOS 18+), Liquid Glass navigation (iOS 26+), and coordinator patterns