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 an in-app purchase API key is the same as generating a Subscription Key – the tab has simply been renamed.
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.
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.
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:
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.
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.
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.
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:
original_transaction_id
is enough.Disadvantages:
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!