Hi, all!
I have an AppStore Server-side question. User sends up an AppReceipt that I am validating. What's the best way to tell the receipt belongs to said user? I want to make sure that the source of the AppReceipt was actually the original purchaser of the item. Is fetching Transaction + AppAccountToken the only way? AppAccountToken can only be utilized if the original purchase used it, and it is associated with the user's data. Is there another way?
StoreKit
RSS for tagSupport in-app purchases and interactions with the App Store using StoreKit.
Post
Replies
Boosts
Views
Activity
Hi, all!
I am wondering about something about App Receipts. I'm using App Receipt hash to key some information server-side. I'm curious however, if that doesn't actually have a possible flaw. Is it possible to get a new receipt that would yield a different hash for the same transaction? Reinstalling the app, perhaps? Installing the app on a new phone? Basically, I want to make sure this hash is something I can rely on. If the user can get a new hash for the same purchase, that's obviously problematic.
Thanks!
Hi apple team,
I'm using Apple Root Certificates from https://www.apple.com/certificateauthority/ for communicating with App Store Server Library for receipt validation API.
Apple Computer, Inc Root certificate from the website is Not Valid After: Monday, 10 February 2025 at 01:18:14 Central European Standard Time.
When we can expect update of this certificate.
Thank you
We are in the process of implementing promotional offers for auto-renewable subscriptions in our app using StoreKit 2.
For testing, we use a sandbox user alongside a new user on our platform. I can successfully purchase an Introductory Offer through the app. Once the user is eligible for a Promotional Offer (based on a previous purchase), we retrieve the Promotional Offer identifier and signature from our backend and display the offer.
After initiating the purchase and having the user enter their Sandbox password, the transaction is added to the Payment Queue. However, it fails with the following error:
Purchase failed error: invalidOfferSignature
Additionally, the error returned is:
"Purchase did not return a transaction: Error Domain=ASDServerErrorDomain Code=3903 "Unable to Purchase" UserInfo={NSLocalizedFailureReason=Unable to Purchase, client-environment-type=Sandbox, AMSServerErrorCode=3903, storefront-country-code=IND}"
We are using StoreKit 2 APIs for this process.
Has anyone encountered this issue when working with StoreKit 2, or found a solution to resolve it?
Hi,
I've been reported, and I've experienced myself, extremely slow responses from StoreKit 2 on boot.
I can reproduce the issue quite consistently with the iPhone, sometimes Transaction.currentEntitlements hangs for minutes even after the device has attained full Internet access.
My app is a VPN client, so there's certainly a chicken-and-egg situation, but I'm performing a basic test without the in-app validation in the packet tunnel provider. The tunnel starts fine on boot (on-demand), whereas the app takes ages to return from AppTransaction.shared and Transaction.currentEntitlements.
I guess this is caused by some network call inside the framework (maybe spoiled by the on-demand VPN?), but with no timeout, exceptions, logging, or future-proof fallback (appStoreReceiptURL is deprecated), I'm poking in the dark. It goes without saying, keeping the users waiting minutes to be credited for their purchases is a no-go.
I still can't find a reasonable workaround, but the frustrating part is that StoreKit 2 was supposed to make these things straightforward. At times, it doesn't feel "ready" and I spend on it a lot of time that I'd rather invest in the product.
Does this sound familiar to anybody?
Thanks in advance
Davide
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?
Hello everyone,
I’m hoping someone might help with auto-renewable subscription validation in Apple’s Sandbox environment. Here’s the situation:
My Setup:
I’ve configured three auto-renewable subscriptions in App Store Connect and generated an In-App Purchase key (with the correct Issuer ID and Key ID). (I also tried the App Store Connect API Keys)
I’m using Apple’s App Store Server API v2 endpoints (GET /inApps/v2/subscriptions/{originalTransactionId}/latest) to fetch the latest subscription status.
I’ve created several Sandbox test users (with fresh email addresses), signed out of old test accounts on my devices, and tested purchasing subscriptions anew.
What Works:
I am receiving valid Server Notifications from Apple (e.g. SUBSCRIBED, DID_RENEW) with the correct environment: "Sandbox" field.
My JWT generation appears to be correct because I’m no longer receiving 401 errors—only 404. That suggests Apple accepts the key and credentials.
My fallback logic attempts production first; if it sees a 404 or 410, it switches over to the Sandbox endpoint. This is exactly what Apple’s documentation recommends.
The Problem:
Whenever I query GET https://api.storekit-sandbox.itunes.apple.com/inApps/v2/subscriptions/{originalTransactionId}/latest using the originalTransactionId from Apple’s own Server Notification, Apple returns a 404 (indicating it can’t find that subscription).
This happens even though the subscription is active in Sandbox (I see notifications arriving for it).
I’ve tried adding a brief waiting period (2 seconds) before calling the Sandbox endpoint, but it consistently returns 404. I’ve also tried multiple retries over a longer timeframe without success.
I tested multiple fresh Sandbox test users, ensuring each one was signed in to the device’s App Store. After each new purchase, I still get the same 404.
Additional Checks:
These are definitely auto-renewable subscriptions, not non-renewing or consumable products.
I also tried calling GET /inApps/v2/subscriptions/{transactionId}/latest but I still see 404.
I tried everything mentioned above in production as I said to no avail:
GET https://api.storekit.itunes.apple.com/inApps/v2/subscriptions/{originalTransactionId}/latest
Inquire the types of notifications that can occur in a SANDBOX environment
Hello, WWDC 2024 is trying to conduct a test to receive notifications related to ONE_TIME_CHARGE, CONNSUMPTION_REQUEST, CONMSUMPTION_INFO, REFUND, and REFUND_DECLINED as described in the example of purchasing consumables, but as a result of the continuous search, I found that it is difficult to occur except for
ONE_TIME_CHARGE.
So, in order to verify only the business logic as shown below, we are testing only the business logic without actually calling the API after purchasing the test and saving the signaled Payload that we received in response to ONE_TIME_CHARGE. Can we actually request a refund for the test purchase and receive the corresponding notification and actually send the response?
public void handleSignedNotification(String signedNotification) throws Exception {
ResponseBodyV2DecodedPayload payload = signedDataVerifier.verifyAndDecodeNotification(signedNotification);
NotificationTypeV2 type = payload.getNotificationType();
//For Apple Server Notification, only ONE_TIME_CHARGE notifications are enabled in the test environment, so for testing, change them as below to test whether they are running business logic
type = NotificationTypeV2.REFUND;
log.info("Apple NotificationType : {}", type);
switch (type) {
case CONSUMPTION_REQUEST:
handleConsumptionRequest(payload);
break;
case REFUND:
handleRefund(payload);
break;
case REFUND_DECLINED:
handleRefundDeclined(payload);
break;
// For other necessary notifications, just take a log
default:
log.info("Unhandled notification: {}", type);
}
}
Regarding the call of 'CONSUMPTION_INFO', which is the response of 'CONSUMPTION_REQUEST'
Is there a value that WWDC 2024 must include when sending CONMSUMPTION_INFO, which is the response to CONNSUMPTION_REQUEST described in the refund example? I'm going to call the API with only sample provision and consumption like the sample code you introduced in the video.
I was told to submit my refund preference within 12 hours, but can I submit it as UNDECLARED at first and use the method to express my intention? When I receive the notification, I will save it in the DB and save it in the administrator page of the service so that the administrator can choose.
2-1. Some of the materials I looked for are told that Apple can proceed with the refund even 12 hours ago, and to express your opinion as soon as I receive the notification, but I wonder if this is correct.
If you get a notification as below, you should write whether you used it or not by referring to the consumption information. I think the customer said to check whether the data was provided when applying for a refund. Should I take it out of decodedTransaction, check the value, and just call it NO_PREFERENCE? I'd appreciate it if you could give me some advice.
Below is a part of the code I implemented.
private void handleConsumptionRequest(ResponseBodyV2DecodedPayload notification) throws Exception {
// 1. transaction ID get
String signedTransactionInfo = notification.getData().getSignedTransactionInfo();
JWSTransactionDecodedPayload decodedTransaction = signedDataVerifier.verifyAndDecodeTransaction(signedTransactionInfo);
String transactionId = decodedTransaction.getTransactionId();
// 2. Extract the relevant transaction (The following example is an in-app payment and will be accumulated in two types of DBs, stored in one of the two)
Sample sample = sampleService.findByAppleTransactionId(transactionId);
Example example = exampleService.findByAppleTransactionId(transactionId);
Boolean canRefund = false;
// 3. Check consumption information
if (sample != null) {
canRefund = checkSampleStatusForApplePurchaseRefund(sample);
} else if (example != null) {
canRefund = checkExampleStatusForApplePurchaseRefund(example);
}
// 4. Create Refund Preferences
RefundPreference refundPreference = determineRefundPreference(canRefund);
// 5. Creating a ConsumptionRequest Object
ConsumptionRequest request = new ConsumptionRequest()
.refundPreference(refundPreference)
.sampleContentProvided(true);
log.info("forTest~ canRefund: {}", canRefund);
log.info("forTest~ sample: {}", sample.toString());
log.info("forTest~ example: {}", example.toString());
log.info("forTest~ refundPreference: {}", refundPreference);
log.info("forTest~ request: {}", request);
// 6. Transfer to App Store (annotated with dummy requests that only confirm current business requests are going right)
// appStoreServerAPIClient.sendConsumptionData(transactionId, request);
}
Hi all,
I am adding the following StoreKit 2 code to my app, and I don't see anything in Apple's documentation that explains the unverified case. When is that case exercised? Is it when someone has tampered with the app receipt? Or is it for more mundane things like poor network connectivity?
// Apple's docstring on `shared` states:
// If your app fails to get an AppTransaction by accessing the shared property, see refresh().
// Source: https://developer.apple.com/documentation/storekit/apptransaction/shared
var appTransaction: VerificationResult<AppTransaction>?
do {
appTransaction = try await AppTransaction.shared
} catch {
appTransaction = try? await AppTransaction.refresh()
}
guard let appTransaction = appTransaction else {
AppLogger.error("Couldn't get the app store transaction")
return false
}
switch appTransaction {
case .unverified(appTransaction, verificationError):
// For what reasons should I expect this branch to be entered in production?
return await inspectAppTransaction(appTransaction, verifiedByApple: false)
case .verified(let appTransaction):
return await inspectAppTransaction(appTransaction, verifiedByApple: true)
}
Thank you,
Lou
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
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.
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
Network
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
No matter what I do whether I delete the app or reset my phone it still prompts me to log in to the previous sandbox test account. I'm not even signed into that account on my phone or in the App Store.
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.
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.
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
https://developer.apple.com/documentation/appstoreserverapi/get-v2-history-_transactionid
I would like to inquire about the detailed triggers for updating receipts in this API specification.
Recently, I was using this API with sort=DESCENDING&revoked=false to retrieve the expiration date of the most recent receipt and determine the subscription status. However, for some reason, an old receipt with an earlier expiration date appeared as the first receipt, and I would like to know the reason for this.
Can you provide information on what specific events or actions trigger the updating of receipts in this API?
Also, regarding https://developer.apple.com/documentation/appstoreserverapi/status, will statuses 3 and 4 not be returned in the response unless the billing grace period is enabled in the App Store?
https://developer.apple.com/documentation/appstoreserverapi/get-v2-history-_transactionid
I would like to inquire about the detailed triggers for updating receipts in this API specification.
Recently, I was using this API with sort=DESCENDING&revoked=false to retrieve the expiration date of the most recent receipt and determine the subscription status. However, for some reason, an old receipt with an earlier expiration date appeared as the first receipt, and I would like to know the reason for this.
Can you provide information on what specific events or actions trigger the updating of receipts in this API?
Also, regarding https://developer.apple.com/documentation/appstoreserverapi/status, will statuses 3 and 4 not be returned in the response unless the billing grace period is enabled in the App Store?
-- Japanese
こちらのAPI仕様ですが、どのようなトリガーでレシートが更新されるのか詳細に伺いたいです。
先日このAPIを使用してsort=DESCENDING&revoked=falseで一番最初のレシートの有効期限を取得して課金状態か判断していたのですが、どういったわけか一番最初のレシートに古い有効期限のレシートが入ってきたので理由を知りたいです。
また、https://developer.apple.com/documentation/appstoreserverapi/status
のステータスはAppleStoreで請求猶予期間を有効化しないと3,4はレスポンスされませんか?