Apphud – integrate, analyze and improve in-app purchases and subscriptions in your iOS/Android apps

Development

App Store Server API in Action

In our previous article we talked about new StoreKit 2 framework that was introduced at WWDC 2021.

Now will continue our set of articles from this conference and will talk about changes in REST API related to In-App Purchases.

App Store Server API – is a new REST API, which lets you get information about all customer’s in-app purchases. The main difference from the old verifyReceipt endpoint is that you no longer need to send a large base64 receipt to the server. Retrieving information is done using original transaction ID, and both requests and responses are being signed using JWT and API Key generated from App Store Connect.

Generating In-App Purchase API Key

Generating a key for in-app purchases is the same as generating a Subscription Key – the tab has been simply renamed:

To generate API Key for in-app purchases go to:

  1. Users and Access
  2. Keys
  3. In-App Purchase

Download a key and save it to a safe place. Note that you can download a key just once.

Issuer ID

For creating requests you will also need Issuer ID which can be found at Keys > App Store Connect API tab. If this field is missing on the page, you will probably need to create your first App Store Connect API Key, even if you won’t use it. You can also try signing from the owner account.

Creating a JWT

JSON Web Token (JWT) uses open standard RFC 7519, that defines a way to securely transmit information.

Generating a token is done using 3 steps:

  1. Creating the JWT header
  2. Creating the JWT payload
  3. Signing the JWT

Header has three fields:

{
"alg": "ES256",
"kid": "2X9R4HXF34",
"typ": "JWT"
}

Where alg and typ – static values, and kid – is your key ID.

JWT payload looks like this:

{
 "iss": "57246542-96fe-1a63e053-0824d011072a",
 "iat": 1623085200,
 "exp": 1623086400,
 "aud": "appstoreconnect-v1",
 "nonce": "6edffe66-b482-11eb-8529-0242ac130003",
 "bid": "com.apphud"
}

iss – is Issuer ID, which we got from App Store Connect.

iat – token creation date, in seconds.

exp – token expiration date, in seconds. Must be less than 1 hour after token creation date.

aud – static value “appstoreconnect-v1”.

nonce – a random unique request identifier, “salt”.

bid – Bundle ID of the app.

More information about JWT payloads can be found here.

Get transaction information

To get a list of transactions, you will need the original transaction ID of the subscription. By default, the API returns 20 transactions at a time, sorted from older to newer ones. If there are more than 20 transactions, a parameter hasMore will be true.

URL is the following:

https://api.storekit.itunes.apple.com/inApps/v1/subscriptions/{original_transaction_id}

And in sandbox domain is the following:

https://api.storekit-sandbox.itunes.apple.com

JWT library is very popular and exists for all main languages. We will provide code example in Ruby.

Let’s create StoreKit class:

require 'jwt'
require_relative 'jwt_helper'
require 'httparty'

class StoreKit
...

attr_reader :private_key, :issuer_id, :original_transaction_id, :key_id, :bundle_id, :response

ALGORITHM = 'ES256'

def jwt
   JWT.encode(
     payload,
     private_key,
     ALGORITHM,
     headers
  )
 end

 def headers
  { kid: key_id, typ: 'JWT' }
 end

 def payload
  {
     iss: issuer_id,
     iat: timestamp,
     exp: timestamp(1800),
     aud: 'appstoreconnect-v1',
     nonce: SecureRandom.uuid,
     bid: bundle_id
  }
 end
end

Pretty simple here. We just defined the methods we described earlier.

Now let’s add URL variable and add some code to start a request:

URL = 'https://api.storekit-sandbox.itunes.apple.com/inApps/v1/subscriptions/%<original_transaction_id>s'

def request!
   url = format(URL, original_transaction_id: original_transaction_id)
   result = HTTP.get(url, headers: { 'Authorization' => "Bearer #{jwt}" })
   # raise UnauthenticatedError if result.code == 401
   # raise ForbiddenError if result.code == 403

   result.parsed_response
 end

To call this code, let’s create a separate file subscription.rb where we initialize our StoreKit class instance and call it :

key_id = File.basename(ENV['KEY'], File.extname(ENV['KEY'])).split('_').last
ENV['KEY_ID'] = key_id

StoreKit.new(
 private_key: File.read("#{Dir.pwd}/keys/#{ENV['KEY']}"),
 issuer_id: '69a6de82-48b4-47e3-e053-5b8c7c11a4d1',
 original_transaction_id: ENV['OTI'],
 key_id: key_id,
 bundle_id: 'com.apphud'
).call

In the response we get JSON with JWT signed fields.

Decoding Response

To decode a response we need a Public Key. It can be extracted from our Private Key. Let’s write a helper class JWTHelper:

require 'jwt'
require 'byebug'
require 'openssl/x509/spki'

# JWT class
class JWTHelper
 ALGORITHM = 'ES256'

 def self.decode(token)
   JWT.decode(token, key, false, algorithm: ALGORITHM).first
 end

 def self.key
   OpenSSL::PKey.read(File.read(File.join(Dir.pwd, 'keys', ENV['KEY']))).to_spki.to_key
 end
end

This class reads a private key using OpenSSL library and extracts a public key using to_spki method (Simple Public Key Infrastructure). Then decodes a JWT from the response using public key and ES256 algorithm.

Let’s decode our response:

def decoded_response
   response['data'].each do |item|
     item['lastTransactions'].each do |t|
       t['signedTransactionInfo'] = JWTHelper.decode(t['signedTransactionInfo'])
       t['signedRenewalInfo'] = JWTHelper.decode(t['signedRenewalInfo'])
     end
   end

   response
 end

If everything is correct, we will get a final JSON:

{
 "environment": "Sandbox",
 "bundleId": "com.apphud",
 "data": [
  {
     "subscriptionGroupIdentifier": "20771176",
     "lastTransactions": [
      {
         "originalTransactionId": "1000000809414960",
         "status": 2,
         "signedTransactionInfo": {
           "transactionId": "1000000811162893",
           "originalTransactionId": "1000000809414960",
           "webOrderLineItemId": "1000000062388288",
           "bundleId": "com.apphud",
           "productId": "com.apphud.monthly",
           "subscriptionGroupIdentifier": "20771176",
           "purchaseDate": 1620741004000,
           "originalPurchaseDate": 1620311199000,
           "expiresDate": 1620741304000,
           "quantity": 1,
           "type": "Auto-Renewable Subscription",
           "inAppOwnershipType": "PURCHASED",
           "signedDate": 1623773050102
        },
         "signedRenewalInfo": {
           "expirationIntent": 1,
           "originalTransactionId": "1000000809414960",
           "autoRenewProductId": "com.apphud.monthly",
           "productId": "com.apphud.monthly",
           "autoRenewStatus": 0,
           "isInBillingRetryPeriod": false,
           "signedDate": 1623773050102
        }
      }
    ]
  }
]
}

As you can see, lastTransactions array has information about last transaction of a subscription, as well as subscription status. The value of the status field is 2, which means expired. All subscription statuses are described here.

There is also a new field "type": "Auto-Renewable Subscription", which is an in-app purchase type in a human-readable string.

Unfortunately, transaction prices are still missing in new API.

The full source code from this article can be found here.

Conclusion

New App Store Connect API provides more information for developers and works much faster due to absence of the large base64 receipt parameter.

Advantages of new API:

  • Lightweight and fast request, passing original_transaction_id is enough.
  • Shared Secret is no longer needed.
  • There are some additional fields, like status, type.
  • New APIs available, like managing refunds from the app.
  • Transactions are already sorted in the API.

Disadvantages:

  • Quite complex request authorization: you need to generate API Key and copy Issuer ID from App Store Connect.
  • Transaction prices are still missing. However, Apphud successfully calculates prices for all transactions, even in such difficult cases, like prorated refunds during upgrades, price increases, etc.

We have only covered one request from new App Store Server API, other requests are signed and decoded in the same way. Apphud already started implementing new API and new Apphud iOS SDK with StoreKit 2 support will be released soon.

Stay tuned!

Leave a Reply

Your email address will not be published. Required fields are marked *