What’s new about StoreKit 2 server API
Tags: ios, discuss, swift
Published: 2021-07-12 13:07:18

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.

##Request authentication

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.   

###Key generation 
First of all, [create a private key](https://developer.apple.com/documentation/appstoreserverapi/creating_api_keys_to_use_with_the_app_store_server_api) 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

![StoreKit 2 API Creation of a private key](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yntpzsq63rdccquuh29f.png)

###Creating a token
The next step is to create a token that will be used to authorize the requests. This process is described in detail in [documentation](https://developer.apple.com/documentation/appstoreserverapi/generating_tokens_for_api_requests), 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. 

```python
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}'
}
```

## Signed transactions
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.  
1. Base64 header.
2. Base64 transaction payload.
3. Transaction signature.

`Base64(header) + "." + Base64(payload) + "." + sign(Base64(header) + "." + Base64(payload))`

###Transaction header
A header is needed to make sure the transaction is authentic. `Alg` key contains an encryption algorithm, `x5c` key contains a certificate chain.

```JSON
{
	"kid": "AMP/DEV",
	"alg": "ES256",
	"x5c": [
		"MIIEO...",
		"MIIDK..."
	]
}
```

###Transaction payload
```JSON
{
  "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](https://developer.apple.com/documentation/appstoreserverapi/jwstransactiondecodedpayload). Below, I will describe the most important changes.
   
1. 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.  
  
2. Apple also added `offerType` and `offerIdentifier` 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.   

3. 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`

4. Another new field – `type` – includes transaction type. Possible values:
`Auto-Renewable Subscription`
`Non-Consumable`
`Consumable`
`Non-Renewing Subscription`

5. `Cancellation_date` and `cancellation_reason` fields have new names now: `revocationDate` and `revocationReason`. 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.
6. All keys return in camelCase format (just like in all App Store Server API requests).
7. All dates are displayed in Unix timestamp format in milliseconds.   

##User subscription status
To check the current user’s [subscription status](https://developer.apple.com/documentation/appstoreserverapi/get_all_subscription_statuses), 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.
 
```JSON
{
  "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.

###Information about subscription renewal
`SignedRenewalInfo` key contains the information about subscription [renewal](https://developer.apple.com/documentation/appstoreserverapi/jwsrenewalinfodecodedpayload).

```JSON
{
  "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.

## User’s transaction history
To get the user’s [transaction history](https://developer.apple.com/documentation/appstoreserverapi/get_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.     

```JSON
{
  "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 transaction notifications

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.

![comparison of notification type in Apple API StoreKit and StoreKit 2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/eftls4jiyqm0x465cxqy.png)

##Notification types
Notifications now have types, thus, one notification for any action of a user is enough to understand what happened. 

![Notification types in StoreKit 2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/eftls4jiyqm0x465cxqy.png)

Notification types
```JSON
{
  "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.  

##Working with Sandbox environment 
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.     

##Conclusion 
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.