StoreKit

RSS for tag

Support in-app purchases and interactions with the App Store using StoreKit.

StoreKit Documentation

Posts under StoreKit subtopic

Post

Replies

Boosts

Views

Activity

StoreKit 2: Delayed Transaction and Entitlement Updates After Promo Code Subscription Redemption
I’m implementing a subscription purchase flow using promo code redemption via an external App Store URL. Flow: User taps “Purchase” in the app (spinner shown) App opens the promo redemption URL (apps.apple.com/redeem) User completes redemption in the App Store User returns to the app The app must determine whether the subscription was purchased within a reasonable time window The app listens to Transaction.updates and also checks Transaction.currentEntitlements when the app returns to the foreground. Issue: After redeeming a subscription promo code via the App Store and returning to the app, the app cannot reliably determine whether the subscription was successfully purchased within a short, user-acceptable time window. In many cases, neither Transaction.updates nor Transaction.currentEntitlements reflects the newly redeemed subscription immediately after returning to the app. The entitlement may appear only after a significant delay, or not within a 60-second timeout at all, even though the promo code redemption succeeded. Expected: When the user returns to the app after completing promo code redemption, StoreKit 2 should report the updated subscription entitlement shortly thereafter (e.g. within a few seconds) via either Transaction.updates or Transaction.currentEntitlements. Below is the minimal interactor used in the sample project. The app considers the purchase successful if either a verified transaction for the product is received via Transaction.updates, or the product appears in Transaction.currentEntitlements when the app returns to the foreground. Otherwise, the flow fails after a 60-second timeout. Questions: Is this entitlement propagation delay expected when redeeming promo codes through the App Store? Is there a recommended API or flow for immediately determining whether a subscription has been successfully redeemed? Is there a more reliable way to detect entitlement changes after promo code redemption without triggering user authentication prompts (e.g., from AppStore.sync())? import UIKit import StoreKit final class PromoPurchaseInteractor { private let timeout: TimeInterval = 60 private struct PendingOfferRedemption { let productId: String let completion: (Result<Bool, Error>) -> Void } private var pendingRedemption: PendingOfferRedemption? private var updatesTask: Task<Void, Never>? private var timeoutTask: Task<Void, Never>? enum DefaultError: Error { case generic case timeout } init() { NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil) } deinit { NotificationCenter.default.removeObserver(self) updatesTask?.cancel() timeoutTask?.cancel() } func purchaseProduct(using offerUrl: URL, productId: String, completion: @escaping (Result<Bool, Error>) -> Void) { guard pendingRedemption == nil else { completion(.failure(DefaultError.generic)) return } pendingRedemption = PendingOfferRedemption(productId: productId, completion: completion) startPurchase(using: offerUrl) } @objc private func willEnterForeground() { guard let pendingRedemption = pendingRedemption else { return } startTimeoutObserver() Task { if await hasEntitlement(for: pendingRedemption.productId) { await MainActor.run { self.completePurchase(result: .success(true)) } } } } private func startPurchase(using offerURL: URL) { startTransactionUpdatesObserver() UIApplication.shared.open(offerURL) { [weak self] success in guard let self = self else { return } if !success { self.completePurchase(result: .failure(DefaultError.generic)) } } } private func completePurchase(result: Result<Bool, Error>) { stopTransactionUpdatesObserver() stopTimeoutObserver() guard let _ = pendingRedemption else { return } pendingRedemption?.completion(result) pendingRedemption = nil } private func startTransactionUpdatesObserver() { updatesTask?.cancel() updatesTask = Task { for await update in Transaction.updates { guard case .verified(let transaction) = update else { continue } await MainActor.run { [weak self] in guard let self = self, let pending = self.pendingRedemption, transaction.productID == pending.productId else { return } self.completePurchase(result: .success(true)) } await transaction.finish() } } } private func stopTransactionUpdatesObserver() { updatesTask?.cancel() updatesTask = nil } private func startTimeoutObserver() { guard pendingRedemption != nil else { return } timeoutTask?.cancel() timeoutTask = Task { try? await Task.sleep(nanoseconds: UInt64(timeout * 1_000_000_000)) await MainActor.run { [weak self] in self?.completePurchase(result: .failure(DefaultError.timeout)) } } } private func stopTimeoutObserver() { timeoutTask?.cancel() timeoutTask = nil } private func hasEntitlement(for productId: String) async -> Bool { for await result in Transaction.currentEntitlements { guard case .verified(let transaction) = result else { continue } if transaction.productID == productId { return true } } return false } }
1
1
191
Jan ’26
Unable to load a subscription product in the app
Hi, I am building a new app in the App Store - the app is not live yet. I have setup an annual subscription product in AppStore Connect. Our problem is that we are unable to retrieve the product from our app - we've made sure that there are no missing metadata (e.g. price, availability). Has anyone encountered before? Appreciate any help provided. Thanks
7
0
183
Jan ’26
Unexpected Change in Apple Refund Handling CONSUMPTION_REQUEST - Impact on Subscription App with AI Backend
We offer a 3-day free trial, and our paywall clearly states that users will be charged after the trial ends. However, some users request refunds after the charge - even after fully using our app for days or even weeks. In some cases, refunds are approved despite the users having consumed our AI processing services for up to a month. Since our app relies on backend AI processing, each user session incurs a real cost. To prevent losses, we utilize RevenueCat’s CONSUMPTION_REQUEST system and have set our refundPreference to: "2. You prefer that Apple declines the refund". Until recently, Apple typically respected this preference, and 90% of refund requests were declined as intended. However, starting about a week ago, we observed a sudden reversal: Apple is now approving around 90% of refund requests, despite our refund preference. As a result, we are operating at a loss and have had to halt both our marketing campaigns and our 3-day free trial. We’re trying to understand whether this shift is due to a change in Apple’s refund policy, or if we need to handle CONSUMPTION_REQUEST differently on our end. Has anyone else experienced similar changes? Any insights would be greatly appreciated.
0
1
362
May ’25
Advanced Commerce (Sandbox) – Generic Product Still “Ready to Submit”
Hello, Our app is approved for the Advanced Commerce API and we are currently testing in the Sandbox environment only. We have created generic product identifiers and have already submitted them via the Advanced Commerce API Access form. However, the generic product status in App Store Connect is still “Ready to Submit.” For Sandbox testing, is this status expected, or do we need to submit an app build or the generic product for review before Advanced Commerce works correctly? Thank you.
2
0
225
Jan ’26
SKAN / AdAttributionKit Development Postback Not Triggering
We’re testing SKAN postbacks via AdAttributionKit but aren’t receiving any requests on our server even after generating development impressions and triggering a postback. Setup: Domain: https://linkrunner-skan.com Configured in Info.plist as: <key>NSAdvertisingAttributionReportEndpoint</key> <string>https://linkrunner-skan.com</string> <key>AttributionCopyEndpoint</key> <string>https://linkrunner-skan.com</string> Apple automatically appends the .well-known paths: /.well-known/private-click-measurement/report-attribution/ /.well-known/skadnetwork/report-attribution/ ATS diagnostics for the domain: PASS for all tests (TLS 1.0–1.3, PFS disabled, arbitrary loads allowed, etc.) Both .well-known paths are publicly accessible and return 200 OK Testing Flow: Enabled Developer → AdAttributionKit Developer Mode on iOS (15+) Followed Apple’s official guide: Testing AdAttributionKit with Developer Mode Generated test impression using: createAdAttributionKitDevelopmentImpression implemented in SKANManager.swift Called Postback.updateConversionValue with lockPostback: true Created Development Postback from Developer Settings Waited 30+ minutes while intercepting server requests (proxy + backend logs) What We’ve Tried So Far: Confirmed ATS compliance with nscurl --ats-diagnostics (all PASS) Verified .well-known paths are accessible publicly without redirects Tested endpoints manually with a POST request – server responds 200 OK Confirmed Info.plist entries exactly match Apple’s required keys Double-checked iOS device is running iOS 15+ with Developer Mode enabled Repeated test flow multiple times with fresh impressions and postbacks Waited up to 1 hour for postback (in case of delays) Issue: No POST requests are being received from Apple to either .well-known endpoint, even though the setup appears correct and ATS tests pass. References Used: Configuring an Advertised App Generating JWS Impressions Question: Has anyone faced a similar issue where AdAttributionKit Development Postbacks are not firing despite correct Info.plist setup, ATS compliance, and reachable .well-known endpoints? Any insight into possible missing configuration steps or testing nuances would be greatly appreciated.
0
1
94
Aug ’25
AppTransaction.shared doesn’t return originalAppVersion for users who installed the app in 2017
Hi, I'm using the AppTransaction.shared API to retrieve the originalAppVersion, but I'm encountering issues for users who originally installed the app in 2017. Specifically, the property doesn't seem to return the expected value (or returns nil) for these older accounts. I have verified this issue using a real App Store purchase from 2017. Steps to Reproduce: Use a test account that originally downloaded the app in 2017. Call: let shared = try await AppTransaction.shared print(shared.originalAppVersion) Observe that originalAppVersion is missing or not returned correctly. Any insights on whether this is expected behavior for very old App Store purchases, or if there is a workaround to reliably detect the original app version? Thanks in advance!
4
1
135
May ’25
transaction.subscriptionStatus (TestFlight) returns nil on iOS 26 (works from Xcode & on iOS 18)
I’m seeing an issue with subscriptions in TestFlight builds on iOS 26. Running from Xcode works as expected, and the App Store build looks fine. But when I install the same build via TestFlight, transaction.subscriptionStatus is nil. The identical binary behaves correctly on an iOS 18 device. Is this expected behavior on iOS 26 TestFlight, or am I missing something? Thanks!
1
1
219
Oct ’25
iOS 26 RC: Testflight showing wrong currency for sandbox accounts
My app has in app purchase for subscriptions, available in many countries. When using Sandbox App Store accounts on TestFlight with a locale different from my own in the iOS 26 RC, I'm getting incorrect currency coming back from Product.products(for: identifiers), and so my app displays the wrong price for the locale. However, the actual Apple Pay buy sheet shows the proper currency symbol and currency amount. This did not happen on prior versions of iOS. Is anyone else experiencing this?
1
1
287
Jan ’26
Unable to sign in to Sandbox Apple Account on Simulator
I am unable to sign in to a Sandbox Apple Account, where this issue occurs only via Simulator. Under Settings > Developer, I tap "Sign In" under Sandbox Apple Account. I enter my account credentials, and after bringing me back to the Developer page, the Sign In button briefly appears as disabled, before being re-enabled, without signing in to the account. (The account credentials are also recognized as correct, as I will receive an alert popup if incorrect.) See screenshots below: After signing in, Sign In button appears disabled... ... then is re-enabled without actually signing in to the account. I have now tried setting up multiple sandbox accounts via App Store Connect with various permutations (no confirmation of Apple Account email, confirming Apple Account email, logging in to iCloud and accepting terms of service), running different device simulators, running simulators on different Mac computers... none of which yield a different result. By contrast, I can sign in to the Sandbox Apple Account without issue on a physical device. The problem occurs only via Simulator.
0
1
188
Apr ’25
storefront.countryCode is fixed to USA in iOS 26 beta
Whether using Storefront.current?.countryCode or SKPaymentQueue.default().storefront?.countryCode, both are returning "USA" only. (It used to return the correct country code before the update.) In the sandbox environment, the country code is returned correctly, but in the TestFlight environment, it always returns "USA". There's no mention of this behavior in the beta release notes, so I'm posting it here for visibility.
1
1
163
Jul ’25
Can you buy an IAP via StoreKit 1 on iOS 26.2?
If an app on the App Store still uses StoreKit 1 (a.k.a. the Original StoreKit) to handle In-App Purchases, would IAPs work for users who download such app on iOS 26.2? Would the app allow the users to purchase an IAP via StoreKit 1 or would it be impossible to buy the IAP on iOS 26? The iOS Documentation says that SKPaymentQueue (which is a part of StoreKit 1) is "Deprecated" and "No longer supported.", with the support being for iOS 3.0–18.0. Does this mean that apps using StoreKit 1 won't be able to make IAP purchases when running on iOS 26?
1
1
180
Dec ’25
StoreKit2.Transaction.updates Returning Large Amounts of Historical Transactions, Causing Verification Traffic Surge
Over the past two days, we've observed an unusual spike in requests from some iOS users to our server endpoint responsible for verifying App Store purchase receipts. After sampling and analyzing the data, we found that the cause is related to the behavior of StoreKit2.Transaction.updates. Specifically, when listening for transaction updates, the system returns a large number of historical transactions — some dating back as far as one year. These callbacks are interpreted as "new" transactions, which in turn trigger repeated calls to Apple’s receipt verification servers, leading to an abnormal surge in traffic and putting pressure on our backend services. This behavior is ongoing and is something we've never encountered in our previous experience. It appears to be outside of expected behavior, and we suspect it may be due to some kind of abnormality or unintended usage scenario. We would appreciate guidance on the following: Is this a known behavior or issue with StoreKit2? Are there specific device states or conditions that could cause the system to emit historical transactions in bulk? Are there any recommended practices for mitigating or filtering such transaction floods? We have attached logs for reference. Any help identifying the root cause or suggestions for investigation would be greatly appreciated. 2025-07-24 12:39:58.594 +0400 listenForTransactions :{ "appTransactionId" : "704289572311513293", "environment" : "Production", "inAppOwnershipType" : "PURCHASED", "originalPurchaseDate" : 1713445834000, "originalTransactionId" : "430001791317037", "purchaseDate" : 1713445834000, "quantity" : 1, "signedDate" : 1753346396278, "storefrontId" : "143481", } 2025-07-24 12:39:58.594 +0400 listenForTransactions :{ "appTransactionId" : "704289572311513293", "deviceVerificationNonce" : "c4f79de2-a027-4b34-b777-6851f83f7e64", "environment" : "Production", "inAppOwnershipType" : "PURCHASED", "originalPurchaseDate" : 1713445849000, "originalTransactionId" : "430001791317270", "purchaseDate" : 1713445849000, "quantity" : 1, "signedDate" : 1753346396278, "storefrontId" : "143481", "transactionId" : "430001791317270", } 2025-07-24 12:39:58.594 +0400 listenForTransactions :{ "appTransactionId" : "704289572311513293", "deviceVerificationNonce" : "02f305d7-0b2d-4d55-b427-192e61b99024", "environment" : "Production", "inAppOwnershipType" : "PURCHASED", "originalPurchaseDate" : 1713511999000, "originalTransactionId" : "430001792218708", "purchaseDate" : 1713511999000, "quantity" : 1, "signedDate" : 1753346396278, "storefrontId" : "143481", "transactionId" : "430001792218708", } 2025-07-24 12:39:58.598 +0400 [INFO] [MKPaymentService:23]: [XLPay] listenForTransactions :{ "appTransactionId" : "704289572311513293", "deviceVerificationNonce" : "5ca85907-1ab6-4160-828e-8ab6d3574d6f", "environment" : "Production", "inAppOwnershipType" : "PURCHASED", "originalPurchaseDate" : 1713512034000, "originalTransactionId" : "430001792219189", "purchaseDate" : 1713512034000, "quantity" : 1, "signedDate" : 1753346396278, "storefrontId" : "143481", "transactionId" : "430001792219189", } 2025-07-24 12:39:58.599 +0400 listenForTransactions :{ "appTransactionId" : "704289572311513293", "deviceVerificationNonce" : "04869b50-b181-4b69-b4ff-025175e9cf14", "environment" : "Production", "inAppOwnershipType" : "PURCHASED", "originalPurchaseDate" : 1713512049000, "originalTransactionId" : "430001792219440", "purchaseDate" : 1713512049000, "quantity" : 1, "signedDate" : 1753346396278, "storefrontId" : "143481", "transactionId" : "430001792219440", }
1
1
170
Sep ’25
Inquiry Regarding Potential StoreKit v2 Transaction Handling Issue
Dear Apple Technical Support Team, We have encountered a potential issue related to transaction handling while using StoreKit v2, and would greatly appreciate your assistance in confirming the behavior or providing any relevant guidance. Issue Description: When calling Transaction.unfinished and listening to Transaction.updates on the client side, we noticed that some transactions—despite having already been processed and successfully completed with finish()—are being returned again upon the next app launch, which results in duplicate receipt uploads. Current Handling Flow: 1. Upon app launch: • Iterate over Transaction.unfinished to retrieve unfinished transactions; • Simultaneously listen for transaction changes via Transaction.updates (e.g., renewals, refunds); 2. For each verified transaction, we immediately call await transaction.finish(); 3. We then construct a transaction model, store it locally, and report it to our backend for receipt verification; 4. After the server successfully verifies the receipt, the client deletes the corresponding local record; 5. On every app launch, the client checks for any locally stored receipts that haven’t been uploaded, and re-uploads them if necessary. Key Code Snippets: private static func verifyReceipt(receiptResult: VerificationResult) -> Transaction? { switch receiptResult { case .unverified(_, _): return nil case .verified(let signedType): return signedType } } public static func handleUnfinishedTransactions(payConfig: YCStoreKitPayConfig, complete: ((YCStoreKitReceiptModel?) -> Void)?) { Task.detached { for await unfinishedResult in Transaction.unfinished { let transaction = YCStoreKitV2Manager.verifyReceipt(receiptResult: unfinishedResult) if let transaction { await transaction.finish() if transaction.revocationDate == nil { let receipt = YCStoreKitV2Manager.createStoreKitReceiptModel( transation: transaction, jwsString: unfinishedResult.jwsRepresentation, payConfig: payConfig, isRenew: false ) complete?(receipt) } } } } } private func observeTransactionUpdates() -> Task<Void, Never> { return Task { for await updateResult in Transaction.updates { let transaction = YCStoreKitV2Manager.verifyReceipt(receiptResult: updateResult) if let transaction { await transaction.finish() if transaction.revocationDate == nil { let receipt = YCStoreKitV2Manager.createStoreKitReceiptModel( transation: transaction, jwsString: updateResult.jwsRepresentation, payConfig: self.payConfig, isRenew: false ) self.callProgressChanged(.receiptPrepared, receiptModel: receipt, errorType: .none, error: nil) } } } } } Our Questions: 1. Is it possible for Transaction.unfinished or Transaction.updates to return transactions that have already been finished? Specifically, if a transaction was successfully finished in a previous app launch, could it still be returned again during the next launch? 2. Are there any flaws in our current handling process? Our current sequence is: finish() → construct model → local save → report to server → delete after verification. Could this order lead to timing issues where StoreKit considers a transaction unfinished? 3. If we need your assistance in investigating specific user transaction records or logs, what key information should we provide? We greatly appreciate your support and look forward to your response to help us further optimize our transaction processing logic.
1
0
97
Jul ’25
StoreKit 2: is there a way for an app to be informed in real time if an auto-renewable subscription is cancelled by the user?
Hello, In my iOS app, I have a customer center where the user can see some details about its current subscription. I display things like the billing period, the price, the introductory offer state, the renewal date if it's not cancelled or the expiration date if it's cancelled, etc. From this screen, the user can open the subscription management sheet. I want to detect if the user cancels the subscription from this sheet or from the App Store (when the app is running) so I can refresh the information displayed on my customer center. I checked the asynchronous sequences provided by StoreKit 2 like Transaction.updates or Product.SubscriptionInfo.Status.updates and tested with a Sandbox account on my physical device with the app debugged using Xcode. But I noticed these sequences don't emit when I cancel the subscription in Sandbox. Is this the expected behavior? Is there a way to observe in real time if a user cancels the subscription? I can still manually check when the sheet is dismissed but it's not ideal because I want to know even if the user cancel from outside of the app with the app running. Thank you, Axel
2
1
370
2w
StoreKit2: appAccountToken in purchase() always returns first value instead of updated UUID
Hello, I’m experiencing an issue with StoreKit 2 when passing a new appAccountToken for each purchase request. Case-ID: 15948169 (for DTS reference) Description of the Problem When initiating a purchase, I generate a new UUID to use as the appAccountToken: let serverTransactionId = UUID() let options: Set<Product.PurchaseOption> = [ .appAccountToken(serverTransactionId) ] let result = try await product.purchase(options: options) Expected Behavior: Each new purchase should return the updated appAccountToken that I pass into the purchase options. Actual Behavior: The payload response after success always contains the same appAccountToken from the very first transaction. It ignores subsequent UUIDs I pass and keeps reusing the original one. This causes issues because the same identifier is being reused across multiple transactions, making it difficult to map purchases to the correct user session. Steps to Reproduce Generate a fresh UUID using UUID(). Pass it as .appAccountToken when calling purchase(). Complete the transaction in the sandbox environment. Inspect the payload response → The appAccountToken value is always the same as the first one used, not the newly provided one. Additional Info I do have a focused test project that reproduces this issue. The issue appears specific to appAccountToken persistence across multiple transactions. Has anyone else experienced this behavior with StoreKit 2? Is this expected (Apple caching the first token) or could this be a bug?
3
1
380
Nov ’25
New App with Subscription Review
We are launching a new app with a subscription in-app purchase (IAP). The Status of the IAP is "Waiting for Review". We want to submit the app to app for review, but the section to select an IAP with the app is not appearing. According to online sources, if we submit a new app for review, without selecting the IAP that goes with it, then the app may be released in the App Store without users being able to purchase the IAP. This actually happened to us years ago and I can't remember how we got around it, but it was painful. It seems strange that Apple would allow this problem to persist for years. Why not just hire an intern to fix it? Maybe because they are too busy running around in circles at the new spaceship headquarters. Regardless, our IAP has been sitting in "Waiting for Review" status for a while now and I'm concerned it may never be approved. Any advice would be appreciated.
2
1
461
7h
How to Simulate Subscription Cancellation with Products.storekit in Simulator?
Hi, Currently, instead of using a real device and test account for in-app purchase testing, we are using Products.storekit with the Simulator. Our app offers a subscription plan with a 3-day free trial. We would like to simulate the following test scenarios: User cancels the subscription within the 3-day free trial period. User cancels the subscription after the 3-day free trial period. However, in Xcode, under Debug > StoreKit > Manage Transactions..., we couldn’t find an option to simulate a subscription cancellation. There is an option to refund the purchase, but we believe this is not the same as a cancellation. Do you have any idea how we can simulate these two cases using Products.storekit and the Simulator? Thanks in advance!
1
1
88
Apr ’25
InvalidRequestError on iOS 18.4
We’ve recently encountered an increased rate of purchase errors StoreKit.InvalidRequestError error 1 (https://developer.apple.com/documentation/storekit/invalidrequesterror) specifically on iOS 18.4. We suspect this might be related to the new Advanced Commerce API introduced by Apple, although we haven’t made any changes related to this API in our app. Have you experienced similar issues since the release of iOS 18.4 or when integrating the Advanced Commerce API? Any insights or suggestions would be greatly appreciated. Thanks!
1
1
314
May ’25
Unable to retrieve data from In App Purchase
I want to add in-app purchasing to my app, but I can't figure out what part of my workflow is wrong. I created a product for my app in iTunes Connect (the product ID is com.mycompany.products.***) and it's in "Ready to submit" status. I created a sandbox test user for this app. I connected to iTunes on a real device using the sandbox AppleID. I went back to XCode and added in-app purchasing to my app. I turned on developer mode on the real device and logged in as the sandbox user. I built the app and ran it on a real device (not the simulator). I tried to get product information (com.mycompany.products.***) but nothing was returned. In-app purchasing is registered in App Store Connect and the status is "Ready to submit". The code only retrieves product information in a simple way, so I don't think there's a problem. inAppPurchase.getProducts(["com.mycompany.products.***"]).then(console.log).catch(console.error); But it only returns an empty array. What could be wrong? Any help would be much appreciated.
2
1
72
Jul ’25