Handle Server to Server (S2S)Notifications for Apple Subscription
Prerequisite :
1. Understanding of apple subscription.
2. Basic knowledge of apple S2S notifications and how to set it up. What to expect from this blog :
1. How to handle several events sent via apple to your secure server.
2. Useful reference/ documentation by apple.
3. At the end of the article you can also find blog to setup secure server for s2s notification server.
Assumption
For the sake of this article, let's assume ABC Corp has 3 subscription package in one package group.com.sample.medium.pro (Highest tier)
com.sample.medium.lite (Mid Level tier)
com.sample.medium.starer (Lowest tier)
Handle INITIAL_BUY
Description :
Occurs at the user’s initial purchase of the subscription. Store latest_receipt
on your server as a token to verify the user’s subscription status at any time by validating it with the App Store.
When It occurs :
1.Initial purchase : Customer completed an initial purchase of a subscription. For example, If company ABC Corp.
has multiple product and user can choose the one which fits and buy it. Backend Server will receive S2S notification of type INITIAL_BUY (In no circumstance this event will come if user buy another product from ABC Corp.
)
What to do :
For 1: Update autorenewal info (and schedule callback on expires at)
Sample Payload :
{
"environment": "Sandbox",
"notification_type": 1,
"password": "*0c1*****************0",
"cancellation_date": "",
"cancellation_date_pst": "",
"cancellation_date_ms": "",
"web_order_line_item_id": "",
"latest_receipt": "*****************",
"latest_receipt_info": {
"quantity": "1",
"product_id": "com.sample.medium.lite",
"transaction_id": "123456789011121",
"original_transaction_id": "123456789011121",
"is_trial_period": "true",
"app_item_id": "12345678",
"version_external_identifier": "13246347",
"web_order_line_item_id": "120000201234567",
"purchase_date": "2019-10-16 07:49:21 Etc/GMT",
"purchase_date_ms": "1571212161000",
"purchase_date_pst": "2019-10-16 00:49:21 America/Los_Angeles",
"original_purchase_date": "2019-10-16 07:49:21 Etc/GMT",
"original_purchase_date_ms": "1571212161000",
"original_purchase_date_ps": "2019-10-16 00:49:21 America/Los_Angeles",
"expires_date": "1573894161000",
"expires_date_ms": "",
"expires_date_pst": "",
"cancellation_date": "",
"cancellation_date_ms": "",
"cancellation_date_pst": ""
},
"latest_expired_receipt": "",
"auto_renew_status": true,
"auto_renew_product_id": "com.sample.medium.lite",
"auto_renew_status_change_date": "",
"auto_renew_status_change_date_pst": "",
"auto_renew_status_change_date_ms": ""
}
Handle CANCEL
Description :
Cancel event can have immediate or deferred effect on user’s subscription.
When it is upgrade, this cancel event has immediate effect since your server need to cancel current subscription and fulfil new subscription.
Where as when user choose not to continue his subscription he can cancel but there is a catch. He can cancel the subscription by himself, in this case your server will receive DID_CHANGE_RENEWAL_STATUS, we will discuss this later in this article. But he apply for refund from apple, and apple will send cancel event immediately and user’s subscription will be revoked.
When It occurs :
1.Subscription is active; user upgraded to another SKU
2.AppleCare refunded a subscription
What to do :
For 1 : We check, is_upgraded in latest receipt, if true -> we do not take any action, we handle it in INTERACTIVE_RENEWAL
https://developer.apple.com/documentation/appstorereceipts/responsebody/latest_receipt_info
For 2 : Cancel immediately
Sample Payload :
{
"environment": "Sandbox",
"notification_type": 2,
"password": "*0c1*****************0",
"cancellation_date": "2019-10-29 03:04:49 Etc/GMT",
"cancellation_date_pst": "2019-10-28 20:04:49 America/Los_Angeles",
"cancellation_date_ms": "1572318289000",
"web_order_line_item_id": "120000201234568",
"latest_receipt": "",
"latest_expired_receipt": "***************",
"latest_expired_receipt_info": {
"quantity": "1",
"product_id": "com.sample.medium.lite",
"transaction_id": "123456789011122",
"original_transaction_id": "123456789011121",
"is_trial_period": "false",
"app_item_id": "1213233",
"version_external_identifier": "12321312",
"web_order_line_item_id": "12312312312",
"purchase_date": "2019-10-29 02:50:29 Etc/GMT",
"purchase_date_ms": "1572317429000",
"purchase_date_pst": "2019-10-28 19:50:29 America/Los_Angeles",
"original_purchase_date": "2019-09-29 02:50:29 Etc/GMT",
"original_purchase_date_ms": "1569725429000",
"original_purchase_date_ps": "2019-09-28 19:50:29 America/Los_Angeles",
"expires_date": "1574999429000",
"expires_date_ms": "",
"expires_date_pst": "",
"cancellation_date": "2019-10-29 03:04:49 Etc/GMT",
"cancellation_date_ms": "1572318289000",
"cancellation_date_pst": "2019-10-28 20:04:49 America/Los_Angeles"
},
"auto_renew_status": false,
"auto_renew_product_id": "com.sample.medium.lite",
"auto_renew_status_change_date": "",
"auto_renew_status_change_date_pst": "",
"auto_renew_status_change_date_ms": ""
}
Handle DID_CHANGE_RENEWAL_PREF
Description :
Your company have have multiple subscription plan under one package group, and user may choose to continue to current subscription and upgrade (when next subscription level is higher than current subscription)or downgrade (when next subscription level is lower than current subscription)or cross grade (when next subscription package are on same level). This event is sent to backend server when user is trying to downgrade from his existing subscription package.
When It occurs :
1.Indicates the customer made a change in their subscription plan that takes effect at the next renewal. The currently active plan is not affected.
What to do :
For 1: Schedule Renewal with new product id and expires_at, handle downgrade on callback, end previous purchase and buy new (mark it downgrade).
On callback first we check if callback is relevant, meaning a user could take multiple actions such that this downgrade may not be applicable anymore, if applicable then downgrade, else just ack and wait for another callback.
Sample Payload :
{
"environment": "Sandbox",
"notification_type": 5,
"password": "20c1f4532eae4016bf28bf458d143020",
"latest_receipt_info": {},
"latest_expired_receipt": "***",
"latest_expired_receipt_info": {
"quantity": "1",
"product_id": "com.sample.medium.lite",
"transaction_id": "1000000623052311",
"original_transaction_id": "1000000538424574",
"is_trial_period": "false",
"version_external_identifier": "0",
"web_order_line_item_id": "1000000050014626",
"purchase_date": "2020-02-05 03:47:47 Etc/GMT",
"purchase_date_ms": "1580874467000",
"purchase_date_pst": "2020-02-04 19:47:47 America/Los_Angeles",
"original_purchase_date": "2019-06-19 04:25:02 Etc/GMT",
"original_purchase_date_ms": "1560918302000",
"original_purchase_date_ps": "2019-06-18 21:25:02 America/Los_Angeles",
"expires_date": "1580874767000"
},
"auto_renew_status": true,
"auto_renew_product_id": "com.sample.medium.starter"
}
Handle DID_CHANGE_RENEWAL_STATUS
Description :
Indicates a change in the subscription renewal status. Check auto_renew_status_change_date_ms and auto_renew_status in the JSON response to know the date and time of the last status update and the current renewal status.
When It occurs :
1.Subscription is active; upgrade to another SKU
2.Subscription has expired; resubscribe to the same SKU
3.Subscription has expired; resubscribe to another SKU (upgrade or downgrade)
4.Auto-renewal disabled (canceled) from the App Store account’s Subscriptions settings
5.AppleCare refund
6.Subscription churned after failed billing retry attempts
What to do :
For 1 : Handle via INTERACTIVE_RENEWAL
For 2 : Find last ended purchase, if found, then repurchase.
For 3 : Handle via INTERACTIVE_RENEWAL
For 4,5,6 : Update autorenewal info in your DB. And for future action on this subscription you can schedule a callback or worker to take necessary action (If no new receipt, then expire, if new receipt found renew / downgrade to new subscription)
Sample Payload :
{
"environment": "Sandbox",
"notification_type": 6,
"password": "***",
"latest_receipt": "***",
"latest_receipt_info": {
"quantity": "1",
"product_id": "com.sample.medium.starter",
"transaction_id": "180000550385163",
"original_transaction_id": "180000550385163",
"is_trial_period": "false",
"app_item_id": "548607187",
"version_external_identifier": "834009732",
"web_order_line_item_id": "280000186288530",
"purchase_date": "2020-01-24 20:50:42 Etc/GMT",
"purchase_date_ms": "1579899042000",
"purchase_date_pst": "2020-01-24 12:50:42 America/Los_Angeles",
"original_purchase_date": "2019-12-24 20:50:43 Etc/GMT",
"original_purchase_date_ms": "1577220643000",
"original_purchase_date_ps": "2019-12-24 12:50:43 America/Los_Angeles",
"expires_date": "1582577442000"
},
"latest_expired_receipt_info": {},
"auto_renew_product_id": "com.sample.medium.starter",
"auto_renew_status_change_date": "2020-02-17 15:05:49 Etc/GMT"
}
Handle DID_FAIL_TO_RENEW
Description :
Indicates a subscription that failed to renew due to a billing issue. Check is_in_billing_retry_period
to know the current retry status of the subscription, and grace_period_expires_date
to know the new service expiration date if the subscription is in a billing grace period.
When It occurs :
1.Subscription failed to renew because of a billing issue. In billing retry this event is followed by DID_RECOVER in case of successful billing retry.
What to do :
For 1: Update Grace period info
Update renewal info
Sample Payload :
{
"environment": "Sandbox",
"notification_type": 7,
"password": "***",
"latest_receipt_info": {},
"latest_expired_receipt": "****",
"latest_expired_receipt_info": {
"quantity": "1",
"product_id": "com.sample.medium.starter",
"transaction_id": "2000000652607894",
"original_transaction_id": "6000000652607898",
"is_trial_period": "false",
"version_external_identifier": "0",
"web_order_line_item_id": "2000000652607895",
"purchase_date": "2020-04-16 08:55:43 Etc/GMT",
"purchase_date_ms": "1587027343000",
"purchase_date_pst": "2020-04-16 01:55:43 America/Los_Angeles",
"original_purchase_date": "2019-06-19 04:25:02 Etc/GMT",
"original_purchase_date_ms": "1560918302000",
"original_purchase_date_ps": "2019-06-18 21:25:02 America/Los_Angeles",
"expires_date": "1587027643000"
},
"auto_renew_status": true,
"auto_renew_product_id": "com.sample.medium.starter"
}
Handle DID_RECOVER
Description :
Indicates successful automatic renewal of an expired subscription that failed to renew in the past. Check expires_date to determine the next renewal date and time.
When It occurs :
1.First apple tries to renew subscription and incase of any billing issue it send DID_FAIL_TO_RENEW and subscription is expired (or goes on hold, your company can choose to end subscription or pause it). Expired subscription recovered by App Store through a billing retry.
What to do :
For 1: First check if there is any past active purchase, end it.
Repurchase to expired purchase.
Sample Payload :
{
"environment": "Sandbox",
"notification_type": 8,
"password": "***",
"latest_receipt": "***",
"latest_receipt_info": {
"quantity": "1",
"product_id": "com.sample.medium.starter",
"transaction_id": "2000000652607896",
"original_transaction_id": "7000000652697896",
"is_trial_period": "false",
"version_external_identifier": "0",
"web_order_line_item_id": "2000000652607897",
"purchase_date": "2020-03-24 17:26:00 Etc/GMT",
"purchase_date_ms": "1585070760000",
"purchase_date_pst": "2020-03-24 10:26:00 America/Los_Angeles",
"original_purchase_date": "2019-10-23 04:02:14 Etc/GMT",
"original_purchase_date_ms": "1571803334000",
"original_purchase_date_ps": "2019-10-22 21:02:14 America/Los_Angeles",
"expires_date": "1585071060000"
},
"latest_expired_receipt_info": {},
"auto_renew_status": true,
"auto_renew_product_id": "com.sample.medium.starter"
}
Handle INTERACTIVE_RENEWAL
Description :
Indicates the customer renewed a subscription interactively, either by using your app’s interface, or on the App Store in the account’s Subscriptions settings. Make service available immediately.
When It occurs :
1.Subscription is active; upgrade to another SKU
2.Subscription is active; downgrade to another SKU
3.Subscription has expired; resubscribe to another SKU (upgrade or downgrade)
What to do :
For 1 : Find last active purchase, check if upgrade, then upgrade
For 2 : Ack and let it be handled via, DID_CHANGE_RENEWAL_PREF
For 3 : For upgrade, find last expired purchase, check if upgrade, then upgrade
For downgrade, find last expired purchase, check if downgrade, then downgrade
Sample Payload :
{
"environment": "Sandbox",
"notification_type": 4,
"password": "***",
"latest_receipt": "***",
"latest_receipt_info": {
"quantity": "1",
"product_id": "com.sample.medium.starter",
"transaction_id": "2000000589933473",
"original_transaction_id": "1200400588424574",
"is_trial_period": "false",
"app_item_id": "",
"version_external_identifier": "0",
"web_order_line_item_id": "2000000589933475",
"purchase_date": "2019-10-16 12:16:25 Etc/GMT",
"purchase_date_ms": "1571228185000",
"purchase_date_pst": "2019-10-16 05:16:25 America/Los_Angeles",
"original_purchase_date": "2019-06-19 04:25:02 Etc/GMT",
"original_purchase_date_ms": "1560918302000",
"original_purchase_date_ps": "2019-06-18 21:25:02 America/Los_Angeles",
"expires_date": "1571228485000"
},
"auto_renew_status": true,
"auto_renew_product_id": "com.sample.medium.starter"
}
Handle REFUND
Description :
Indicates that App Store successfully refunded a transaction. The cancellation_date_ms
contains the timestamp of the refunded transaction; the original_transaction_id
and product_id
identify the original transaction and product, and cancellation_reason
contains the reason.
When It occurs :
1.AppleCare successfully refunded the transaction for a consumable, non-consumable, or a non-renewing subscription
What to do :
For 1 : You can record this refunded transaction (amount and timestamp)in backend for revenue recognition.
Sample Payload :
<coming soon>
Handle RENEWAL (Deprecated)
Indicates successful automatic renewal of an expired subscription that failed to renew in the past. Check expires_date to determine the next renewal date and time.Deprecated.
Coming Soon:
1. Design and scalable architecture of webhook gateway to handle S2S events from apple or any other service providers.
Reference :
https://developer.apple.com/videos/play/wwdc2019/302/https://developer.apple.com/documentation/appstoreservernotifications/notification_type