Verifying Receipts latest_expired_receipt_info and latest_receipt_info types Arrays or just receipts for Subscrcriptions

Hello I'm working on integrating renewable subscriptions statuses from apple into our cloud and I have found some inconsistent behavior that is making very hard to properly create a deserializer for the verification receipt endpoint. I am following the documentation found in here https://developer.apple.com/documentation/appstorereceipts/responsebody


We are testing only renweables subscriptions right now, nothing else. According to the documentation latest_expired_receipt_info and latest_receipt_info should be an array of receipts, which is sometimes the case, however, sometimes the response from the verification receipt endpoint returns latest_expired_receipt_info and latest_receipt_info not as an array, but as a single receipt object.


This is the first time I'm integrating with apple subscription, I'm using golang and this non consistent responses break the deserializer i have for the response, all this behavior is from the Sandbox environment using the endpoint https://sandbox.itunes.apple.com/verifyReceipt

to verify the receipts and setting the exclude-old-transactions option to true in the request.


I would appreacite it if anyone would point me to the right direction as of why the inconsistent response JSONs.


Here is an example where the property is not an array


{
  "auto_renew_status": 0,
  "latest_expired_receipt_info": {
  "original_purchase_date_pst": "2019-11-12 10:01:07 America/Los_Angeles",
  "quantity": "1",
  "unique_vendor_identifier": "removed for this post",
  "bvrs": "removed for this post",
  "expires_date_formatted": "2019-11-12 22:41:18 Etc/GMT",
  "is_in_intro_offer_period": "false",
  "purchase_date_ms": "1573598178000",
  "expires_date_formatted_pst": "2019-11-12 14:41:18 America/Los_Angeles",
  "is_trial_period": "false",
  "item_id": "removed for this post",
  "unique_identifier": "removed for this post",
  "original_transaction_id": "removed for this post",
  "subscription_group_identifier": "removed for this post",
  "transaction_id": "removed for this post",
  "bid": "removed for this post",
  "web_order_line_item_id": "removed for this post",
  "purchase_date": "2019-11-12 22:36:18 Etc/GMT",
  "product_id": "removed for this post",
  "expires_date": "1573598478000",
  "original_purchase_date": "2019-11-12 18:01:07 Etc/GMT",
  "purchase_date_pst": "2019-11-12 14:36:18 America/Los_Angeles",
  "original_purchase_date_ms": "1573581667000"
  },
  "status": 21006,
  "auto_renew_product_id": "removed for this post",
  "receipt": {
  "original_purchase_date_pst": "2019-11-12 10:01:07 America/Los_Angeles",
  "quantity": "1",
  "unique_vendor_identifier": "removed for this post",
  "bvrs": "removed for this post",
  "expires_date_formatted": "2019-11-12 22:41:18 Etc/GMT",
  "is_in_intro_offer_period": "false",
  "purchase_date_ms": "1573598178000",
  "expires_date_formatted_pst": "2019-11-12 14:41:18 America/Los_Angeles",
  "is_trial_period": "false",
  "item_id": "removed for this post",
  "unique_identifier": "removed for this post",
  "original_transaction_id": "removed for this post",
  "subscription_group_identifier": "removed for this post",
  "transaction_id": "removed for this post",
  "web_order_line_item_id": "removed for this post",
  "version_external_identifier": "0",
  "purchase_date": "2019-11-12 22:36:18 Etc/GMT",
  "product_id": "removed for this post",
  "expires_date": "1573598478000",
  "original_purchase_date": "2019-11-12 18:01:07 Etc/GMT",
  "purchase_date_pst": "2019-11-12 14:36:18 America/Los_Angeles",
  "bid": "removed for this post",
  "original_purchase_date_ms": "1573581667000"
  },
  "expiration_intent": "1",
  "is_in_billing_retry_period": "0"
}


However, according to the documentation it should be always returned this way.


{
            "status": 0,
            "environment": "Production",
            "receipt": {
                "receipt_type": "Production",
                "adam_id": 0000000000,//changed for this post
                "app_item_id":  0000000000,//changed for this post
                "bundle_id": "com.foo.product",
                "application_version": "000", //changed for this post
                "download_id":  0000000000,//changed for this post
                "version_external_identifier":  0000000000,//changed for this post
                "receipt_creation_date": "2019-11-26 17:01:33 Etc/GMT",
                "receipt_creation_date_ms": "1574787693000",
                "receipt_creation_date_pst": "2019-11-26 09:01:33 America/Los_Angeles",
                "request_date": "2019-12-02 23:09:39 Etc/GMT",
                "request_date_ms": "1575328179029",
                "request_date_pst": "2019-12-02 15:09:39 America/Los_Angeles",
                "original_purchase_date": "2019-11-07 05:57:29 Etc/GMT",
                "original_purchase_date_ms": "1573106249000",
                "original_purchase_date_pst": "2019-11-06 21:57:29 America/Los_Angeles",
                "original_application_version": "99",
                "in_app": [
                    {
                        "quantity": "1",
                        "product_id": "com.foo.p", //changed for this post
                        "transaction_id": " 0000000000",//changed for this post ",
                        "original_transaction_id": " 0000000000",//changed for this post ,
                        "purchase_date": "2019-11-26 17:01:30 Etc/GMT",
                        "purchase_date_ms": "1574787690000",
                        "purchase_date_pst": "2019-11-26 09:01:30 America/Los_Angeles",
                        "original_purchase_date": "2019-11-26 17:01:31 Etc/GMT",
                        "original_purchase_date_ms": "1574787691000",
                        "original_purchase_date_pst": "2019-11-26 09:01:31 America/Los_Angeles",
                        "expires_date": "2020-11-26 17:01:30 Etc/GMT",
                        "expires_date_ms": "1606410090000",
                        "expires_date_pst": "2020-11-26 09:01:30 America/Los_Angeles",
                        "web_order_line_item_id": "240000240409401",
                        "is_trial_period": "false",
                        "is_in_intro_offer_period": "false"
                    }
                ]
            },
            "latest_receipt_info": [
                {
                    "quantity": "1",
                    "product_id": "com.foo.prod", //changed for this post
                    "transaction_id": "0000000000",//changed for this post
                    "original_transaction_id": "0000000000",//changed for this post
                    "purchase_date": "2019-11-26 17:01:30 Etc/GMT",
                    "purchase_date_ms": "1574787690000",
                    "purchase_date_pst": "2019-11-26 09:01:30 America/Los_Angeles",
                    "original_purchase_date": "2019-11-26 17:01:31 Etc/GMT",
                    "original_purchase_date_ms": "1574787691000",
                    "original_purchase_date_pst": "2019-11-26 09:01:31 America/Los_Angeles",
                    "expires_date": "2020-11-26 17:01:30 Etc/GMT",
                    "expires_date_ms": "1606410090000",
                    "expires_date_pst": "2020-11-26 09:01:30 America/Los_Angeles",
                    "web_order_line_item_id": " 0000000000",//changed for this post
                    "is_trial_period": "false",
                    "is_in_intro_offer_period": "false",
                    "subscription_group_identifier":  "0000000000",//changed for this post
                }
            ],
            "latest_receipt": "encoded receipt removed for this post ",
             "pending_renewal_info": [
                {
                    "auto_renew_product_id": "com.fooapp.fooprod",
                    "original_transaction_id": " 0000000000",//changed for this post
                    "product_id": "com.fooapp.fooprod",
                    "auto_renew_status": "1"
                }
            ]
        }


So I need to know why it returns the property as an array sometimes and as a single receipt object other times.


Thanks in advance.

Replies

Here is a pointer to the earlier documentation:

https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateRemotely.html#//apple_ref/doc/uid/TP40010573-CH104-SW2


The field you are referencing is not "latest_receipt_info". That receipt might contain many IAP transactions and would have to be an array. Rather, the field you are referencing is "latest_expired_receipt_info". That field is not described in the current documentation - please report the bug. In the older documentation (referenced above) that field is " (t)he JSON representation of the receipt for the expired subscription." Only one transaction will be the 'latest' and therefore there is no array.

Further to my earlier comments, the receipt you posted is seriously flawed. Not the "latest_receipt_info" but the field "receipt": { ......}" is all wrong. Can you confirm that is actually what was returned?



EDIT:


Oh wait, I see now, it's a 21006.....(nevermind)


"21006  
This receipt is valid but the subscription has expired. When this status code is returned to your server, the receipt data is also decoded and returned as part of the response. Only returned for iOS 6-style transaction receipts for auto-renewable subscriptions."

Be careful trusting the receipt in your sandboxed vs. live example - as the docs state "The JSON data returned in the response from the App Store." .

Is there any way to make it consistant? Why do the type of the properties change like that based on an status? Also you mentioned


" Only returned for iOS 6-style transaction receipts for auto-renewable subscriptions. " We are selling renewable subscriptions for sure, is there a configuration setting on our side that perhaps we have overlooked? or it just the nature of the Apple's subscription system? I can create a falback deserializer, but I want to understand the reason underneath, if it the nature of the Apple's system, it is what it is, but if there is away we can make it consistent through a configuration setting that would be a useful to understand.

The documentation is stating that the receipt response you are getting is for an iOS 6 receipt. Is this correct? Is this a receipt that is over 4 years old and that was obtained from transaction.transactionReceipt or is this a receipt that you acquired from NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL]; ?


If this is an iOS 6 receipt then you are querying a system that has been replaced years ago. You are complaining about inconsistencies in a grandfathered system.

What did you edit on December 2?

Is this thread still open or are you comfortable accepting:


"The documentation is stating that the receipt response you are getting is for an iOS 6 receipt. Is this correct? Is this a receipt that is over 4 years old and that was obtained from transaction.transactionReceipt or is this a receipt that you acquired from NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL]; ?


If this is an iOS 6 receipt then you are querying a system that has been replaced years ago. You are complaining about inconsistencies in a grandfathered system."