In 2017, Apple added a new, long-awaited feature to their fraught in-app subscriptions system: server-to-server notifications.
Notifications are a popular feature of most subscription billing platforms. They allow developers to rely on their billing platform for many of the event driven aspects of subscriptions (i.e. renewals, cancellations, and billing issues).
This came as an extremely welcome addition to the Apple subscription ecosystem. Listen to the applause 34 minutes in to the WWDC video announcing them.
Up to this point, Apple had made developers rely on polling via the
/verifyReceipt endpoint. While busy polling Apple doesn’t require a PhD, it does introduce a lot of extra code and infrastructure. This extra complexity has been a thorn in the side of subscription developers for years and we thought, for a brief moment, that we had reached deliverance.
But as developers began to use these new notifications, it became clear that Apple had bolted on another bungled feature to their already challenged subscriptions system.
The core of Server Notifications is a URL that you add to App Store Connect. Apple then sends
POST requests when subscription things happen.
You might think “That’s great! I just pop this URL in and I’ll get pushes when something happens with my subscribers!” Unfortunately, this isn’t the case. If you read the fine print above you notice there is an iPhone XS Max sized caveat.
It is recommended to use these notifications in conjunction with Receipt Validation to validate users’ current status and provide them with service.
So Apple wants you to use Server Notifications with receipt polling. “Ok”, you think, “that’s fine I’ll just pull the receipt when I receive a notification.”
Turns out, that’s not possible either.
A Server Notification can be one of 6 different types. The full documentation is here.
At first glance, these events seem complete. A user buys and triggers an
INITIAL_BUY, their subscription renews with a
RENEWAL event, and then they eventually cancel, triggering a
Unfortunately, that interpretation is completely wrong.
This happens when the first subscription is purchased in a subscription group. This type does basically what it says.
Interestingly though, the Apple docs are conflicted about this
latest_receipt field, stating:
The base-64 encoded transaction receipt for the most recent renewal transaction. Posted only if the
INTERACTIVE_RENEWAL, and only if the renewal is successful.
This is incorrect. From our own testing, the
latest_receipt is sent with every notification type except
The way Apple implemented the
CANCEL notification type is the first real head scratcher.
Cancel does not mean a user normally cancelled their subscription
It only is sent when a user cancels their subscription via customer support. This is also known colloquially as a refund. There is currently no way to easily detect when a user opts-out of renewing normally, i.e. via the App Store settings.
RENEWAL events aren’t sent when a subscription renews. This is another example where the common interpretation doesn’t align with what the notification type actually does.
RENEWAL is sent when a subscription has expired, then later, the user starts the subscription again.
What most developers are looking for is a notification that they can rely on happening when someone renews, allowing them to update their local cache of expiration date. This doesn’t provide that, requiring developers to still update their receipts daily.
INTERACTIVE_RENEWAL is almost the same as
RENEWAL. The documentation says the following:
Customer renewed a subscription interactively after it lapsed, either by using your app’s interface or on the App Store in account settings. Service is made available immediately.
They both only occur for already expired subscriptions, not subscriptions naturally renewing.
The difference is that
INTERACTIVE_RENEWAL is initiated by the user from settings, while a
RENEWAL occurs when a billing issue is resolved. Either way, the importance to a developer is minimal and the distinction just leads to further confusion.
This notification could be really interesting. Apple refers to the renewal preference when discussing what is going to happen at the end of a subscribers current period. This usually means “will they renew or not?”
Knowing the moment a user unsubscribes is really interesting to developers, and Apple is even pushing them to make use of this data. You can use it to make early estimates of the retention of a cohort, send a targeted retention offer, or even to send a survey to figure out why someone churned.
But, this notification falls short of unlocking this information. It is only sent when a user moves from one product to another in the same product group.
In the receipt itself, Apple provides a host of interesting information via the auto renew preference. This data is available if you poll the receipt and can be acted upon. However, Apple didn’t see fit to send us a notification when this happens.
The promise was to make it easier to build rich subscription services and reduce the amount of
/verifyReceipt polling that you need to do. The truth is, Apple shipped another confusing, half finished system.
In theory you could “reduce” the amount of polling you need to do, but it would require a very lawyerly understanding of the docs, which aren’t clear at all. Given the stakes of not providing paid content to your users, its better to error on the side of polling more than relying on server notifications.
Would you be suprised to know that RevenueCat can help you?
When we first dug into Server Notifications, we were excited to see how we could use them with RevenueCat to improve our service. After realizing their limitations, and seeing so many developers become frustrated with them, we decided to build our own.
The RevenueCat webhooks are a sane and thoughtful implementation of server-to-server subscription notifications. The design is oriented around the way developers actually want to use notifications.
We support Android and iOS and normalize events for both so you don’t need to maintain two implementations.