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

IAP working in StoreKitTest on XCODE but not in TestFlight – shows mapped error "Product not available"
Hey everyone, I'm currently preparing an older iOS app for App Store release that includes a non-consumable In‑App Purchase using StoreKit 2. Everything works perfectly in the StoreKitTest environment inside Xcode – the product loads, the purchase flow runs, the transaction verifies. However, when I run the same app through TestFlight, I always get the error: ❌ Product not available - mapped to Here’s what I’ve already checked: ✅ The product ID is correct and matches what’s in App Store Connect (case-sensitive). ✅ The IAP is created in App Store Connect and includes: Title Product ID Price Tier Screenshot for review ✅ The App Store "Paid Applications" agreement is active. ✅ The app is using the correct bundle ID. ✅ I'm using Product.products(for: [productID]) from StoreKit 2. ✅ I’ve implemented fallback and retry logic (e.g. reload after delay). ✅ All IAP logic is wrapped in @MainActor and async-safe. As the App got Rejected on Review, the IAP is also now in the Rejected Status. Now the IAP shows status: 🟠 "Developer Action Required" And App Review rejected the IAP with the message: "Your first In‑App Purchase must be submitted together with a new app version." But if I add the App to the Test again and therefore the IAP, then the app will get Rejected again for App Completeness, IAP does not work... What am I doing wrong here? :) Thanks a lot in advance Cheers, Niklas
1
0
105
Aug ’25
Can users create their own subscription plans?
I'm considering developing an app where users can create their own subscription plans by freely setting their prices, similar to YouTube's membership feature. I understand that in-app purchases must be used to unlock features within the app. With that in mind, I searched for APIs to enable this functionality but couldn't find relevant information. When I contacted Apple directly, they mentioned that they couldn't provide specific answers unless the app is under review. If anyone has knowledge about the following points, I would greatly appreciate your response: Is it possible to implement a feature similar to YouTube's membership using in-app purchase APIs? If it's not feasible with in-app purchases, is it allowed to use external payment services like Stripe?
1
0
222
Jan ’25
The operation could not be completed. (Error 2 in SKErrorDomain.) during subscription upgrade
I have a renewing monthly subscription in my app and recently added upgrade possibilities to yearly and 6 month subscriptions. Those new subscriptions were reviewed, approved and published to App Store. I'm showing a modal for users in the app from where they can upgrade their subscription. Upgrading was tested with real devices on Sandbox and TestFlight. There has been successful purchases through the in app modal in production app, and directly upgrading from App Store. However, for some users there seems to happen a failed transaction with paymentCancelled error code during the upgrade. The IAP is still successful, their subscription is upgraded and they haven't voluntarily canceled the IAP. The localized description of the error is "Toimintoa ei voitu suorittaa. (Virhe 2 kohteessa SKErrorDomain.)" which translates to "The operation could not be completed. (Error 2 in SKErrorDomain.)" These users have various iPhones (iPhone 12 Pro, iPhone 14 Pro, iPhone 15 Pro, iPhone 16 Pro) with up to date iOS versions (>= 18.3.1). I'm receiving DID_CHANGE_RENEWAL_PREF (UPGRADE) server notification of these purchases on my server. I haven't been able to reproduce this error myself. Any ideas why StoreKit might fail the transaction with paymentCancelled error but still successfully upgrade the subscription?
0
0
182
Mar ’25
Urgent: Reports of Duplicate Charges via AlipayHK on Apple Pay
We’ve recently observed an escalating number of complaints from AlipayHK users regarding duplicate charges when completing transactions via Apple Pay. While no similar issues have been reported by users of other credit card providers integrated with Apple Pay, the problem appears isolated to AlipayHK transactions. Key Details: Multiple users confirm being charged twice for single transactions. Complaints are increasing in frequency, indicating a potential systemic issue. No overlapping reports from non-AlipayHK payment methods at this time. To safeguard customer trust and ensure seamless payment experiences, we kindly request Apple’s support in: Investigating whether the root cause stems from Apple Pay’s transaction handling. Collaborating with AlipayHK (if necessary) to resolve the issue promptly. Providing guidance on interim measures to prevent further duplicate charges. Could Apple confirm if this is a known issue and share a timeline for resolution? We’re eager to assist in any way possible to mitigate impact on users. Thank you for your urgent attention to this matter.
1
0
83
May ’25
External Link Account Modal language
Hi All, We are developing our app with an approved External Link Account Entitlement. During the development process (such as installing from Xcode or creating an Ad-hoc build and installing it on a phone), the open() function of the External Link Account API displays the modal our native language. The app only localized to that language. However, after uploading the app with the same configuration to TestFlight, the modal somehow appears in English instead. What could be causing this issue with the External Link Account modal? How can the open() function display the modal in another language when installed from Xcode or an Ad-hoc release build, but in English when installed from TestFlight? How can we show only our native lanugage version only to our Users? Thank you in advance
2
0
169
Mar ’25
Transaction.Id from Product.Purchase is different from the transactionId notified by the server notification
Hi, In an auto-renewable subscription scenario, I receive a transaction from Product.Purchase and then send the transaction ID (e.g., 500000000738201) to my API server. After receiving the response, I called transaction.finish(). The account has purchased the subscription before and expired. So it's re-subscribe. And then, I received a RESUBSCRIBE notification from Apple’s server to my API server. I noticed a discrepancy where the transaction ID in the notification is decreased by one (e.g., 500000000738200 instead of 500000000738201). I’m wondering why this discrepancy occurs and how it happens. Best regards, RoyHuang
0
0
289
Jan ’25
Handling failed transactions in Storekit 2
When simulating a Storekit error like an invalid device verification or others of that type, should we finish a failed transaction? When I test with a Storekit configuration file, all failed transactions persist after every restart. The Apple-provided sample code for Storekit 2 has transactions finished only when they are successful.
0
0
206
Jan ’25
[StoreKit1] IAP Works in TestFlight but Fails During App Review (2.1 Rejection)
Hello Apple Developer Team, We're experiencing consistent IAP approval rejections under Guideline 2.1, despite successful TestFlight verification. Here's our detailed situation: Environment StoreKit 1 implementation Tested on iOS 18.5 or 18.6 devices Sandbox environment works perfectly Verification Steps Taken ✅ Confirmed all Product IDs match App Store Connect exactly ✅ Validated 10+ successful TestFlight transactions (attached screenshot samples) ✅ Verified banking/tax agreements are active Objective-C Code (StoreKit1 Implementation) - (void)buyProductId:(NSString *)pid AndSetGameOrderID:(NSString *)orderID{ if([SKPaymentQueue canMakePayments]){ if (!hasAddObserver) { [[SKPaymentQueue defaultQueue] addTransactionObserver:_neo]; hasAddObserver = YES; } self.neoOrderID = orderID; [[NSUserDefaults standardUserDefaults] setValue:orderID forKey:Pay_OrderId_Key]; self.productID = pid; NSArray * product = [[NSArray alloc]initWithObjects:self.productID, nil]; NSSet * nsset = [NSSet setWithArray:product]; SKProductsRequest * request = [[SKProductsRequest alloc]initWithProductIdentifiers:nsset]; request.delegate = self; [request start]; }else{ NSString * Err = @"Pembelian tidak diizinkan. Silakan aktifkan perizinan di pengaturan"; // UnitySendMessage("GameManager", "IAPPurchaseFailed", [Err UTF8String]); return; } } - (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response { NSArray * product = response.products; if ([product count] == 0) { [[SKPaymentQueue defaultQueue]removeTransactionObserver:_neo]; hasAddObserver = NO; NSString * Err = [NSString stringWithFormat:@"Err = 01, Item tidak ditemukan %@",self.productID]; // UnitySendMessage("GameManager", "IAPPurchaseFailed", [Err UTF8String]); return; } SKProduct * p = nil; for (SKProduct * pro in product) { if ([pro.productIdentifier isEqualToString:self.productID]){ p = pro; }else{ [request cancel]; [[SKPaymentQueue defaultQueue]removeTransactionObserver:_neo]; hasAddObserver = NO; NSString * Err = [NSString stringWithFormat:@"Err = 02, %@",self.productID]; // UnitySendMessage("GameManager", "IAPPurchaseFailed", [Err UTF8String]); return; } } SKMutablePayment * mPayment = [SKMutablePayment paymentWithProduct:p]; mPayment.applicationUsername = [NSString stringWithFormat:@"%@",self.neoOrderID]; if(!hasAddObserver){ [[SKPaymentQueue defaultQueue] addTransactionObserver:_neo]; hasAddObserver = YES; } [[SKPaymentQueue defaultQueue] addPayment:mPayment]; } - (void)request:(SKRequest *)request didFailWithError:(NSError *)error{ [[SKPaymentQueue defaultQueue]removeTransactionObserver:_neo]; hasAddObserver = NO; NSString * Err = [NSString stringWithFormat:@"Err = 0%ld %@", (long)error.code, self.productID]; // UnitySendMessage("GameManager", "IAPPurchaseFailed", [Err UTF8String]); } - (void)requestDidFinish:(SKRequest *)request{ } - (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transaction{ for(SKPaymentTransaction *tran in transaction){ if (SKPaymentTransactionStatePurchased == tran.transactionState){ [self completeTransaction:tran]; }else if(SKPaymentTransactionStateFailed == tran.transactionState){ [self failedTransaction:tran]; } } } - (void)failedTransaction: (SKPaymentTransaction *)transaction { NSString * detail = [NSString stringWithFormat:@"%ld",(long)transaction.error.code]; // UnitySendMessage("GameManager", "IAPPurchaseFailed", [detail UTF8String]); [[SKPaymentQueue defaultQueue]removeTransactionObserver:_neo]; hasAddObserver = NO; [[SKPaymentQueue defaultQueue] finishTransaction: transaction]; } - (void)completeTransaction:(SKPaymentTransaction *)transaction{ NSMutableDictionary * mdic = [NSMutableDictionary dictionary]; NSString * productIdentifier = transaction.payment.productIdentifier; NSData * _recep = nil; NSString * _receipt = @""; if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_6_1) { _recep = transaction.transactionReceipt; _receipt = [[NSString alloc]initWithData:_recep encoding:NSUTF8StringEncoding]; } else { _recep = [NSData dataWithContentsOfURL:[[NSBundle mainBundle] appStoreReceiptURL]]; _receipt = [_recep base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed]; } NSString * gameOrderid = [transaction payment].applicationUsername; if (gameOrderid == nil) { gameOrderid = [[NSUserDefaults standardUserDefaults] objectForKey:Pay_OrderId_Key]; } if(_receipt != nil && gameOrderid != nil){ mdic[@"orderid"] = gameOrderid; mdic[@"productid"] = productIdentifier; mdic[@"receipt"] = _receipt; }else{ [[SKPaymentQueue defaultQueue] finishTransaction:transaction]; return; } NSData * data = [NSJSONSerialization dataWithJSONObject:mdic options:kNilOptions error:nil]; NSString * jsonString = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding]; if (hasAddObserver) { [[SKPaymentQueue defaultQueue] removeTransactionObserver:_neo]; hasAddObserver = NO; } // UnitySendMessage("GameManager", "IAPPurchaseSuecess", [jsonString UTF8String]); [self verifyReceipt:_recep completion:^(BOOL success, NSDictionary *response) { if (success) { NSLog(@"verify success"); // [[SKPaymentQueue defaultQueue] finishTransaction:transaction]; [self verifySuecessDelTransactions]; } }]; } - (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue { for(SKPaymentTransaction *tran in queue.transactions){ if (SKPaymentTransactionStatePurchased == tran.transactionState){ [self completeTransaction:tran]; } } } - (void)verifySuecessDelTransactions{ SKPaymentQueue *paymentQueue = [SKPaymentQueue defaultQueue]; NSArray<SKPaymentTransaction *> *transactions = paymentQueue.transactions; if (transactions.count == 0) { return; } for (SKPaymentTransaction *transaction in transactions) { if (transaction.transactionState == SKPaymentTransactionStatePurchased || transaction.transactionState == SKPaymentTransactionStateRestored) { [paymentQueue finishTransaction:transaction]; } } }
1
0
124
Aug ’25
In app loot purchasing
hI, I’m looking for some clarity on the rules around in app purchasing, so I play a game with in app purchases that you have random odds of getting items out of loot boxes, they split the loot into sections, so featured, mega rare, rare and common. for each of these sections they give a % chance that they show of what chance you have to get an item out of that section, I.e featured is 1.24% and common is 74% ect. my question is should they then have to display the odds of what each item has in those sections as well or is it good enough just to have the sections with % chance? So each section could have 10 items in, should those items have a % next to as well? any advice you can give with this would be appreciated, if needs be I can send picture examples of what I’m explaining
0
0
230
Jan ’25
How do win-back offers displayed for discounted subscriptions?
In all the illustrations of win-back offers, I see an example of "Get 3 months off, then $X/month", as seen below. First, I'm not exactly clear how each configuration translates into an actual offer in practice: If I want to offer 3 months off on an annual subscription, ie. only if the user pays for the annual offer (basically a 25% discount), is that possible? If I set a "Free" type, of 3 months, I guess that would allow the user to cancel before paying for the annual, correct? If I set a "Pay up front" type, with a 25% discount, how would that show up to the user on the App Store? Secondly, is eligibility to an offer determined by the user elapsing on the same subscription or any subscription in the same subscription group? Thank you
0
0
323
Jan ’25
ExternalPurchaseCustomLink.token(for:) returns nil on one TestFlight device (while isEligible == true) — other device gets SERVICES token
I’m implementing StoreKit External Purchase Custom Links (EU) and so far it is really painful. I am running into a strange, device-specific issue. On 3/4 devices it works. On one device I never get a token at launch nor before a transaction. isEligible is true everywhere. All devices have versions 18.5 and are located in Germany. Info.plist: SKExternalPurchaseCustomLinkRegions is set to EU storefront codes and I have followed every step in the documentation: https://developer.apple.com/documentation/storekit/externalpurchasecustomlink Good device: At launch → ACQUISITION = nil, SERVICES = token present. Works consistently. Faulty device: At launch → ACQUISITION = nil, SERVICES = nil. Same before transaction. No token ever reaches my server from this device. isEligible is true on both devices. Any experts or help on the matter?
5
0
179
3w
Puchases / subscriptions issues
Hi, I've some issues on my application about purchases. My app is available for iOS and tvOS. Some part of my code is the same on both target (my purchase code is one of them). The issue is: Some users can't do a purchase on Apple TV (but not all and I can't reproduce) Some users purchased on a device and are not considered as pro on the other device (logged with same apple account). Again it's not all. Here is my store code
1
0
322
Jan ’25
Inconsistent notification coming from AppStore Servers
I encountered a scenario involving a subscription and need to determine if it's a problem or an expected outcome. Here are the details: My service received a notification from Apple of type DID_CHANGE_RENEWAL_STATUS with subtype AUTO_RENEW_DISABLED. The status field received on the payload was equal to 1 - Active. (2024-12-19T15:34:53.801) My service again received a DID_CHANGE_RENEWAL_STATUS with subtype AUTO_RENEW_DISABLED. But the status field received was 2 - Expired. (2024-12-19T23:34:57.527) My service received an EXPIRED with subtype VOLUNTARY notification. (2024-12-19T23:35:01.669) Is the event 2 an inconsistent event? Since we are receiving a notification that means the auto renew was disabled when the subscription was already expired.
0
0
335
Jan ’25
App Store Server Notifications v1 deprecation timelime
Greetings! I was reviewing the documentation for the Apple Store Server Notification v1 APIs and saw the deprecation notice. We are currently unable to migrate to v2 in the near future. With that in mind, I want to know if there were any plans to shut off v1, and if so, was there any timeline we could expect? Thank you in advance. Reference: https://developer.apple.com/documentation/appstoreservernotifications/app-store-server-notifications-version-1
0
0
312
Jan ’25
Python App. Sandbox testing IAP Auto Renewal Subscription
I have created a Python app and built it with pyinstaller and codesigned everything. Now I want to Sandbox test it. In my appstore connect account i have created a subscriptions id. I read that if I am logged out from the AppStore and have codesigned my .app file with a Developer Certificate i should be able to run the app on my local mac and when i click on the "Buy" button it should connect to my app store connect setup. I have implemented StoreKit in my app and use a storekit_bridge to combine the .swift code with my python app. However when i run the app. I get this: "25-07-24 21:01:12,557 - FEC - WARNING - StoreKit: fetchProducts returned empty result 2025-07-24 21:01:12,557 - FEC - INFO - StoreKit fetch_products returned: {"products": []} 2025-07-24 21:01:12,557 - FEC - ERROR - StoreKit: Failed to parse product info: No products returned from JSON" And no login screen appears where I should be able to enter my Sandbox email adress and password. Anyone here who has experience with a Python app combined with In App Purchases? Hope someone can help me out with this.
0
0
144
Jul ’25
Dev Defined IAP Transaction Metadata
I have an app that works by being able to map IAP transactions to a predefined user ID. This means when I consume events from Apple's App Store Server Notifications endpoints I have to do a reverse lookup in order to assign permissions within my app. Workflow: User purchases subscription within the app via IAP. The app persists the subscriptionID from the Apple IAP library in my cloud database (Firestore). Cloud function receives the event from App Store Server Notifications endpoint and looks up the user ID containing the persisted transactionID (with retries to avoid race condition). Question: This workflow works but it seems an improvement would be to allow dev's to append metadata, like the user ID, to the transaction submitted to IAP that we can access within the signedTransactionInfo of the event from the App Store Server Notifications endpoint in order to facilitate a direct lookup of the user document needing it's permissions updated. This would greatly simplify workflows that use non-Apple systems as a source of truth for app permissions. Does this actually exist already? If not, is there a feature request platform?
0
0
51
Mar ’25
react native iap not providing the subscription information
I am handling the buy subscription with this function const handleBuySubscription = async (productId) =&gt; { try { await requestSubscription({ sku: productId, }); setLoading(false); } catch (error) { setLoading(false); if (error instanceof PurchaseError) { errorLog({ message: [${error.code}]: ${error.message}, error }); } else { errorLog({ message: "handleBuySubscription", error }); } } }; but the requestSubscription({ sku: productId, }) does not return anything, and it is stuck at await
0
0
78
Aug ’25
Non-consumable IAP for free trial
Hello. My newly released app includes a 1 day free trial. I've done this by creating a non-consumable in-app purchase priced at 0. I consider the free trial active if there's a transaction (from Transaction.currentEntitlements) for that product such that transaction.originalPurchaseDate is less than 24 hours ago. This works fine locally in the simulator and also in TestFlight, however it does not seem to work in the actual app from the App Store. The user can "purchase" it fine; they see the purchase sheet with the product name and the $0.00 price, and when they double press the side button it all seems to work. However, the app then behaves as if it didn't work. The free trial product is no longer available though. One thing is that I didn't follow the naming convention “XX-day Trial”. Could that be the problem? If so, is that meant to be for the product reference name?
2
0
312
Feb ’25