How we implemented Apple Server To Server notifications

And highly useful to us as you can imagine.

So in June 2017, when Apple announced that they were to launch S2S notifications for iOS subscriptions, along with providing more information on billing status, we immediately wanted to try it out.

This is how we went about achieving that.

Benefits:Before we jump into implementation, here are some benefits we would get from doing this:With S2S notifications, we get real-time data on subscriptions.

This is great because it means that we no longer have to run lengthy scripts that iterate through every single active iOS subscription in our database, to find out its status.

If the user cancelled their subscription, we now know exactly when they did so, and we can contact them to see if we can win them back.

We now know which users are in the Apple billing retry logic (because of e.

g.

an expired credit card), and can potentially communicate with them in an attempt to reduce involuntary churn.

Lots of juicy benefits…Implementation — S2S:For the purpose of this post, I’ll focus solely on how we implemented the ‘cancellation’ S2S notifications.

But it’s important to note that there are, at the time of writing, another 4 available notifications: ‘Initial Buy’, ‘Renew’, ‘Interactive Renew’ and ‘Did Change Renewal Pref’.

First thing — it’s really important to define what a ‘cancellation’ is.

We first thought that this particular notification might be fired when the user toggled the “automatic renewal” option in their subscription settings to “off”.

But that wasn’t the case.

Instead, the cancellation notification is only fired when a user contacts Apple’s customer support team and one of their agent’s then cancels and refunds the user.

So from our point of view, when we receive this notification we should remove the user’s premium access immediately and update our database to show that the subscription has ended.

Ok, back to it.

So the first thing we did was to configure the “subscription status url” in iTunes Connect to point to our endpoint that would consume the notification:Once this was switched on, the notifications started to send to our servers.

Here’s an example of one that’s fired:{ "environment": "PROD", "auto_renew_status": "false", "web_order_line_item_id": "***", "latest_expired_receipt_info": { "original_purchase_date_pst": "2018-03-17 05:09:03 America/Los_Angeles", "cancellation_date_ms": "1522134672000", "quantity": "1", "cancellation_reason": "0", "unique_vendor_identifier": "***", "original_purchase_date_ms": "1521288543000", "expires_date_formatted": "2019-03-24 12:09:02 Etc/GMT", "is_in_intro_offer_period": "false", "purchase_date_ms": "1521893342000", "expires_date_formatted_pst": "2019-03-24 05:09:02 America/Los_Angeles", "is_trial_period": "false", "item_id": "***", "unique_identifier": "***", "original_transaction_id": "***", "expires_date": "1553429342000", "app_item_id": "***", "transaction_id": "***", "bvrs": "2", "web_order_line_item_id": "***", "version_external_identifier": "***", "bid": "com.

busuu.

english.

app", "cancellation_date": "2018-03-27 07:11:12 Etc/GMT", "product_id": "com.

busuu.

app.

subs12month_FT_jan_18", "purchase_date": "2018-03-24 12:09:02 Etc/GMT", "cancellation_date_pst": "2018-03-27 00:11:12 America/Los_Angeles", "purchase_date_pst": "2018-03-24 05:09:02 America/Los_Angeles", "original_purchase_date": "2018-03-17 12:09:03 Etc/GMT" }, "cancellation_date_ms": "1522134672000", "latest_expired_receipt": "***", "cancellation_date": "2018-03-27 07:11:12 Etc/GMT", "password": "***", "cancellation_date_pst": "2018-03-27 00:11:12 America/Los_Angeles", "auto_renew_product_id": "com.

busuu.

app.

subs12month_FT_jan_18", "notification_type": "CANCEL"}There’s quite a lot happening in the above.

What are the keys we care about?‘environment’‘notification_type’‘original_transaction_id’‘latest_expired_receipt’Now I bet you were expecting me to also call out the ‘cancellation_date_ms’ and ‘cancellation_reason’ keys, but not just yet.

Hold your horses.

What we do with this information?First of all, we don’t process the cancellation unless the environment is ‘PROD’.

If it’s something else, we’ll simply return a response with the status code 200 and not touch any of our other logic.

And while we’re here, one thing to note is that unless we return a 200 response (for any environment), Apple will continue to send us that same notification for a given period of time.

Next, we check the value in ‘notification_type’ to determine exactly what it is and how to process it.

If the value is ‘CANCEL’, we proceed through our cancellation logic.

If it’s ‘INITIAL_BUY’, we proceed through different logic.

And so on.

We then use the ‘original_transaction_id’ in the notification to check for a matching subscription in our database.

If there is one (fingers crossed!), we validate that it’s not already been marked as cancelled / finished.

Not a big deal but we found that sometimes we receive duplicate notifications from Apple, and of course, we only want to process the first one.

Anyway, once we know we’ve got a currently ‘active’ subscription that we need to cancel, we use the ‘latest_expired_receipt’ key in the notification, and call Apple’s ‘verifyReceipt’ endpoint just to make sure we’ve got the latest information.

Here’s an example of some of the response:{ "auto_renew_status": 0, "status": 21006, "cancellation_date": "2018-04-18 06:18:23 Etc/GMT", "auto_renew_product_id": "com.

busuu.

app.

subs12month_FT_jan_18", "cancellation_reason": "0", "latest_expired_receipt_info": { "original_purchase_date_pst": "2018-04-02 04:46:01 America/Los_Angeles", "cancellation_date_ms": "1528331233000", "quantity": "1", "cancellation_reason": "0", "unique_vendor_identifier": "***", "bvrs": "2", "expires_date_formatted": "2019-04-16 18:46:01 Etc/GMT", "is_in_intro_offer_period": "false", "purchase_date_ms": "1523904361000", "expires_date_formatted_pst": "2019-04-16 11:46:01 America/Los_Angeles", "is_trial_period": "false", "item_id": "***", "unique_identifier": "***", "original_transaction_id": "***", "expires_date": "1555440361000", "app_item_id": "***", "transaction_id": "***", "web_order_line_item_id": "***", "original_purchase_date": "2018-04-09 18:46:01 Etc/GMT", "cancellation_date": "2018-04-18 06:18:23 Etc/GMT", "product_id": "com.

busuu.

app.

subs12month_FT_jan_18", "purchase_date": "2018-04-16 18:46:01 Etc/GMT", "purchase_date_pst": "2018-04-16 11:46:01 America/Los_Angeles", "cancellation_date_pst": "2018-04-17 23:18:23 America/Los_Angeles", "bid": "com.

busuu.

english.

app", "original_purchase_date_ms": "1523299561000" }.

}Ok great, so now we know we’re working with the latest receipt information.

It’s at this point that we look for a ‘cancellation_date’ key.

If there is one, we can proceed with the cancellation.

And just a reminder — cancellation from an Apple notification perspective means that the user has been refunded.

Therefore we mark the subscription as cancelled and finished in our database and we then immediately remove the user’s premium access.

Job done.

One other thing that we are interested in but not directly using at the time of writing is the ‘cancellation_reason’ key.

This key can have two different values and indicates why the user contacted Apple and asked for their subscription to be cancelled / refunded:"1" — Customer canceled their transaction due to an actual or perceived issue within your app.

"0" — Transaction was canceled for another reason, for example, if the customer made the purchase accidentallySo potentially something for us to look at in the near future with regards to reducing churn.

Implementation — Pending Renewal Info:Not related to S2S notifications, but something we implemented at the same time.

Apple now lets us know when a user is in their ‘billing retry period”.

This period refers to when Apple has tried to take payment from a user for an auto-renewable subscription and, for whatever reason, this has failed.

Apple will continue to attempt to take payment for up to 60 days before cancelling the subscription.

As you can imagine, just like the S2S notifications, this is really interesting information for us as it allows us to proactively contact a user who’s e.

g.

credit card details have expired, and ask them to update their details in order to continue to use busuu.

It also allows us to grant the user a “grace period” of continued access to their premium subscription, before we make a decision on when to mark their subscription as unpaid and finished.

An example:Let’s imagine we’ve identified an active iOS subscription in our database and have called Apple’s ‘verifyReceipt’ endpoint to find out the latest information about it.

The response contains a ‘pending_renewal_info’ section, an example of which is below:"pending_renewal_info": [ { "expiration_intent": "2", "auto_renew_product_id": "com.

busuu.

app.

subs12month_FT_jan_18", "original_transaction_id": "***", "is_in_billing_retry_period": "1", "product_id": "com.

busuu.

app.

subs12month_FT_jan_18", "auto_renew_status": "1" }]The keys we care about for the purpose of the billing retry period are “is_in_billing_retry_period” (who would’ve thought…) and “expiration_intent”.

The “1” in the “is_in_billing_retry_period” means that the App Store is still attempting to renew the subscription, and the “2” in the “expiration_intent” means that there has been a billing error, for example the user’s payment information is no longer valid.

It’s worth noting that the “expiration_intent” key can have several values according to the official documentation — https://developer.

apple.

com/library/content/releasenotes/General/ValidateAppStoreReceipt/Chapters/ReceiptFields.

htmlTherefore based on the example above, we will enter the user into our grace period and consider whether to contact them to prod them to update their payment details in Apple.

Pretty cool.

And that’s it!First technical blog post done 🙂 Until next time…We hope you enjoyed this post!.If this sounds great, you’d like to progress in your tech career, and you’ve got a love for learning languages (tech or otherwise!), we’ve got lots of open roles in our Tech team!.

. More details

Leave a Reply