DEV Community

Surik Sarkisyan for Qonversion

Posted on • Edited on • Originally published at qonversion.io

WWDC22 overview: how to integrate and migrate in-app purchases to App Store Server API

Hey everyone, today we’re going to continue our WWDC22 blog post series on what’s new with in-app purchases. We’ve already covered some of the updates: enhancements to StoreKit 2 and App Store Server API.

Today, we’ll focus on the next updates that were announced during the session Explore in-app purchase integration and migration. The session is divided into two chapters – App Store Server API and App Store Server Notifications Version 2.

In this article, I’ll mostly refer to the first part of the video. We’ll talk about App Store Server API – powerful, secure, and with efficient server-to-server endpoints, then we’ll explain how to migrate to it.

The main idea behind this API is pretty clear: to give you the all data you need about in-app purchases.

App Store API was introduced on WWDC 2021. These are two great sessions that I highly recommend to watch; however, they won’t affect your understanding of today’s topic.

What we will cover:

  • How to use the App Store Server API
  • How to sign JSON Web Tokens
  • How to verify signed transactions
  • How to migrate from verifyReceipt So, let’s get going!

Using the App Store Server API

In this part, we’ll cover the cases of Using the App Store Server API. Additionally, I’ll cover how to use the new API with StoreKit in different scenarios: when you need to support Original StoreKit, StoreKit 2, and both of it – for instance, if you decided to keep supporting StoreKit for users with previous iOS versions and add StoreKit 2 support for those who use updated versions of Apple OS.

Which endpoints are available with the new API?

  1. GET /inApps/v1/subscriptions/{originalTransactionId} – get the statuses for all of a customer’s auto-renewable subscriptions in your app.
  2. GET /inApps/v1/history/{originalTransactionId} – get a customer’s in-app purchase transaction history for your app.
  3. PUT /inApps/v1/transactions/consumption/{originalTransactionId} – send consumption information about a consumable in-app purchase to the App Store after your server receives a consumption request notification.
  4. GET /inApps/v1/refund/lookup/{originalTransactionId} – get a list of all refunded in-app purchases in your app for a customer.
  5. PUT /inApps/v1/subscriptions/extend/{originalTransactionId} – extend the renewal date of a customer’s active subscription using the original transaction identifier.
  6. GET /inApps/v1/lookup/{orderId} – get a customer’s in-app purchases from a receipt using the order ID.

The first 5 requests require originalTransactionid as an input parameter, which allows you to easily use these requests just having originalTransactionId. You can receive this parameter from receipts, signed transactions, signed auto-renewals, and notifications.

The 6th request requires orderId as an input parameter. But what is that parameter used for?

The main idea is to support and interact with your clients. When the customer makes a purchase, he receives an email with the data about the completed purchase. This email includes some details, and the orderId is one of them. This data could be received as well from a purchase history on the App Store.

Thus, when the user contacts your support team and sends a support query, he shares the orderId, and you receive the needed data through the Look Up Order ID endpoint.

It’s worth mentioning that Apple announced 3 more endpoints as well, but as we’ll explore these and other Apple notifications in future articles. For now, let’s just take a quick look and move on.

Here they are:

  1. POST /inApps/v1/notifications/test – Ask App Store Server Notifications to send a test notification to your server.
  2. GET /inApps/v1/notifications/test/{testNotificationToken} – Check the status of the test App Store server notification sent to your server.
  3. GET /inApps/v1/notifications/history – Get a list of notifications that the App Store server attempted to send to your server.

How to get originalTransactionIds with the Original StoreKit?

When you call the endpoint verifyReceipt, you can see that in responseBody => responseBody.Receipt.In_app field you can find originalTransactionId for each user’s purchase. The same can be found in responseBody.Latest_receipt_info and responseBody.Pending_renewal_info.

Let’s take a look at what this looks like:

Image description

With knowledge of how to get the originalTransactionId from Original StoreKit transactions, let’s explore the whole flow between a customer, the App Store Server, and your server:

  1. Obtain the app receipt on our server
  2. Take the app receipt and call verifyReceipt with it from your server.
  3. Get the decoded receipt in a response.
  4. Get all of the originalTransactionIds from the decoded receipt in precisely the same ways I previously showed.
  5. Call GET /inApps/v1/history/{originalTransactionId} with any one of the gathered originalTransactionIds.
  6. Handle history of transactions for this user in response. These transactions include non-consumables, refunded consumables, non-renewing subscriptions, and auto-renewing subscriptions.

Then, if you want to get the latest signed transaction and signed renewal information for a specific subscription, call GET /inApps/v1/subscriptions/{originalTransactionId} with the corresponding originalTransactionId. As a result, you’ll receive the list of all signed transactions and signed renewals, which corresponds to originalTransactionId.

Image description

How to get originalTransactionIds with the StoreKit 2?

On the client you can get the originalTransactionIds with the following steps:

In terms of the API side – you’ll receive the originalTransactionIds on the top-level field, as described in the screenshot below. Here you’ll see an example of a signed JWS transaction, which is the data type you receive in signed transactions and in signed renewals from the App Store Server API and App Store Server Notifications.

Image description

Then, let’s proceed with the following steps:

  1. Get the transaction on a user’s device
  2. Send the transaction to your server
  3. When you need to perform an operation on a subscription, such as extending the renewal date of a subscription, you can use the originalTransactionId from the signed transaction to call PUT /inApps/v1/subscriptions/extend/{originalTransactionId} endpoint and get back the data that you need. The screenshot:

Image description

How to handle originalTransactionIds with both versions of StoreKit?

So, we explored the ways to use the new API with the Original StoreKit and StoreKit2.

For the cases when you have to support both versions of StoreKit, the advice is quite simple: just take all the advantages of the App Store API and use them regardless of the version of StoreKit that processed the purchase.

To validate new purchases that were made with the Original StoreKit, the process is simple:

  1. Get the receipt from a customer
  2. Send this receipt to your server
  3. Call verifyReceipt endpoint to receive decoded receipt
  4. Request the status of the purchase with GET /inApps/v1/subscriptions/{originalTransactionId}
  5. Get all signed transactions and renewals It’s essentially the same as the process described before, except that, as you might notice, the history purchase step is skipped.

Image description
That’s it for App Store API!

Signing JSON Web Tokens

JSON Web Token is needed for Apple to authenticate you when calling the App Store Server API. This token should be used as an authorization header in each of your server’s requests.
JWT consist of: header, payload, and the signature.

How to construct JSON Web Token for your app?

On the picture below you can see the structure of the JWT, header and payload.

Image description

The token itself can be divided into three parts, separated by periods:

  1. The base64 encoded header
  2. The base64 encoded payload
  3. The signature, which is composed of the base64 encoded header + base64 encoded payload, signed using your signing secret.

Header

The header consists of metadata that represents the information on how to sign your data.

Pay attention to the key ID field (kid) – this is your private key ID from the App Store Connect. This key ID should correspond to the key that you use for JWT signature.

Payload

Payload keeps all the data about your application. To learn more about this field please read the official documentation from Apple.

For additional info on how to get your API key please read this article.

Once you receive all the data that you need for the JWT, you need to sign the JWT ​​using the certificate that corresponds to the key ID.

Let’s take a closer look at an example (pseudocode). Please note that the final code will look different depending on the programming language, the chosen tools, and libraries with which you will implement it. But the logic behind remains the same.

import jwtLibrary;

var privateKey = readFile(“/path/to/private_key.key”)
var token = jwtLibrary.sign(payload, privateKey, header)
One your preferred library generates token, you can paste it into cURL that will get subscription statuses by `originalTransactionId`
Enter fullscreen mode Exit fullscreen mode
This is an example:

curl -v -S -H 'Content-Type: application/json' \
    -H 'Accept: application/json' \
    -H 'Authorization: Bearer ${token}' \
    https://api.storekit.itunes.apple.com/inApps/v1/subscriptions/${originalTransactionId}
Enter fullscreen mode Exit fullscreen mode

Don’t forget to:

  • replace ${token} with the parameter that you’ve generated in example above.
  • replace ${originalTransactionId} – the Id, with which one you’d like to get the data.

Verifying signed transactions

Let’s discuss how to verify the transactions that you receive from Apple and that were signed by the App Store.

Signed transactions are JSON and are cryptographically signed, so if someone would try to replace it between App Store and your server, you can easily detect it.

These transactions are signed in the JWS format – Json Web Signature (do not confuse this with JWT, which was discussed above). By verifying this object you receive the data from the App Store and can ensure that it was not tampered by anyone.

How can I verify a signed transaction?

  1. Decode base64 header
  2. By alg claim in this base64 header you could understand which algorithm you should use. It will be used for the JWS verification.
  3. Verify certificate chain in the x5c claim Now, once we verified this data, we are on the safe side – no one tampered our data.

You can learn more about JWS here. Also, please read the App Store Server API documentation to learn more about JWSTransaction.

What is the x5c chain?

This is a chain of certificates, the successful verification of which means that the data can be trusted and is signed by Apple.

What is important for the certificate chain is the order. At the beginning you’ll get the root certificate. The root certificate may be followed by additional certificates from the chain. Each following certificate is signed by the previous one. The last certificate in this chain is referred to as the leaf certificate. The first certificate is referred to as the root certificate, and is self-signed.

This certificate should match to the root certificate you obtain from the Apple’s Certificate Authority. If it does not, then the verification chain will be defined as failed.

The last certificate (leaf certificate) is used for JWS signing.

Let’s take a look at the JWS header:

Image description

At the beginning of the first line, in the alg field, there is the name of the algorithm that is used for the JWS signing. Next, in the x5c, there is a chain of certificates listed in order.

Let’s look a bit more closely at what x5c certificate chain generation looks like.

At the beginning, we take a root certificate from the Apple’s Certificate Authority. Next, we sign the intermediate certificate. There might be several certificates in this chain, and as we discussed above, each following certificate is signed by previous one. In the example below we consider the one intermediate certificate. Then, this intermediate certificate signs the leaf certificate.

Image description

Ok, we’ve described the way this chain generates. Now it’s time to move on to the next chapter – how you can verify it.

This part is simple – you just need to reverse the process.

First, make sure that the leaf certificate is signed by intermediate one. Then, that intermediate certificate should be signed by the root certificate. And the root certificate should match the one one of Apple’s Certificate Authorities. If all steps are successful, the chain is verified and counted as valid.

Image description

Let’s discuss a specific method of chain certificate verification.

This is the command fot x5c chain using OpenSSL verification:

openssl verify -trusted AppleRootCA-G3.pem -untrusted AppleWWDRCAG6.pem leaf.pem

The command verify initiates the process of certificate verification.

With -trusted we send the certificate that we trust and that will be used for the verification of the following certificates. This should be root certificate that was obtained from Apple’s Certificate Authority.

With -untrusted we send a certificate that wasn’t verified yet, but that we’d like to verify.

As a first parameter we use the WWDR certificate that received from the Apple’s Certificate Authority and signed by root certificate. This should match to the second certificate from the x5c chain. The last parameter is a leaf certificate that was signed by the previous certificate. In case of successful verification, you’ll receive a success code, and in case of failed verification, you’ll receive an error code. If verification is unsuccessful, the data shouldn’t be used.

More information about the verify command in openssl is here.

The information on openssl is here.

Migration from verifyReceipt to App Store Server API

In this part, we’ll explore the several corner cases.

How to check the subscription status of the particular user

Previously, you had to call verifyReceipt and define the status by checking several fields from the response. Now, with App Store Server API you can call GET /inApps/v1/subscriptions/{originalTransactionId} and receive all the needed data, such as subscription status, renewal info and other pieces of data in one response.

How to receive the latest transactions of the user

The next case described is when you’d like to get the latest transactions of the user to check what he purchased, whether the subscription is auto renewed, or whether there were changes like upgrades, downgrades, or plan switches.

As in the previous example, with the previous API version you had to call verifyReceipt and get the data from the responseBody.Receipt.In_app and responseBody.Latest_receipt_info fields.

With the new App Store Server API you can refer to the GET /inApps/v1/history/{originalTransactionId} endpoint that covers all needed data.
And last but not least – the new appAccountToken field that fetches UUID. We’ve covered this field in our previous article, but just to remind you: this field lets you add the unique ID of the users from your system just to connect your user with their purchases. In the previous API version there was an analogue of this field – applicationUsername – that fetched the string, but the official documentation recommended sending UUID into this field. And once you send the UUID format string into this field, you’ll see this parameter automatically in appAccountToken. The needed value will appear in StoreKit Transaction, App Store API, and App Store Server Notifications.

Conclusion

In this article, we explored the new capabilities of App Store Server API, how to integrate and migrate in-app subscriptions into API, and how to sign JSON Web Tokens and verify signed transactions.

If you still have any questions about the logic behind these updates, please feel free to reach out to us. Qonversion provides a complete cross-platform infrastructure that allows you to make and restore purchases, validate receipts, and provide your app with an accurate subscription status without the need to build your server.

If you'd like to learn more on how to handle StoreKit errors, please follow our guide. Also, recently I've created an article on how to solve SKDomainError 0, feel free to check!

Top comments (0)