What’s new in Apphud: Paywall Analytics, Stripe Web Payments Support and Xiaomi SDKLet’s see
Apphud
Why Apphud?PricingContact
Alex
Alex
June 18, 2021
9 min read

App Store Server API in Action

We continue our set of articles and talk about changes in REST API related to In-App Purchases.

App Store Server API in Action

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

Now, we will continue our series of articles from this conference and will talk about changes in the REST API that are related to in-app purchases.

The App Store Server API is a new REST API, which lets you get information about all of a 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 the original transaction ID, and both requests and responses are signed using the JWT and API Key generated from App Store Connect.

App Store Connect, as you probably remember, is a rebranded name of iTunes Connect, which Apple released in 2018. It has all the same functions. Moreover, App Store Connect gives developers an opportunity to manage their applicants, take a look at performance insights, get insight into sales reports, gain access to app analytics, and much more.

Both of theseApple App Store APIs, App Store Server API and App Store Connect API,make a lot of processes much easier and allow you to manage your app in the most efficient way and boost in-app purchases.


Generating In-App Purchase API Key

Generating an in-app purchase API key is the same as generating a Subscription Key – the tab has simply been renamed.

Generating key for In-App PurchaseGenerating key for In-App Purchase

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

Users and Access > Keys > In-App Purchase

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


Issuer ID

To create requests, you will also need an Issuer ID, which can be found at Keys > App Store Connect API. 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 in from the owner’s account.

Issuer IDIssuer ID

Creating a JWT

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

Generating a token is done using 3 steps:

  • Creating the JWT header
  • Creating the JWT payload
  • Signing the JWT

The header has three fields:

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

Where alg and typ are 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"  
}  

Where

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.

You can find more information about JWT payloads 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 oldest to newest. If there are more than 20 transactions, the 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 a code example in Ruby.

Let's create the 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 a 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, called 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 the JSON with JWT signed fields.


Decoding Response

To decode a response, we need a Public Key. You can extract it 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 a 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 the new API.

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

Conclusion

The new App Store Connect API provides more information for developers and works much faster due to the absence of the large base64 receipt parameter that previously had to be sent to the in-app purchase server.

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 are available, like managing refunds from the app.
  • Transactions are already sorted in the API.

Disadvantages:

  • A 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 the new App Store Server API, other requests are signed and decoded in the same way.

Stay tuned!

Alex
Alex
CTO at Apphud
Software architect and engineer with 12+ years experience. More than 50 projects are in the background with 5 successful exits.