The Ultimate Handbook On App-Store Receipt Validation

Infrastructure

Apr 27, 2021

Michael

The Ultimate Handbook On App-Store Receipt Validation

Infrastructure

Apr 27, 2021

Michael

The Ultimate Handbook On App-Store Receipt Validation

Infrastructure

Apr 27, 2021

Michael

The Ultimate Handbook On App-Store Receipt Validation

Infrastructure

Apr 27, 2021

Michael

The Ultimate Handbook On App-Store
The Ultimate Handbook On App-Store
The Ultimate Handbook On App-Store
The Ultimate Handbook On App-Store

iOS developers are familiar with StoreKit, a framework that allows them to monetize apps through in-app purchases and subscriptions. For everything to work smoothly, avoid piracy, and make sure users get access to what they purchased developers need to perform receipt validation.

The process begins once a user makes an in-app purchase. After that, developers have to process the App Store receipt, the data of which is Base64-encoded. For a decoded version, you need to pass it through Apple’s verifyReceipt endpoint.

Unfortunately, understanding the decrypted receipt can be an arduous process. There are many nuances and potential pitfalls in the data structure. That’s why, in this article, we will cover each element of the decrypted receipt and help you get a better grasp on how you can avoid common problems. 

iOS developers are familiar with StoreKit, a framework that allows them to monetize apps through in-app purchases and subscriptions. For everything to work smoothly, avoid piracy, and make sure users get access to what they purchased developers need to perform receipt validation.

The process begins once a user makes an in-app purchase. After that, developers have to process the App Store receipt, the data of which is Base64-encoded. For a decoded version, you need to pass it through Apple’s verifyReceipt endpoint.

Unfortunately, understanding the decrypted receipt can be an arduous process. There are many nuances and potential pitfalls in the data structure. That’s why, in this article, we will cover each element of the decrypted receipt and help you get a better grasp on how you can avoid common problems. 

iOS developers are familiar with StoreKit, a framework that allows them to monetize apps through in-app purchases and subscriptions. For everything to work smoothly, avoid piracy, and make sure users get access to what they purchased developers need to perform receipt validation.

The process begins once a user makes an in-app purchase. After that, developers have to process the App Store receipt, the data of which is Base64-encoded. For a decoded version, you need to pass it through Apple’s verifyReceipt endpoint.

Unfortunately, understanding the decrypted receipt can be an arduous process. There are many nuances and potential pitfalls in the data structure. That’s why, in this article, we will cover each element of the decrypted receipt and help you get a better grasp on how you can avoid common problems. 

iOS developers are familiar with StoreKit, a framework that allows them to monetize apps through in-app purchases and subscriptions. For everything to work smoothly, avoid piracy, and make sure users get access to what they purchased developers need to perform receipt validation.

The process begins once a user makes an in-app purchase. After that, developers have to process the App Store receipt, the data of which is Base64-encoded. For a decoded version, you need to pass it through Apple’s verifyReceipt endpoint.

Unfortunately, understanding the decrypted receipt can be an arduous process. There are many nuances and potential pitfalls in the data structure. That’s why, in this article, we will cover each element of the decrypted receipt and help you get a better grasp on how you can avoid common problems. 

Elements of a Decrypted Receipt

Elements of a Decrypted Receipt

If the customer has a long past purchase history, the App Store decoded receipt response body may be unnerving. Depending on circumstances, there can be 3-7 top-level keys in the data structure:

  • status

  • is-retryable

  • environment

  • receipt

  • latest_receipt

  • latest_receipt_info

  • pending_renewal_info

{ "status": 21199, "is-retryable": 1, "environment": "Sandbox", "receipt": {}, "latest_receipt": "", "latest_receipt_info": [{}], "pending_renewal_info": [{}], }Copy

responseBody

Since not all top-level keys are always present, let’s first take a look at the key kinds and identify when they are present in the responseBody.

To better understand each of these properties, let’s take a closer look at each one of them.

status

When you get a response from verifyReceipt, the status key is the first thing you should look at. There’ll either be a 0, meaning the receipt is valid, or a status code indicating an error. To know how to proceed you need to recognize status codes and what they mean. 

As you may have noticed, to resolve some status values you just have to try again. This is why your receipt validation service needs to be strong enough to handle various scenarios. Whether it’s network timeouts or service unavailable status codes, you need to be able to deal with it all. If failure continues to occur, it might be worth attempting to process the receipt later and adding it to the job queue. 

Now, let’s move on to the next property. 

is-retryable

In the previous section, we went through status values and their meanings. One of the last possible values was a range of 21100-21199. If the status value is in that range, you should see the is-retryable key in the response payload. 

Despite Apple listing the type as “boolean”, the values displayed aren’t the typical values of true or false. Instead, they are 0 and 1. 0 means the issue is unresolvable and you shouldn’t try validating this receipt. 1 means the issue is temporary and you can try to validate the receipt again.

environment

This value indicates the environment for which the receipt was generated. It’ll either be Sandbox, a receipt generated from the sandbox environment, or Production, a receipt generated from the production environment. 

receipt

The value of receipt is a JSON representation that was sent for verification. This is a key that always exists in a successful receipt, regardless of whether the user has made an in-app purchase. 

Ideally, you should send receipt data from your app to a server so that you store and decode many receipts that contain the receipt key with specific metadata even if there have been no in-app purchases.

If you remember, the App Store was built on the principles of the iTunes Store. Back then there were only two types of apps – paid and free. Now, however, we have in-app purchases and subscriptions which have made things a bit more complex. 

Additional context was added to the data structure following the introduction of in-app purchases. To understand in-app purchases there’s a full context for you to work with, unlike for the subscription in-app purchases.

This is where latest_receipt_info and pending_renewal_info keys come in. For auto-renewable subscriptions, you will find additional context inside these two properties. 

{ "environment": "Sandbox", "receipt": { "receipt_type": "ProductionSandbox", "adam_id": 0, "app_item_id": 0, "bundle_id": "product.name", "application_version": "7", "download_id": 0, "version_external_identifier": 0, "receipt_creation_date": "2021-04-13 06:46:04 Etc/GMT", "receipt_creation_date_ms": "1618296364000", "receipt_creation_date_pst": "2021-04-12 23:46:04 America/Los_Angeles", "request_date": "2021-04-26 08:40:45 Etc/GMT", "request_date_ms": "1619426445186", "request_date_pst": "2021-04-26 01:40:45 America/Los_Angeles", "original_purchase_date": "2013-08-01 07:00:00 Etc/GMT", "original_purchase_date_ms": "1375340400000", "original_purchase_date_pst": "2013-08-01 00:00:00 America/Los_Angeles", "original_application_version": "1.0", "in_app": [ { Copy

latest_receipt

The latest Base64 encrypted app receipt. This one is only returned for auto-renewable subscription receipts. 

latest_receipt_info

This key is an array containing all in-app purchase transactions. Once again, it’s only returned for auto-renewable subscription receipts. 

To include only the latest transactions, you can set the exclude-old-transactions field to true in your verifyReceipt request.

"latest_receipt_info": [ { "quantity": "1", "product_id": "product.name.1", "transaction_id": "1000000000000000", "original_transaction_id": "1000000000000000", "purchase_date": "2021-04-09 08:20:55 Etc/GMT", "purchase_date_ms": "1617956455000", "purchase_date_pst": "2021-04-09 01:20:55 America/Los_Angeles", "original_purchase_date": "2021-04-13 09:51:39 Etc/GMT", "original_purchase_date_ms": "1618307499000", "original_purchase_date_pst": "2021-04-13 02:51:39 America/Los_Angeles", "expires_date": "2021-04-09 08:25:55 Etc/GMT", "expires_date_ms": "1617956755000", "expires_date_pst": "2021-04-09 01:25:55 America/Los_Angeles", "web_order_line_item_id": "1000000000000000", "is_trial_period": "false", "is_in_intro_offer_period": "false", "subscription_group_identifier": "10000000" },Copy

pending_renewal_info

Also an array of JSON objects where each element represents the pending renewal information for each auto-renewable subscription identified by the product_id. It’s only returned for auto-renewable subscription receipts.

responseBody.Receipt

Now that we’ve covered all the responseBody properties, let’s deep dive into the potential elements of responseBody.Receipt.

This may look overwhelming, but don’t worry. We are going to look over each of these properties in more detail so you can get a better understanding of what they mean.

adam_id and app_item_id

This property is generated by App Store Connect and used to uniquely identify the app corresponding with the receipt. It’s a 64-bit long integer that is assigned solely in production. Thus, expect a unique value in production and a 0 in the sandbox.

application_version

Indicates the app’s version number at the time of the receipt’s receipt_creation_date_ms. It corresponds to CFBundleVersion (iOS) or CFBundleShortVersionString (macOS) in the Info.plist. The value is always “1.0” in the sandbox.

bundle_id

This string is provided on App Store Connect and relates to the value of CFBundleIdentifier in the Info.plist file of the app. It’s a bundle identifier to the app to which the receipt belongs.

download_id

This is a unique identifier for the app download transaction. In the sandbox, the field will be populated with a 0, while in production you will see a unique identifier.

Unfortunately, the download_id is not well covered by Apple. It appears to be connected to the download transaction represented by original_application_version and original_purchase_date.

expiration_date, expiration_date_pst, expiration_date_ms

Illustrate the time at which the receipt expires for apps bought through the Volume Purchase Program. 

For the expiration_date the date-time format is similar to that of ISO 8601. For the expiration_date_pst it’s in the Pacific Time zone. Finally, for expiration_date_ms it’s in UNIX epoch time format, in milliseconds.

in_app

This is an array that contains in-app purchase receipt fields for all in-app purchase transactions.

original_application_version

Indicates the version of the app that the user originally acquired. If the originally purchased version was 2.5, the value in this field would be 2.5, even if currently the application runs on version 5.0. In the sandbox, the value is always “1.0”.

This number corresponds to CFBundleVersion (iOS) or CFBundleShortVersionString (macOS) from the Info.plist.

Original_purchase_date, original_purchase_date_pst, original_purchase_date_ms

Illustrate the time of the original app purchase. 

As was the case with the expiration_date property, the original_purchase_date has the date-time format similar to that of ISO 8601. original_purchase_date_pst is in the Pacific Time zone format. original_purchase_date_ms is in UNIX epoch time format, in milliseconds.

preorder_date, preorder_date_pst, preorder_date_ms

If the app was available for pre-orders, this field would indicate the time the user made the pre-order. 

preorder_date property has a date-time format similar to that of ISO 8601. preorder_date_pst is in the Pacific Time zone format and preorder_date_ms is in UNIX epoch time format, in milliseconds. 

receipt_creation_date, receipt_creation_date_pst, receipt_creation_date_ms

Specify the time at which the App Store generated the receipt.

You probably already got the idea, but let us detail the formats anyway. receipt_creation_date property has a date-time format similar to that of ISO 8601. Receipt_creation_date_pst is in the Pacific Time zone format and receipt_creation_date_ms is in UNIX epoch time format, in milliseconds.

receipt_type

Lists the type of the receipt that was generated. The value relates to the environment in which the app or VPP purchase was made. Possible values include:

  • Production: the receipt was created in the App Store production environment

  • ProductionVPP: the receipt was created in the VPP production environment

  • ProductionSandbox: the receipt was created in the App Store sandbox environment

  • Production VPPSandbox: the receipt was created in the VPP sandbox environment

request_date, request_date_pst, request_date_ms

Show the time the request to the verifyReceipt endpoint was processed and generated a response. 

For the request_date the date-time format is similar to that of ISO 8601. For the request_date_pst it’s in the Pacific Time zone. Finally, for request_date_ms it’s in UNIX epoch time format, in milliseconds.

version_external_identifier

A random number that specifies a revision of the app. In the sandbox, this value is “0”.

responseBody.latest_receipt_info Properties

responseBody.latest_receipt_info Properties

Time to move on to the properties of responseBody.latest_receipt_info

Hopefully, you already got used to our format. Just like before, let’s explore each of these keys individually.

cancellation_date, cancellation_date_pst, cancellation_date_ms

Indicate the time at which the subscription was canceled. A couple of things can cause this. First, Apple’s customer support team may have refunded the transaction. Second, if Family Sharing is set up, changes to access could lead to subscription cancellation. Finally, the user may have simply upgraded to a different product.

For the cancellation_date the date-time format is similar to that of ISO 8601. For the cancellation_date_pst it’s in the Pacific Time zone. Lastly, for cancellation_date_ms it’s in UNIX epoch time format, in milliseconds.

cancellation_reason

Lists the reason behind the refunded transaction. Possible values are 1 and 0. “1” means that the customer canceled because of an issue within your app. On the other hand, “0” means the cancellation occurred for another reason. Often, if the customer accidentally made the purchase.

expires_date, expires_date_pst, expires_date_ms

Illustrate the time at which the subscription will expire or renew.

Expires_date property has a date-time format similar to that of ISO 8601. expires_date_pst is in the Pacific Time zone format and expires_date_ms is in UNIX epoch time format, in milliseconds.

in_app_ownership_type

In this property, you will see whether the user is the purchaser of the product, or rather a family member with access to it through Family Sharing.

The value will either be FAMILY_SHARED or PURCHASED.

is_in_intro_offer_period

Illustrates if an auto-renewable subscription is in the introductory price period or not. Possible values are true and false. true indicates that the subscription is in fact in an introductory price period, while false negates it. 

is_trial_period

Specifies whether a subscription is in the free trial period or not.

is_upgraded

This property is only present for upgraded transactions and illustrates if a subscription has been canceled because of an upgrade. The value can only be true, signifying that this is what happened.

offer_code_ref_name

Only present when a subscription offer code was redeemed. Indicates the reference name of the offer that was set up in App Store Connect.

original_purchase_date, original_purchase_date_pst, original_purchase_date_ms

Illustrate the time of the original auto-renewable subscription purchase.

original_purchase_date property has a date-time format similar to that of ISO 8601. original_purchase_date_pst is in the Pacific Time zone format and original_purchase_date_ms is in UNIX epoch time format, in milliseconds.

original_transaction_id

Identifies the original purchase transaction.

product_id

Identifies the product purchased as set up for that product in App Store Connect.

promotional_offer_id

Identifies the user’s redeemed subscription offer.

purchase_date, purchase_date_pst, purchase_date_ms

Showcase the time at which the user’s account was charged by the App Store for a subscription purchase or renewal.

Purchase_date property has a date-time format similar to that of ISO 8601. purchase_date_pst is in the Pacific Time zone format and purchase_date_ms is in UNIX epoch time format, in milliseconds.

quantity

Illustrates the number of accessible products purchased. Usually 1-10, typically 1. 

subscription_group_identifier

Identifies the group to which the subscription belongs.

web_order_line_item_id

This is the primary key for identifying subscription purchases. It identifies purchase occurrences across devices.

transaction_id

Identifies purchases, restores, and renewals. The transaction is a purchase when transaction_id matches original_transaction_id. If they don’t match then it’s a restore or renewal.

responseBody.pending_renewal_info Properties

responseBody.pending_renewal_info Properties

Now, let’s take a look at responseBody.pending_renewal_info properties!

} ], "latest_receipt": "[new]", "pending_renewal_info": [ { "expiration_intent": "1", "auto_renew_product_id": "test.vip.6month.3d.3999.1", "is_in_billing_retry_period": "0", "product_id": "product.name.1", "original_transaction_id": "100000000000000", "auto_renew_status": "0" } ], "status": 0 } Copy

auto_renew_product_id

Identifies customer’s unresolved subscription renewal. You will only see this field if the user has downgraded or crossgraded to a different duration subscription for the following period. 

auto_renew_status

Indicates whether an auto-renewable subscription will renew at the end of the current subscription period. “1” illustrates that it will and “0” shows that the customer has canceled automatic renewal.

If you see the latter, the customer is at high risk of churn and it might be worth presenting him with a special offer.

expiration_intent

Showcases the reason for subscription expiry. Possible values are 1-5.

  • 1 = the customer canceled the subscription themselves. Consider sending a win-back campaign.

  • 2 = there was a billing error. Consider sending a reminder to update billing information to avoid losing access. 

  • 3 = the user didn’t agree to a price increase.

  • 4 = the product was unavailable at the time of renewal.

  • 5 = unknown

grace_period_expires_date, grace_period_expires_date_pst, grace_period_expires_date_ms

Signify the time when the subscription renewal grace period expires.

grace_period_expires_date property has a date-time format similar to that of ISO 8601. grace_period_expires_date_pst is in the Pacific Time zone format and Grace_period_expires_date_ms is in UNIX epoch time format, in milliseconds.

is_in_billing_retry_period

Illustrates if the customer’s auto-renewable subscription is in the billing retry period. Possible values are 1 and 0. The former indicates that the App Store is attempting to renew the subscription while the latter shows it has stopped attempting to renew. If the value is 1, this is a good time to send a billing update request. 

offer_code_ref_name

Showcases the reference name of the subscription offer you set up in App Store Connect. This field is only visible when a subscription offer code was redeemed.

original_transaction_id

Identifies the transaction of the original purchase.

price_consent_status

Indicates the consent status for the subscription price increase. The customer has either consented to the increase or hasn’t. 1 means that the customer has agreed and 0 illustrates that the customer has been notified of the price increase but hasn’t agreed to it yet.

product_id

Identifies that purchased product per your setup in App Store Connect.

Date Format Variations

Date Format Variations

You have probably noticed that throughout the decrypted receipt there are various date formats:

  • ISO 8601 -like GMT

  • ISO 8601 -like PST

  • UNIX epoch time in milliseconds

But do you already know what these date-time formats mean?

In its documentation, Apple refers to a data-time format similar to the ISO 8601 or an RFC 3339 date. Converting these string-based formats can be difficult, so let’s take a closer look.

As you have seen throughout this document, each base date has two modifiers: _pst and _ms. The former represents ISO 8601-like PST (Pacific Standard Time) and the latter represents milliseconds since UNIX epoch time.

A couple of things you should note:

  • Be extra careful during Pacific Daylight Time (PDT) because the value remains in PST time.

  • The easiest format to work with is UNIX epoch time in milliseconds because the majority of modern programming languages can convert it to a native date-time format.

Improve Your Receipt Management

Congratulations! You got through our rundown of decrypted receipt elements. 

Hopefully, you now have a better grasp on each of them, but don’t worry if things are still a bit overwhelming. You can always bookmark this page for future reference. 

Overall, you’ve probably noticed that decrypted receipts are quite complex. New features are added to the App Store every year and writing good code to manage receipts and leverage everything the Store has to offer is incredibly challenging.

We understand that you may not want to do all of this yourself. That’s precisely why we are here! 

If you want to avoid the hassle with StoreKit or don’t want to build your own subscription validation server, you can always turn to Qonversion! You can head to our Apple Receipt Checker right now or explore the Product Center which provides full in-app purchases infrastructure. As developers, you don’t have to perform this time-consuming receipts management anymore. Leave it to us.