iOS Subscriptions are Hard
The unreasonable difficulty of implementing iOS subscriptions.
Subscriptions provide developers a better way to make money. Building an audience of paying subscribers not only generates more revenue, but it offers better customer-developer alignment and leads to better apps.
Apple’s StoreKit lacks some of the basic functionality for implementing subscriptions correctly. It is up to the developer to fill this gap.
What’s missing?
Apple bolted on Subscriptions to the existing in-app purchase infrastructure that introduced almost ten years ago. Doing this was not a great start. StoreKit assumes a transactional, user-driven purchasing experience. For subscriptions, purchases are only transactional initially. Beyond the first transaction, they occur automatically, recurring without user input.
StoreKit’s queue and receipt mechanism is clunky for handling renewing transactions. At some point, before a subscription renews, an SKPaymentTransaction will appear on the StoreKit queue, unprompted by any user action. This unprompted appearance makes handling the queue much more complicated. Users can switch devices, receipts can be stale, and transactions can go unhandled. Unexpected transactions create an unmanageable number of cases that you need to consider to provide a user with their paid content.
What’s a developer to do?
There are two ways to handle subscriptions in your iOS app:
- Doing everything on the device (device-side)
- Doing receipt handling on a server (server-side)
Device-Side iOS Subscriptions
Implementing subscriptions without a server is possible. Device-side means your app implements some form of the following:
- Normal IAP flow
- On-device validation of the receipt file
- Local storage and tracking of the latest expiration_date for a given product.
- Handling of renewal transactions
Let’s go through each of these.
Purchasing using normal IAP flow
Besides different user interface copy, purchasing a subscription product is identical to purchasing a non-subscription in-app product. The difference is that subscription products will renew, generating unsolicited transactions on the StoreKit queue at a later date. Subscription in-app transactions also have an expiration_date field in the receipt data. You will use this field to determine what products or services to which a user is entitled.
On-device Receipt Validation
To protect yourself from IAP piracy and to extract a user’s transaction history, you need to validate your App Store receipt. Without a server, you do this on the device. I won’t go into the whole process here, but I’ve written about it in the past.
Apple doesn’t provide built-in methods for verifying a receipt file. They argue that those built-in methods would just become the target of IAP crackers. Instead, they recommend everyone roll their own, ensuring a standard IAP cracker can’t defeat your application. However, the code for verifying a receipt file on the device isn’t exactly trivial.
Parsing out the expiration date
Once you’ve unpacked the receipt file, you are only interested in the latest expiration_date. You extract this by looping through all in-app purchases records in a receipt and finding the latest expiration_date field, then caching it, usually in NSUserDefaults or something similar.
Handling renewal transactions
Handling renewals is a common area for mistakes. In a standard IAP flow, you only need to worry about the status of the StoreKit payment queue while a user is completing a purchase. With subscriptions, your app needs to be prepared to handle these transactions at any time. Now you need to handle purchases from literally anywhere in the app; the effect of a user’s subscription status changing needs to be considered on every screen.
The appropriate way to handle it is: as soon as it appears on the StoreKit queue, validate the receipt, and update a user’s entitlements. However, this can be difficult. For example, if your application has an account system, how do you handle a transaction that occurs while there is no user logged in? Or consider a transaction that occurs in the middle of some flow that depends on a user’s subscribed state. You have to plan for all of these possibilities when designing your app.
The gaps in device-side subscriptions
Device-side receipt handling makes a few important things difficult or impossible.
Firstly, your subscription status is trapped on the device. If you ever want to expand your service beyond your current app, you will need to design some elaborate escape plan for the current subscription status of your users.
Limiting subscription handling to the device also makes it difficult to understand the performance of your business. iTunes Connect has gotten better but is still lacking if you ever want to understand anything on a user-by-user basis. All of Apple’s dashboards are fully anonymized. Aggregate metrics are fine if all you want is a bird’s eye view, but you will quickly be unable to answer even simple data questions using Apple’s dashboards.
I think the biggest reason to avoid a device only subscription implementation is just being at the mercy of the StoreKit queue. If for some reason there is a hiccup either in your code or in StoreKit, you could miss a transaction. This would potentially deprive a paying customer or their service. If you are using only device side subscriptions, you may have a hard time debugging or remediating the situation.
It really makes sense to use a server.
Subscriptions with a Server
Using a server means: rather than parsing a receipt on the device, you send that receipt to a server for validation and parsing. On the device, implementation is similar to that of device-side subscriptions but with a couple of key changes:
- Normal IAP flow
- The receipt is sent to a server where it validated, parsed, stored, and the data returned
- The server response is stored on the device
- Handling of renewal transactions (semi-optional)
Sending the receipt to a server
Step 2 represents the most radical difference from device-side subscriptions. Rather than implementing receipt parsing and validation in the app, you send the receipt over HTTP to your server. Doing this has two significant advantages:
- You can use Apple’s /verifyReceipt endpoint
- You can keep the receipt file server-side to be used as a refresh token for a user’s subscription status.
Being able to use Apple’s provided endpoint to validate a receipt will save you a lot of time. It also provides something that device-side receipt validation cannot: a list of the most recent transactions.
When you parse a receipt device side, whatever transactions are in the file are what you get. When you use /verifyReceipt, Apple sends along a list of the most up-to-date subscription transactions. The receipt file acts as a fetch token to poll data from Apple. This polling is essential for building a complete subscription server.
Polling and Status Notifications
One of the advantages of storing receipt files server side is being able to keep a user’s subscription status up to date. To do this means polling the /verifyReceipt endpoint and using Apple’s new status update notifications. Setting this up can add significant complexity to your backend. You now need a job to check receipts periodically and to be able to handle POSTs from Apple. Also, how often do you poll?
When a user has an active subscription, the answer is relatively simple. You can start checking when they are close to renewing, but you will miss out on returns. The complexity comes once a customer cancels a subscription. There is a possibility that, at any time in the future, they resubscribe. If they do this in your app, you will be able to send a new receipt and tell your backend to update their status. However, a user can also resubscribe from the App Store settings page. You need to rely on some combination of polling and the status notifications to have complete knowledge of a user’s subscription status.
What to store server-side
Once you’ve received and validated a receipt, what to store on the server is up to you. The more you can save the better, but it depends on your needs. The simplest implementation is just to store the latest expiration_date, providing you with a quick way of determining a user’s subscription status.
However, only storing the latest expiration cuts you off from a lot of the interesting information a receipt provides. A better implementation would be storing the complete transaction history of a user. Storing the complete history gives you the ability to understand more complex subscriber behaviors like average subscriber lifetimes, cancellations, and the evolution of your paying subscriber base day-by-day.
Metrics
Without a server holding your receipts, getting good metrics is very difficult. If you plan on understanding your LTV and perhaps making some decisions about user acquisition spending you need to know the dollar amount that each user is spending. Tracking a user’s total transaction history enables this.
You can try to do this on top of existing analytics infrastructure. Event tracking services like those integrated with Segment can typically track StoreKit transactions, but you are at the mercy of the behavior of your app, your user’s device, and whichever 3rd party tracker you are using. When you are trying to determine if you are breaking even on ad spend, you want to eliminate as much systematic error as possible and event-based trackers are full of it.
Having a system that parses the user’s entire transaction history and attaches dollar values to each transaction gives you the basis for answering any monetization questions you have and in the most accurate manner possible.
Customer service
The last big advantage that server-based subscription tracking provides is related to customer support. Supporting Apple subscriptions customers can be painful.
Developers lack programmatic access to a user’s account history and the ability to modify or cancel their subscription. Often our best course of action is to tell the user how to contact Apple, or send them instructions for managing their subscription via App Store settings. With a server, we can do better.
Having a user’s entire history accessible via your backend, you are much more equipped to understand the user’s situation. IAP transactions fail in weird ways, user’s get confused, and emotions can run high when money is involved. Having a user’s entire subscription history at your fingertips will give you a much better starting point for understanding a customer’s issue.
With a server as your source of truth for a customer’s subscription, you are empowered to resolve more support tickets than if everything were device-side. Adding “support” transactions to a user’s history allows you to grant a user a subscription for as long as you’d like. This requires some work on the back end but can really pay off in avoided 1-star reviews.
Use a server
If there is a possibility that the app becomes a serious money-making endeavor, having a server tracking your subscriptions from day one will make your life easier.
The server features described above represent a substantial amount of work. If you only have time to do one thing, I suggest using a server for receipt validation and storing the receipt file. Having the receipt file will enable you to build everything else mentioned at a later date. If your receipts are trapped on the device, your options are limited.
If you want to get up and running with subscriptions faster and have access to all the features mentioned above, you might try RevenueCat. Our mission is to fill the gap left by Apple and other platforms and provide a service for developers to get off the ground quickly with a great subscriptions implementation.
You might also like
- Blog post
Implementing in-app purchases and monetizing your Roku app: A step-by-step guide
A comprehensive guide to implementing and monetizing your Roku channel, from choosing the right strategy to technical tips and integration best practices.
- Blog post
How we built the RevenueCat SDK for Kotlin Multiplatform
Explore the architecture and key decisions behind building the RevenueCat Kotlin Multiplatform SDK, designed to streamline in-app purchases across platforms.
- Blog post
Inside RevenueCat’s engineering strategy: Scaling beyond 32,000+ apps
The strategies and principles that guide our global team to build reliable, developer-loved software