Discover the latest updates to managing in-app purchases on your server. Explore how you can use servers to track status changes, handle refunds, and manage subscriber status. Learn about App Store server APIs around status and in-app purchase transactions, and find out how App Store server notifications can help you track more customer lifecycle events. We'll also take you through managing Family Sharing for in-app purchases, and the latest improvements to testing in-app purchases in the sandbox environment.
Hello, and welcome to WWDC. I'm Tori, and I'm so excited to talk to you about the new features we have coming for your server and to help set some guidelines for running an effective server to track the status of all of your in-app purchases. So, let's dive right in.
This session is part two of a three-session series focusing on in-app purchases. If you've not already watched "Meet StoreKit 2" or "Support customers and handle refunds," I recommend you take a look after this session so you can get the full story.
For this session, we're going to focus on the server and how you can build your server to manage your in-app purchases. To get us started in this session, let's first talk about some of the reasons it's useful to have a server. Having a server is useful for several reasons, and, in the case of in-app purchase, most of them revolve around tracking status. When you have a server, we're able to notify you in real time when the status of one of your in-app purchases changes through App Store server notifications, and you can call us on-demand to check status at any time using server-to-server APIs. Having a server allows you to validate customer access to your content, even if their device is offline or if the status changes outside the app, so you know if your customer is still subscribed after a renewal or if the coins they bought in your game have been refunded. If you already have a server, you may have set one up for some of these reasons. If you don't have a server and are thinking of building one, these are strong reasons to consider, as they give you more control of your content. Even when you have a server, our story still begins on an iPhone, iPad, or other device with an in-app purchase. When you send information about that purchase to your server, including things like the transactionId, originalTransactionId, and the receipt, you're now able to track that purchase from your server by communicating directly with our server. Today, this involves using APIs like verifyReceipt or frameworks like App Store server notifications. We only want to make integrating with our server even better for you, which brings us to today's content. I'll review all of the changes we have coming on the server side and how you can integrate with these to build a better, stronger server. To start with, I will go through validating access with App Store Receipts, tracking status with App Store Server APIs, then I will dive into how you can track status passively with App Store server notifications.
I will also go over what this means for managing family sharing and how you can test your server in sandbox.
Let's get started with validating status using receipts. Today, our receipts are in the unified app receipt format. To get the JSON version of the receipt, you either have to do an on-device receipt verification in your app or, since we're talking about the server, call our server-to-server verifyReceipt endpoint. When you call us server-to-server, you get this decoded receipt, plus any new transactions in a latest_receipt_info section, upcoming renewal information in a pending_renewal_info section, and a latest_receipt. This receipt can be huge, and it contains transaction from your entire app, whether they are non-consumables, consumables, subscriptions, or non-renewing subscriptions. This provides you with a ton of information, but we wonder if it can be too much. Additionally, with StoreKit 2, we're introducing new signed transactions in a JWS, or JSON web signature, format on the client side, and we want to provide the same thing to you on the server. Why did we decide to introduce signed transactions? At Apple, we care about security. Using JWS to sign these transactions will increase security through signing and signature verification. Additionally, the transactions are easy to decode and to verify, so much so that you can do it on your server without having to call us. Let's take a look at these signed transactions now. Our signed transactions consist of three strings separated by a period. The first string is a base64-encoded JSON header, then a base64-encoded JSON payload, followed by a signature. If you base64 decode the header, it contains the signing algorithm we used, as well as an x5C claim. This contains the certificate chain you need to verify the signature. We'll get back to verifying the signature in a bit. Next, if you base64 decode the payload, you'll see the receipt JSON. That means all you need to do to decode the transaction is base64 decode the payload, a simple operation that you can do on your own on your server. Let's take a quick look at the decoded transaction. Just glancing at it, you may notice that some data types have changed from strings in the previous receipt to more appropriate data types, like numbers or booleans. Also notice that we have reduced date formats to only one, milliseconds since epoch. We've also added a few new fields. We've added a field called "type," which tells you the content type the transaction applies to. We also added a field called "appAccountToken." When you provide this value to StoreKit at buy time in your StoreKit 2 app, we persist it on the server to return it in each of your transactions. We will also return this not only in the new signed transactions, but also in our existing unified app receipt for each transaction. The next two fields I want to call out here aren't really new, but rather renamed. We have renamed cancellation_date and cancellation_reason to revocation_date and revocation_reason to make it more clear that the presence of these fields indicate that service should be revoked, as of the revocation date. These last two fields may look new but are really a simplification of some information from our previous receipt. We've combined isTrialPeriod, isIntroOfferPeriod, promotionalOfferIdentifier, and offerCodeRefName into offerType and offerIdentifier. offerType tells you what type of offer your customer has applied to this period, with 1 for an intro offer, 2 for a subscription offer, and 3 for an offer code. If the offer type is 2 or 3, you'll also see a value in the offer identifier field with either the promotional offer ID or the offerCodeRefName. Now, I want to talk about verifying the signature portion of the signed transaction info. Verifying the signature is an option for you to validate that the transaction came from Apple and is trustworthy. If you only want to see the contents of the transaction, this step is not required. However, to verify the signature, you will need to use the claims available in the header portion of the signed transaction info. Use the alg claim to know what signing algorithm we used, and use the certificate chain in the array in the x5c claim.
Once you have these two things, you can use your favorite cryptographic library to verify the signature of the signed transaction info. So that covers our changes for App Store Receipts, or, as we call them now, signed transactions.
Now, let's move on to how you can check status with APIs. So while you don't need an API like today's verifyReceipt to verify the validity of your signed transactions or to decode the transactions, we still wanted to build APIs that would help you on your server. That is why we're introducing a brand-new library of App Store Server APIs this year at WWDC that will provide you with some new features previously unavailable to you on your server and will also make use of our new signed transactions. So we're going to talk about two brand-new APIs right now: the subscription status API, and the in-app purchase history API. First, I wanna talk about the subscription status API. The subscription status API provides the latest status of your auto-renewable subscriptions, indicated by an originalTransactionId from your app. With this API, you'll get a quick answer as to your subscriber's status. You'll quickly know whether their subscription is active, expired, in a grace period, or other states, with one simple check. Let's take a look at it now. The request to this API is simple, requiring only an originalTransactionId in the URL. The response from this API contains a status for every subscription your customer is subscribed to in your app, grouped by a subscriptionGroupIdentifier. For each subscriptionGroupIdentifier, we provide a list of the latest transactions, with an entry for each originalTransactionId in the subscription group. Each entry in this array contains a status, the originalTransactionId, the signedTransactionInfo, and a signedRenewalInfo, also signed in a JWS format. Let's take a closer look at that status field now.
The status field will give you a quick answer as to the status of your subscription so you can know whether to unlock service for your subscriber. We're starting with five possible values for status: 1, meaning that the subscription is active; 2, meaning that the subscription is expired; 3, meaning that the subscription is in a billing retry period; 4, meaning that the subscription is in a grace period; and 5, meaning that the subscription access has been revoked due to a cancellation or some other event. Looking at the status field gives you a quick answer about your subscription. For more information on that status, you can look at the payload of the signed transaction info and the payload of the signed renewal info. To decode the signedRenewalInfo, follow the same steps as you would for the signed transaction info by base64 decoding the payload portion.
You can additionally verify the signature of the signedRenewalInfo in the same manner, using the header. Once decoded, you will see something like this. The renewal info contains the same fields we offer in the pending renewal info section of verifyReceipt today with some updates such as including only one date format and making some fields booleans or numbers where applicable. We will also be adding our new fields offerType and offerIdentifier to the signedRenewalInfo. This will let you know if the customer plans to redeem an offer at their next renewal. In addition to the subscription status API, we want to provide a way for you to get all of the transactions associated with your app, much like we provide in the latest_receipt_info section of verifyReceipt today. For this reason, we're also adding an in-app purchase history API. The in-app purchase history API will provide the history of all transactions for your app, much like you receive in the latest_receipt_info section of verifyReceipt today. The key difference here is each transaction will be in the new signed transaction info format, and the API will be paginated to control the size of the response you receive from the App Store. The initial request for this is, like the subscription status API, quite simple. We require only an originalTransactionId from you to process your request.
In the response you'll receive app metadata, like your app's Apple ID and bundle ID, and an array of the latest 20 transactions for your app in our new signed transaction info format. We return 20 signed transaction infos to you per request. If you have more transactions, look to the hasMore and revision values in the response. hasMore will be true if there are more transactions remaining for your app. In this case, make another request, passing the revision token as a query parameter, to get the next 20 transactions. Repeat this until hasMore is false. Now let's pivot and talk about how all of the App Store Server APIs will be consistent with each other. They will all be behind JWT — or JSON web token — authentication, support our new signed transactions, and feature JSON request and response formats. And best of all, they all key off of an originalTransactionId that you provide in the request, rather than requiring a receipt and a shared secret in the request. Now, I want to cover JWT authentication. All of our new App Store Server APIs will make use of JSON Web Token, or JWT, authentication. We chose this to increase the security of communication between our server and yours. To generate this JWT, you will need to download a private key from App Store Connect. This process will automatically register the public key with our server. Then you must sign the token using the ES256 algorithm before calling our server. To generate your private key in App Store Connect, navigate to the Users and Access page and visit the Keys tab. Select the in-app purchase keys option, and you'll see a page like this. Add a key and give it a name. Save the key in a safe place, as you can only download it once, and take note of the key ID. Now, let's take a look at what this JWT actually looks like. A JWT consists of three parts: a header, a payload, and a signature. In the header, you should include the key ID of your private key and the algorithm used for signing. We require an elliptic curve signature with a SHA 256 hash, or ES256. You will also include the type of the token, which, in this case, is always JWT. The payload should include your issuer ID. You can find this value in App Store Connect. You will include the time the token was issued and the time it should expire, in seconds since epoch. The difference between these two times should be no more than an hour. Include the audience, which is always appstoreconnect-v1.
You'll have to generate a nonce, or a one-time unique string. Finally, you'll have to include the bundle identifier of your app. Once you have all of this information, you have to implement the signing of this token using the ES256 algorithm, or an elliptic curve signature with a SHA 256 hash. Before I move on, let's review the key takeaways of our App Store Server APIs. First, we've separated determining status from looking up the history of transactions, as these are separate functions. Next, these APIs require only the originalTransactionId in the request, meaning that you can take the signed transactions you receive, either from your app or from a response from our server, store the fields you're interested in, including originalTransactionId, and then get rid of the signed transaction info. There is no need to store signed transactions anymore as we have guided you to do with receipts in the past. So that covers how you can check your customer status with our new App Store Server APIs. Now, I want to go over how we are making our App Store server notifications consistent and how you can track status using notifications. Let's first start with a quick review of App Store server notifications. We've discussed App Store server notifications for a few years now, so let's review why they're useful. With App Store server notifications, you can receive notifications when the status of one of your transactions changes directly from the App Store. When you receive the notification, you can update your status immediately, without your customer having to open the app on their phone. With App Store server notifications, you also don't need to call us for status. We'll just tell you when something changes. They are one of the most powerful tools your server can take advantage of. Our goal for this year is to make App Store server notifications even more powerful by making use of our new, easy-to-use signed transactions. In addition to this, we will update the notifications to make sure only one notification is sent for one user action, we will update the payload, and the entire payload will be signed using JWS to enhance security. We'll also allow you to opt in to the v2 notifications when you're ready and will continue sending the existing notifications for some time. This is our current notification offering for v1 notifications. There are 11 total types, including everything from INITIAL_BUY to REVOKE. With v2 notifications, we're deprecating four of our notification types: INITIAL_BUY, INTERACTIVE_RENEWAL, CANCEL, and PRICE_INCREASE_CONSENT. But we're adding five new types: SUBSCRIBED, OFFER_REDEEMED, EXPIRED, GRACE_PERIOD_EXPIRED, and PRICE_INCREASE. In addition to the new notification types, we're adding a new field called "substate" to the notification. This will help you narrow a more general notification type to a specific user action. Currently, substates apply to six of our v2 notification types: SUBSCRIBED, DID_CHANGE_RENEWAL_STATUS, DID_CHANGE_RENEWAL_PREFERENCES, OFFER_REDEEMED, EXPIRED, and PRICE_INCREASE. Let's take a look at some examples of how substates apply to these notification types. First, I want to talk about the SUBSCRIBED notification and its substates. When a customer makes a first-time purchase, you will receive SUBSCRIBED with a substate of INITIAL_BUY. When a customer resubscribes to the same SKU or a different SKU, you will receive SUBSCRIBED with a substate of RESUBSCRIBE, as long as the subscription is within the same subscription group. One of our new notification types without an equivalent type in v1 App Store server notifications is the OFFER_REDEEMED notification. So I want to take a look at this example. OFFER_REDEEMED is received whenever a customer redeems a promotional offer. If the customer redeems an offer for a first-time purchase, you'll receive OFFER_REDEEMED with a substate of INITIAL_BUY. If the customer redeems an offer to resubscribe to the same inactive subscription, you'll receive OFFER_REDEEMED with a substate of RESUBSCRIBE. If the customer redeems an offer to upgrade their active subscription, you'll receive OFFER_REDEEMED with a substate of UPGRADE. If the customer redeems an offer to downgrade their active subscription, you'll receive OFFER_REDEEMED with a substate of DOWNGRADE. Additionally, if the customer redeems an offer to resubscribe to their active subscription after canceling within the same period, you will receive OFFER_REDEEMED with a substate of AUTO_RENEW_ENABLED.
Now, let's look at EXPIRED. With the new EXPIRED notification type, you'll receive EXPIRED when the subscription expires after the customer has disabled auto renew with a substate of VOLUNTARY. If a subscription expires because the billing retry period has ended without a successful recovery, you will receive EXPIRED with a substate of BILLING_RETRY. Additionally, if a subscription expires because the customer has not consented to a price increase, you'll receive EXPIRED with a substate of PRICE_INCREASE. So combining the v2 notification types, plus their applicable substates, we now cover over 20 different customer life cycle events. Just looking at the notification type should be enough to get a general idea of what has changed in your purchase, but looking at the substate will help you get a more specific state if you want to go into more detail. Now let's take a quick look at the new payload. For v2 notifications we will always include the same set of fields, regardless of the notification type. The notification type, the subtype, the notification version, which will be 2 if you subscribed to v2 notifications, the environment the notification applies to, some app metadata like bundle ID, the app Apple ID, and bundle version, the latest transaction for the affected in-app in our new signedTransactionInfo format, and the latest renewal info for the in-app in our new signedRenewalInfo format. These changes will make the notifications easier to parse and hopefully easier to adopt as they make use of our new signed transactions and only contain information about the affected in-app purchase. As I mentioned earlier, the entire payload will be signed to increase the security and trustworthiness of our notifications. The payload we just saw is unsigned for readability, but the signing will be similar to how we are signing transactions and renewal info in a JWS format. We want you to be able to opt in to v2 notifications when you're ready. For this reason, we're adding an option to the notification URL in App Store Connect to allow you to select your App Store server notification version. To do this, go to your App's page and scroll to the new App Store Server Notifications section. If you select the production server URL, you now have the option to choose version 1 or version 2 App Store server notifications. When these changes launch later this year, you'll be able to opt in to version 2 App Store server notifications. So now, I want to go over some example scenarios using our new App Store server notifications, starting with the first-time purchase of a subscription. For a first-time subscription purchase in your app, you'll receive a signed transaction info as a result of the purchase. You can choose to verify this on your app and send the originalTransactionId and other relevant fields to your server or send the signed transaction info to your server for verification and choose which fields to store in your database at that time. Around the same time, you'll receive a SUBSCRIBED notification with a substate of INITIAL_BUY. Now that the signed transaction info in the notification contains the app account token, you can immediately link this notification to your in-app user, even if communication is lost between your server and your app after the purchase. There's no need to call our server to verify the signed transaction info. You may call our server at any time if you wish to check the status or in-app purchase history API by sending us the originalTransactionId. Now I've covered purchasing a subscription. Let's move onto subscription renewal. Now we've reached the renewal of this subscription. If this subscription renews successfully, you will receive a notification type DID_RENEW. You can look at the signed transaction info and the signed renewal info in the payload to verify the next renewal date of your subscription and your customer's renewal preferences for their next renewal. You can also schedule a job to call our subscription status API to check the status of your subscription at its renewal time as a fail-over mechanism. Once again, there's no need to call us to verify the transaction you receive in the notification. Of course, auto-renew doesn't always go according to plan, especially if there is a billing issue. So now, I want to cover grace period and billing retry. Now let's suppose that your subscription did not renew as expected. When this happens, we notify you with a DID_FAIL_TO_RENEW notification. If you have grace period enabled and the subscription exits the grace period without renewing successfully, we send you a GRACE_PERIOD_EXPIRED notification, and you can know that your customer has entered the billing retry period. If the subscription still is not recovered during the billing retry period, we'll send you an EXPIRED notification with a substage of BILLING_RETRY. If we recover the billing of the subscription during the grace period or the billing retry period, we'll send you a DID_RECOVER notification. No matter the outcome of the renewal, we notify you of the result with a v2 notification, containing a signed transaction info and signed renewal info. You can call the subscription status or history API at any point in this process to double check your subscription status. Now, we realize subscriptions are not the only thing customers will purchase in your app. So now let's pivot and cover what to expect during a first-time purchase of a consumable. For a first-time purchase for a consumable on your app, you'll receive a signed transaction info as a result of the purchase. You can choose to verify this on your app and send the originalTransactionId and other relevant fields to your server or send the signed transaction info to your server for verification and choose which fields to store in your database at that time. Keep note of the originalTransactionId always, as you might need it later. For consumables and other content types like non-consumables and non-renewing subscriptions, not much changes over the life cycle of that purchase unless the customer requests a refund. So I want to cover that case now. Now, suppose your customer requests a refund for their consumable purchase. We'll send you a REFUND notification, containing the revocation date and revocation reason in the signed transaction info. You can know to stop providing access to the consumable purchase after the revocation date. Should you be concerned about the status of your consumable purchase at any time, you can call the in-app history API and look for it in the response. Canceled consumables will always be included, so you will know if the transaction status has changed. Now I want to talk about outages. Sometimes, despite best efforts, you may experience an outage on your server. Now I'll cover how you can help your server recover from an outage. If you experience an outage on your server and you miss App Store server notifications, you'll want to know what has changed in the interim. The in-app history API is your solution here. Simply call the API for each customer, providing any originalTransactionId from your app, and you'll get the latest history of transactions for your app so you can update your server. You can then call the subscription status API to get the latest subscription status for each of your subscriptions. Now I want to cover one final case-- migrating to signed transactions on your server. This is especially important if you'll be ready to update your server before your app or if you're still receiving the unified app receipt from older versions of your app. Migrating to signed transactions on your server is easy, as it only requires an originalTransactionId. You can easily convert the unified app receipts your server receives from your app to JWS receipts so your server can be compatible with our App Store server APIs and App Store server notifications. To do this, first call verifyReceipt with the unified app receipt and pull all of the unique originalTransactionIds from the response. Call the in-app purchase history API for one of these originalTransactionIds to get the history for your app in signed transactions. Then call the subscription status API for a subscription originalTransactionId to get signed transactions and signedRenewalInformation for all of your customer subscriptions. Write down any relevant data from the payload of the signed transaction, and you're all set to continue using these APIs and to receive v2 App Store server notifications. So now I've covered all of the changes we have coming for App Store Server Notifications and how you can use notifications to check your customer status.
Now I want to talk about how we are making it even easier for you to manage family sharing for in-app purchases from your server. Family sharing for in-app purchases is currently supported for auto-renewable subscriptions and non-consumable purchases, if you have enabled family sharing for that in-app purchase in App Store Connect. Right now, we provide a field called inAppOwnershipType to indicate if a transaction is family shared or purchased, and we support a subset of notifications for family members: REVOKE, DID_RECOVER, and DID_FAIL_TO_RENEW. The in-app ownership type field and the existing supported notification types will remain with our new signed transactions and App Store server notifications v2. However, coming later this year, we're adding more support for App Store server notifications for family members. For v1 notifications, we're adding DID_CHANGE_RENEWAL_STATUS, DID_CHANGE_RENEWAL_PREF, DID_RENEW, and INTERACTIVE_RENEWAL. For v2 notifications, we're adding even more support for family members. In addition to DID_CHANGE_RENEWAL_STATUS, DID_CHANGE_RENEWAL_PREF, and DID_RENEW, we're adding support for SUBSCRIBED, EXPIRED, GRACE_PERIOD_EXPIRED, and OFFER_REDEEMED for purchasers as well as for family members. This will make it even easier for you to track the status of all your customers, both purchasers and family members, through App Store server notifications. So with the changes we have coming for notifications for family members this year, this should make managing family sharing for in-app purchases even easier for you when coupled with our existing family sharing functionality. Now I want to wrap up with one more thing: testing your server in sandbox. We want you to feel confident in your app and your server. So we want you to be able to integrate with our new App Store Server APIs and App Store server notifications in sandbox before production. For the App Store Server APIs we discussed today, that means that they are fully testable in sandbox, and live, starting now! This includes the subscription status API and the in-app purchase history API. In addition to this, we're adding a few other new features in sandbox. Coming later this year, you will be able to add a sandbox-specific notification URL in App Store Connect. With this addition, you can keep your production and sandbox notifications completely separate. Additionally, you'll also be able to choose your sandbox notification version so you can test v2 notifications in sandbox before production. Last year, we brought you some exciting sandbox improvements, like resetting trial eligibility and providing a Manage Subscriptions page in sandbox. We want to continue making testing in sandbox easier and are adding a few new enhancements this year. These are clearing purchase history for a sandbox Apple ID, changing sandbox account region, and adjusting subscription renewal rate in sandbox. Additionally, as a security enhancement, we are returning an error from verifyReceipt for TestFlight receipts when we detect the customer is no longer a TestFlight user. These new sandbox enhancements will be accessible from the Sandbox Testers page in App Store Connect. To clear purchase history, select edit, then toggle a tester, and select the clear purchase history button. Once you confirm clearing purchase history for a tester, the action cannot be reversed. So remember the testers you've selected this option for. Clearing purchase history is a powerful new testing tool for you that enables you to purchase something again without creating a new account. It also allows you to have a fresh, empty receipt for testing. To change account region or adjust subscription renewal rate, navigate back to the testers page and select a tester row.
In the tester settings, you will see the new options to change App Store region and to adjust subscription renewal rate.
You can change the account region for a tester by selecting the desired region. This makes it possible to test in 175 storefronts in sandbox, all with one tester account. Our final new sandbox feature is adjusting your subscription renewal rate in sandbox. To edit this, select your desired renewal rate from the drop-down. Right now, a month correlates to 5 minutes in sandbox. We'll give you some more options to adjust the renewal rate for your testers. Adjusting the subscription renewal rate gives you more time to do things like cancel, upgrade, or downgrade a subscription, and it allows you to rapidly speed up renewals to simulate longer-term customers. That is everything we have coming to sandbox this year. We hope you love testing with these new features. So we've covered a lot of new information today, and now I want you to be able to explore all of it. I hope you take some time to update your apps and servers to adopt our new JWS receipts. Make use of our new App Store Server APIs, especially in sandbox, where they are live right now. And enroll in App Store server notifications if you haven't already and get ready for the v2 update coming later this year. Our new sandbox enhancements are also coming later this year. Make use of these to enhance your sandbox testing experience. Finally, check out the other two sessions from this year in this series, "Meet StoreKit 2" and "Support customers and handle refunds." For more background on App Store server notifications and how you can set them up, check out "What's new with in-app purchase" from WWDC 2020 and "In-app purchases and using server-to-server notifications" from WWDC 2019. Our receipts, APIs, and notifications are three powerful tools you have to manage your in-app purchases from your server. By taking advantage of these, you can make your server and your app more powerful than ever. Please take advantage of all the new features we've gone over today, and we look forward to hearing your feedback. Thanks so much for listening today, and enjoy the rest of WWDC. [upbeat music]
Looking for something specific? Enter a topic above and jump straight to the good stuff.
An error occurred when submitting your query. Please check your Internet connection and try again.