98
What’s new about StoreKit 2 server API
Apple introduced a new version of StoreKit 2 during WWDC 2021 that took place recently. This is a framework responsible for making purchases in iOS. A share of apps with in-app purchase and subscription features grows steadily, and Apple significantly simplified integration of in-app purchases into the app by releasing StoreKit 2. Today, we will consider working with StoreKit 2 on the part of the server, in other words, with the help of App Store Server API.
In the current API version, you need Shared Secret to send a request. This is a secret fixed string that you can get in App Store Connect. A new version of API uses JSON Web Token (JWT) standard for request authentication.
First of all, create a private key that will be used to authorize the requests. Open App Store Connect and go to the Users and Access section, then to Keys tab. Select In-App Purchase key type. Download a new key. You will also need its ID – you can copy it on the same page as Issue ID which can be found in the App Store Connect API tab.
Creation of a private key for working with App Store Server API
The next step is to create a token that will be used to authorize the requests. This process is described in detail in documentation, so there’s no reason to pay too much attention to it. Here’s an example of a ready-made implementation for Python. It’s worth noting that it makes no sense to generate a new token for every new request. When creating a token, you set its lifetime at up to 60 minutes and use the same token during this period.
import time, uuid
from authlib.jose import jwt
BUNDLE_ID = 'com.adapty.sample_app'
ISSUER_ID = '4336a124-f214-4d40-883b-6db275b5e4aa'
KEY_ID = 'J65UYBDA74'
PRIVATE_KEY = '''
-----BEGIN PRIVATE KEY-----
MIGTAgMGByqGSMBHkAQQgR/fR+3Lkg4...
-----END PRIVATE KEY-----
'''
issue_time = round(time.time())
expiration_time = issue_time + 60 * 60 # 1 hour expiration
header = {
'alg': 'ES256',
'kid': KEY_ID,
'typ': 'JWT'
}
payload = {
'iss': ISSUER_ID,
'iat': issue_time,
'exp': expiration_time,
'aud': 'appstoreconnect-v1',
'nonce': str(uuid.uuid4()),
'bid': BUNDLE_ID
}
token_encoded = jwt.encode(header, payload, PRIVATE_KEY)
token_decoded = token_encoded.decode()
authorization_header = {
'Authorization': f'Bearer {token_decoded}'
}
In a new version of API, all transactions returned in JSON Web Signature (JWS) standard. This is a string consisting of three parts divided by dots.
- Base64 header.
- Base64 transaction payload.
- Transaction signature.
Base64(header) + "." + Base64(payload) + "." + sign(Base64(header) + "." + Base64(payload))
A header is needed to make sure the transaction is authentic. Alg
key contains an encryption algorithm, x5c
key contains a certificate chain.
{
"kid": "AMP/DEV",
"alg": "ES256",
"x5c": [
"MIIEO...",
"MIIDK..."
]
}
{
"transactionId": "1000000831360853",
"originalTransactionId": "1000000806937552",
"webOrderLineItemId": "1000000063561721",
"bundleId": "com.adapty.sample_app",
"productId": "basic_subscription_1_month",
"subscriptionGroupIdentifier": "27636320",
"purchaseDate": 1624446341000,
"originalPurchaseDate": 1619686337000,
"expiresDate": 1624446641000,
"quantity": 1,
"type": "Auto-Renewable Subscription",
"appAccountToken": "fd12746f-2d3a-46c8-bff8-55b75ed06aca",
"inAppOwnershipType": "PURCHASED",
"signedDate": 1624446484882,
"offerType": 2,
"offerIdentifier": "basic_subscription_1_month.pay_as_you_go.3_months"
}
Apple changed and extended the transaction format. From my point of view, now, it’s more convenient to work with them. You can learn details about a new format in documentation. Below, I will describe the most important changes.
Apple added
appAccountToken
field, which contains your system’s user ID. This ID must be in UUID format, it is set in the mobile app when a purchase is being initialized. If it is set, it will be returned in all transactions in this chain (renewal, billing issues, etc.), and you will easily understand which user made a purchase.Apple also added
offerType
andofferIdentifier
fields that contain the information about a used offer (if any). Here are values for offerType field:
1 — intro offer (available only for the users without active or expired subscriptions);
2 — promo offer (available only for current and expired subscriptions);
3 — offer code. If a promo offer or offer code was used,offerIdentifier
key will contain the ID of the used offer. In the past, it was impossible to track the use of the offer on the server-side, this worsened the analytics. Now, you can use offer codes for analytics.Apple added
inAppOwnershipType
field, which helps to understand whether a user bought a product or accessed it thanks to a family subscription. Possible values:
PURCHASED
FAMILY_SHARED
Another new field –
type
– includes transaction type. Possible values:
Auto-Renewable Subscription
Non-Consumable
Consumable
Non-Renewing Subscription
Cancellation_date
andcancellation_reason
fields have new names now:revocationDate
andrevocationReason
. As a reminder, they contain a date and a reason for subscription revocation as a result of a refund, so the new name looks more logical.All keys return in camelCase format (just like in all App Store Server API requests).
All dates are displayed in Unix timestamp format in milliseconds.
To check the current user’s subscription status, send a GET request to https://api.storekit.itunes.apple.com/inApps/v1/subscriptions/{originalTransactionId}
, where {originalTransactionId}
is the ID of any transaction chain of the user. In return, you will get transactions with statuses for every group of subscriptions.
{
"environment": "Sandbox",
"bundleId": "com.adapty.sample_app",
"data": [
{
"subscriptionGroupIdentifier": "39636320",
"lastTransactions": [
{
"originalTransactionId": "1000000819078552",
"status": 2,
"signedTransactionInfo": "eyJraWQiOi...",
"signedRenewalInfo": "eyJraWQiOi..."
}
]
}
]
}
The status
key displays the current subscription status, based on it, you can decide if you should provide a user with access to the paid features of the app. Possible values:
1 — subscription is active, a user must be able to access paid features.
2 — subscription has expired, a user must not be able to access paid functions.
3 — the subscription’s status is Billing Retry, meaning that a user didn’t cancel it, but experiences problems with paying. Apple will try to charge the card for 60 days. A user must not be able to access paid functions.
4 — the subscription’s status is Grace Period, meaning that a user didn’t cancel it, but experiences payment issues. Grace Period is on in App Store Connect, so a user must be able to access paid features.
5 — subscription was canceled as a result of a refund, a user must not be able to access paid functions.
SignedTransactionInfo key contains the information about the last transaction in the chain. You can find the details about its format above.
SignedRenewalInfo
key contains the information about subscription renewal.
{
"expirationIntent": 1,
"originalTransactionId": "1000000819078552",
"autoRenewProductId": "basic_subscription_1_month",
"productId": "basic_subscription_1_month",
"autoRenewStatus": 0,
"isInBillingRetryPeriod": false,
"signedDate": 1624520884048
}
This information allows us to understand what will happen to the subscription during the next pay period. For example, if you see that a user canceled auto-renewal, you can offer them to switch to another subscription plan or provide them a promo offer. It’s convenient to track this kind of events with the help of server notifications, which I’ll tell you about soon.
To get the user’s transaction history, send GET request to https://api.storekit.itunes.apple.com/inApps/v1/history/{originalTransactionId}
, where {originalTransactionId}
is the ID of any chain of transactions of the user. In return, you will get an array of transactions sorted by time.
{
"revision": "1625872984000_1000000212854038",
"bundleId": "com.adapty.sample_app",
"environment": "Sandbox",
"hasMore": true,
"signedTransactions": [
"eyJraWQiOiJ...",
"joiRVMyNeyX...",
"5MnkvOTlOZl...",
...
]
}
A request can contain no more than 20 transactions. If a user has more, the hasMore
flag’s value will be true
. If you need the next transaction page, send the request again with the revision
GET parameter containing. It will contain the value from the same key.
Server notifications help to get information about new purchases, renewals, billing issues, etc. This helps to build more accurate analytics, as well as simplifies managing the subscriber’s status.
The existing server notifications (V1) can solve most of the problems, but sometimes they are inconvenient. Mostly, it’s about the situation when you get several notifications for just one action of a user. For example, now, when a user upgrades a subscription, Apple sends two notifications: DID_CHANGE_RENEWAL_STATUS
and INTERACTIVE_RENEWAL
. To process this case currently, you need to save the status somehow and check if the second notification was sent. In a new version of server notifications (V2), there’s only one notification for one action of a user. This is much more convenient.
The second version of server notifications features new events - OFFER_REDEEMED
, EXPIRED
, and GRACE_PERIOD_EXPIRED
. They make managing subscriber status much easier. SUBSCRIBED
and PRICE_INCREASE
events are improved events from the first version.
Notifications now have types, thus, one notification for any action of a user is enough to understand what happened.
Notification types
{
"notificationType": "SUBSCRIBED",
"subtype": "INITIAL_BUY",
"version": 2,
"data": {
"environment": "Sandbox",
"bundleId": "com.adapty.sample_app",
"appAppleId": 739104078,
"bundleVersion": 1,
"signedTransactionInfo": "eyJraWQiOi...",
"signedRenewalInfo": "eyJraWQiOi..."
}
}
Server notifications contain information about a transaction and a renewal in JWS format which I described earlier.
You need to use the URL of the Sandbox environment to test purchases: https://api.storekit-sandbox.itunes.apple.com.
A new version of server notifications isn’t available for testing yet. Once it’s available, it will be possible to specify different URLs for Production and Sandbox notifications. You can choose V2 for Sandbox, and V1 for Production for testing.
Also, App Store Connect now allows to:
*Clear purchase history for the Sandbox user, meaning that you don’t have to create a new account to do that anymore.
- Change the store country for the Sandbox user.
- Change Sandbox subscription renewal period, for example, you can make a monthly purchase that lasts 1 hour instead of 5 minutes.
Apple significantly improved working with in-app purchases and subscriptions on the server-side. From my point of view, here’re the most useful new features:
- Full-fledged promo offer and offer code support;
- Simpler and more informative server notifications;
- An opportunity to learn about the current subscription’s status with a simple API call;
- Clearing purchase history of the user’s Sandbox.
Switching to a new API won’t be hard, it’s enough to get originalTransactionId
for every receipt. It’s likely it’s already contained in your base.
98