Skip to main content
Developer · mobile-subscriptions

How to Verify Receipts for In-App Purchases on the App Store: A Definitive Guide

Learn how to verify receipts for in-app purchases on the App Store to ensure transaction integrity and protect your app from fraud.

***Update: *Since this guide was published, Apple announced StoreKit 2 which adds new capabilities and APIs such as Signed Transactions. StoreKit 1-style receipts and APIs will continue to be available, so we hope this article will continue to be a useful reference. If you’re interested in learning more about StoreKit 2, head over here. Also, the new APIs are a major change and another and a great time to build atop Nami which abstracts away these underlying implementation details.

What’s an App Store Receipt?

An App Store receipt is a digital proof of purchase generated by Apple for any transaction made within the App Store, including app purchases, in-app purchases, and subscriptions. This receipt is a critical component in ensuring the integrity and authenticity of transactions, as it contains detailed information about the purchase, such as the product ID, transaction ID, purchase date, and subscription details if applicable.

The receipt is stored on the user’s device and can be retrieved and verified to confirm that the user has made a legitimate purchase. This verification process typically involves sending the receipt data to Apple’s servers, where it is checked for validity and authenticity. By verifying receipts, app developers can protect their apps from fraudulent transactions and unauthorized access to premium content or features.

The receipt data provided by StoreKit is a Base64-encoded. To receive a decoded version, described in detail in this article, you need to transmit the encoded receipt to Apple’s verifyReceipt endpoint. To fully understand verifyReceipt, see this article where we code a simple Python script step-by-step that demonstrates how to send an encoded receipt and receive the decoded receipt.

Why Does it Need to be Verified?

Why is all of this necessary? The two most important reasons:

  1. Receipt validation helps you ensure a user is getting access to what they purchased — which is especially important with subscriptions
  2. Validating receipts from a server-side provides an important safeguard against piracy

The mechanics of receipt validation are relatively straightforward as demonstrated in the Python script referenced early.

However, grokking the decoded receipt itself is painful. It’s not the most elegant data structure in the world, and there are all sorts of esoteric details and nuances. Our goal with this article is build upon and go beyond Apple’s documentation to help you better understand the decoded receipt and avoid common (and more exotic) pitfalls.

How To Validate Receipts in App Store

1. Obtain the Receipt Data

The first step is to obtain the receipt data from the app. This data can be retrieved using the SKReceiptRefreshRequest class or from the receipt URL located in the app’s bundle.

if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL,
  FileManager.default.fileExists(atPath: appStoreReceiptURL.path) {
   do {
       let receiptData = try Data(contentsOf: appStoreReceiptURL, options: .alwaysMapped)
       let receiptString = receiptData.base64EncodedString(options: [])
       // Send receiptString to your server for verification
   } catch {
       print(“Couldn’t read receipt data with error: ” + error.localizedDescription)
   }
}

2. Send Receipt Data to Your Server

Once you have the receipt data, you need to send it to your server. This step is crucial because the verification process should be performed on a secure server to prevent tampering. Your server will then forward the receipt data to Apple’s verification server.

3. Verify Receipt with Apple

Your server should send a request to Apple’s verification server at the following URL:

  • For production: https://buy.itunes.apple.com/verifyReceipt
  • For sandbox: https://sandbox.itunes.apple.com/verifyReceipt

The request should include the receipt data and your app’s shared secret for subscriptions. Here’s an example using Swift’s URLSession to send the receipt data:

let requestDictionary: [String: Any] = [“receipt-data”: receiptString, “password”: “your_shared_secret”] guard JSONSerialization.isValidJSONObject(requestDictionary) else { return } do {    let requestData = try JSONSerialization.data(withJSONObject: requestDictionary, options: [])    let storeURL = URL(string: “https://buy.itunes.apple.com/verifyReceipt”)!    var request = URLRequest(url: storeURL)    request.httpMethod = “POST”    request.httpBody = requestData    request.setValue(“application/json”, forHTTPHeaderField: “Content-Type”)    let task = URLSession.shared.dataTask(with: request) { data, response, error in        guard error == nil, let data = data else {            print(“Error verifying receipt: \(error?.localizedDescription ?? “No data”)”)            return        }        // Process the verification response    }    task.resume() } catch {    print(“JSON serialization failed: \(error.localizedDescription)”) }

4. Handle the Response

Apple’s response will include the status of the receipt and, if valid, the receipt’s details. Your server should parse this response and take appropriate action, such as granting the purchased content or renewing a subscription.

let requestDictionary: [String: Any] = [“receipt-data”: receiptString, “password”: “your_shared_secret”]
guard JSONSerialization.isValidJSONObject(requestDictionary) else { return }

do {
   let requestData = try JSONSerialization.data(withJSONObject: requestDictionary, options: [])
   let storeURL = URL(string: “https://buy.itunes.apple.com/verifyReceipt”)!
   var request = URLRequest(url: storeURL)
   request.httpMethod = “POST”
   request.httpBody = requestData
   request.setValue(“application/json”, forHTTPHeaderField: “Content-Type”)

   let task = URLSession.shared.dataTask(with: request) { data, response, error in
       guard error == nil, let data = data else {
           print(“Error verifying receipt: \(error?.localizedDescription ?? “No data”)”)
           return
       }
       // Process the verification response
   }
   task.resume()
} catch {
   print(“JSON serialization failed: \(error.localizedDescription)”)
}

The Receipt Decoded: Element-by Element

The responseBody JSON payload can be daunting (example), especially if the receipt belongs to an end-user with a lengthy past purchase history.

It turns out, that there are somewhere between 3 and 7 top-level keys in the verifyReceipt responseBody JSON data structure. Here are the keys:

responseBody

Some of the top-level keys are present only in certain circumstances. Here’s a summary of each top-level key, what it is, and when you should expect to see it in the responseBody:

   

             

                 

Key

             

             

               

Type

           

             

                 

Is Returned?

             

   

         

             

                 

status

             

             

               

Integer

           

             

✓              

   

   

       

           

is-retryable

       

       

         

Integer

     

       

Receipts with status code from 21100-21199        

   

       

       

           

environment

       

       

         

String

     

     

✓      

   

   

       

           

receipt

       

       

       

JSON Object

   

   

✓    

   

   

       

           

latest_receipt

       

       

       

Byte

   

   

Receipts with auto-renewable subscriptions    

   

   

       

           

latest_receipt_info

       

       

       

Array of JSON objects

   

   

Receipts with auto-renewable subscriptions    

   

   

       

           

pending_renewal_info

       

       

           

Array of JSON objects

       

   

Receipts with auto-renewable subscriptions    

   

Let’s dig into each of these top-level elements in detail, starting with response metadata keys: status, is-retryable, environment.

status

Upon received a response from verifyReceipt, the first thing you should do is check that status key. If the value is 0, the receipt is valid. Otherwise, an error occurred and you will need to interpret the status code to know what to do next.

   

             

                 

HTTP Status Code

             

             

               

Apple Response Status Code

           

             

                 

Description

             

             

               

Resolution

           

   

         

             

                 

200

             

             

               

0

             

             

The receipt is valid

             

-

   

   

     

         

200

     

     

       

21000

     

     

The request was not made using HTTP Post

     

Change HTTP request method to POST

   

   

     

         

200

     

     

       

21001

     

     

Deprecated status code no longer sent

     

-

   

   

     

         

200

     

     

       

21002

     

     

Malformed receipt-data property sent in the request

     

Check that the receipt-data property of the request (sample code)

   

   

     

         

200

     

     

       

21002

     

     

The receipt server experienced a temporary issue

     

Try to validate this receipt again later

   

   

     

         

200

     

     

       

21003

     

     

The receipt could not be authenticated

     

Do not grant the user access to the purchased content

   

   

     

         

200

     

     

       

21004

     

     

The shared secret sent in the request is incorrect

     

Send the correct app-specific shared secret per App Store Connect

   

   

     

         

200

     

     

       

21005

     

     

The receipt server was temporarily unable to provide the receipt

     

Try to validate this receipt again later

   

   

     

         

200

     

     

       

21006

     

     

This receipt is valid but the subscription has expired. (Only returned for iOS 6-style receipts for auto-renewable subscriptions)

     

Upgrade to iOS 7+ style receipts in your app code from Bundle.main.appStoreReceiptURL

   

     

     

         

200

     

     

       

21007

     

     

This is a sandbox receipt sent to the production environment

     

Send this receipt to the sandbox endpoint: https://sandbox.itunes.apple.com/verifyReceipt

   

   

     

       

200

   

     

       

21008

     

     

This is a production receipt sent to the sandbox environment

     

Send this receipt to the production endpoint: https://buy.itunes.apple.com/verifyReceipt

   

   

     

       

200

   

     

       

21009

     

     

Internal data access error

     

Try to validate this receipt again later

   

   

     

       

200

   

     

       

21010

     

     

The Apple ID cannot be found or has been deleted

     

-

   

   

     

       

200

   

     

       

21100-21199

     

     

Various Apple internal data access errors

     

Check is-retryable to determine if you should try again

   

   

     

         

500

     

     

       

-

     

     

Internal Service Error

     

Implement retry logic or add request to a job queue for future processing

 

 

   

       

502

   

   

     

-

   

   

Bad Gateway

   

Implement retry logic or add request to a job queue for future processing

   

       

           

503

       

       

         

-

       

       

Service Unavailable

       

Implement retry logic or add request to a job queue for future processing

   

ProTip: Notice that the resolution to a number of the status codes is to retry later. It’s important that your receipt validation service is robust enough to handle a wide variety of scenarios including: network timeouts, HTTP status codes such as 503, and also status codes embedded in the return payload of a  HTTP 200 response. In some of these cases, you may want to to support retry logic to try again immediately. If you still failed to validate the receipt, you may want to add them to job queue for a future processing attempt.

is-retryable

If the Apple status value is within the range of 21100-21199, the is-retryable key should be present in the response payload. Apple’s documentation says the type is boolean, but the provided values are 0 and 1, not the proper JSON boolean values of true and false.

   

             

                 

Value

             

             

                 

Description

             

   

         

       

           

1

             

             

 Temporary issue - try to validate this receipt again later              

   

     

           

               

0

           

           

 Unresolvable issue - do not retry validating this receipt                          

   

 

environment

The environment value tells you which Apple environment the receipt was generated from.

 

           

               

Value

           

           

               

Description

           

 

     

           

               

Sandbox

           

           

This receipt was generated from the sandbox environment (Development builds)            

 

 

           

               

Production

           

           

This receipt was generated from the production environment (Ad-Hoc, TestFlight, App Store builds)            

 

Now let’s look beyond the metadata into the primary keys containing the responseBody’s substance: receipt, latest_receipt_info, pending_renewal_info.

receipt

The value of receipt, a key within the decoded receipt response, is a JSON object containing the decoded receipt data.

This key always exists, whether a user has made an in-app purchase or not.

In fact, if you follow the best practice and send receipt data from your app to a server you will end up storing and decoding (via receipt validation) many receipts, containing this receipt key with certain metadata even if there have be no in-app purchases.

Consider the history of the App Store. It was built atop the foundations of the iTunes Music Store. On iTunes, an email receipt was sent to the user whether the song or album cost money or was free. This lineage carried on to the App Store, where in the early days there were just two types of apps: paid and free.  

Once in-app purchases were added, additional context was introduced to this data structure. In fact, it contains the full context you need to understand the purchase.

Now, with the advent of subscriptions things have gotten a bit more complicated. Some context about the subscription in-app purchase exists in this data structure, but some does not. For auto-renewable subscriptions, you will find additional context inside the latest_receipt_info and pending_renewal_info top level keys which we go into more detail later in this article.

Jump to an element-by-element breakdown of the receipt data structure.

latest_receipt

The latest Base64 encoded app receipt. This field is only returned if the app receipts contains an auto-renewable subscriptions.

latest_receipt_info

An array of JSON objects representing in-app purchase transactions for auto-renewable subscriptions. This field is only returned if the app receipts contains an auto-renewable subscriptions.

ProTip: If your verifyReceipt request includes the field exclude-old-transactions set to true, only the latest auto-renewable subscription transaction will be included in this field in the response.

Jump to an element-by-element breakdown of the latest_receipt_info data structure.

pending_renewal_info

An array of JSON objects representing open (pending) or failed auto-renewable subscription renewals as identified by the product_id. This field is only returned for app receipts that contain auto-renewable subscriptions.

Jump to an element-by-element breakdown of the pending_renewal_info data structure.

responseBody.receipt

Let’s take a look at all of the possible elements you may encounter in the receipt JSON object.

   

             

                 

Key

             

             

               

Type

           

             

                 

Is Returned?

             

   

         

             

                 

adam_id

             

             

               

Integer

           

             

✓              

   

   

       

           

app_item_id

       

       

         

Integer

     

     

✓    

   

       

       

           

application_version

       

       

         

String

     

     

✓      

   

   

       

           

bundle_id

       

       

       

String

   

   

✓    

   

   

       

           

download_id

       

       

       

Integer

   

   

✓    

   

   

       

           

expiration_date

       

       

       

String

   

   

Returned for apps purchased via VPP    

   

   

       

           

expiration_date_pst

       

       

       

String

   

   

Returned for apps purchased via VPP    

   

   

       

           

expiration_date_ms

       

       

       

String

   

   

Returned for apps purchased via VPP    

   

   

       

           

in_app

       

       

       

Array

   

   

✓    

   

   

       

           

original_application_version

       

       

       

String

   

   

✓    

   

   

       

           

original_purchase_date

       

       

       

String

   

   

✓    

   

   

       

           

original_purchase_date_pst

       

       

       

String

   

   

✓    

   

   

       

           

original_purchase_date_ms

       

       

       

String

   

   

✓    

   

   

       

           

preorder_date

       

       

       

String

   

   

Returned if user pre-ordered app    

   

   

       

           

preorder_date_pst

       

       

       

String

   

   

Returned if user pre-ordered app    

   

   

       

           

preorder_date_ms

       

       

       

String

   

   

Returned if user pre-ordered app    

   

       

           

receipt_creation_date

       

       

       

String

   

   

✓    

   

   

       

           

receipt_creation_date_pst

       

       

       

String

   

   

✓    

   

   

       

           

receipt_creation_date_ms

       

       

       

String

   

   

✓    

   

   

       

           

receipt_type

       

       

       

String

   

   

✓    

   

   

       

           

request_date

       

       

       

String

   

   

✓    

   

   

       

           

request_date_pst

       

       

       

String

   

   

✓    

   

   

       

           

request_date_ms

       

       

       

String

   

   

✓    

   

   

       

           

version_external_identifier

       

       

           

Integer

       

       

✓        

   

adam_id / app_item_id

A unique 64-bit long integer generated by App Store Connect. This is used by the App Store to unique identify the app associated with the receipt.

In production, expect a unique identifier. In sandbox, this field will be populated with a 0.

application_version

The current version of the app installed on the user’s device at the time of the receipt’s receipt_creation_date_ms. The version number is based upon the CFBundleVersion (iOS) or CFBundleShortVersionString (macOS) from the Info.plist.

In the sandbox, the value is always “1.0”.

bundle_id

The bundle identifier for the app to which the receipt belongs. You provide this string on App Store Connect and it corresponds to the value of CFBundleIdentifier in the Info.plist file of the app.

download_id

A unique identifier for the app download transaction.

While not well documented by Apple, this unique value appears to be tied to the download transaction represented by original_application_version and original_purchase_date.

In production, expect a unique identifier. In sandbox, this field has been observed to be populated with a 0.

expiration_date

The time the receipt expires for apps purchased through the Volume Purchase Program (VPP).

 

           

               

Key

           

           

               

Format

           

 

     

           

               

expiration_date

           

           

ISO 8601-like GMT            

 

 

   

       

expiration_date_pst

   

   

       ISO 8601-like PST    

 

           

               

expiration_date_ms

           

           

               Milliseconds since Unix epoch time            

 

expiration_date_pst

See expiration_date

expiration_date_ms

See expiration_date

in_app

An array containing in-app purchase receipt fields for all in-app purchase transactions.

original_application_version

The original version of the app installed on the user’s device. For example, if the current app version is 2.0 but the user originally had version 1.8 installed, this value would be “1.8”. For the current version installed, see application_version.

The version number is based upon the CFBundleVersion (iOS) or CFBundleShortVersionString (macOS) from the Info.plist.

In the sandbox, the value is always “1.0”.

original_purchase_date

The time of the original app purchase.

 

           

               

Key

           

           

               

Format

           

 

     

           

               

original_purchase_date

           

           

ISO 8601-like GMT            

 

 

   

       

original_purchase_date_pst

   

   

       ISO 8601-like PST    

 

           

               

original_purchase_date_ms

           

           

               Milliseconds since Unix epoch time            

 

original_purchase_date_pst

See original_purchase_date

original_purchase_date_ms

See original_purchase_date

preorder_date

The time the user ordered the app available for pre-order. This field is only present if the user pre-orders the app.

 

           

               

Key

           

           

               

Format

           

 

     

           

               

preorder_date

           

           

ISO 8601-like GMT            

 

 

   

       

preorder_date_pst

   

   

       ISO 8601-like PST    

 

           

               

preorder_date_ms

           

           

               Milliseconds since Unix epoch time            

 

preorder_date_pst

See preorder_date

preorder_date_ms

See preorder_date

receipt_creation_date

The time the App Store generated the receipt.

 

           

               

Key

           

           

               

Format

           

 

     

           

               

receipt_creation_date

           

           

ISO 8601-like GMT            

 

 

   

       

receipt_creation_date_pst

   

   

       ISO 8601-like PST    

 

           

               

receipt_creation_date_ms

           

           

               Milliseconds since Unix epoch time            

 

receipt_creation_date_pst

See receipt_creation_date

receipt_creation_date_ms

See receipt_creation_date

receipt_type

The type of receipt generated. The value corresponds to the environment in which the App Store or VPP purchase was made.

   

             

                 

Value

             

             

                 

Description

             

   

       

           

               

Production

           

           

               This receipt was generated in App Store production environment.            

   

   

       

           

ProductionVPP

       

       

           This receipt was generated in VPP production environment.                      

   

   

       

           

Sandbox

       

       

           This receipt was generated in App Store sandbox environment.        

   

   

       

           

ProductionVPPSandbox

       

       

           This receipt was generated in VPP sandbox environment.                      

   

 

request_date

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

 

           

               

Key

           

           

               

Format

           

 

     

           

               

request_date

           

           

ISO 8601-like GMT            

 

 

   

       

request_date_pst

   

   

       ISO 8601-like PST    

 

           

               

request_date_ms

           

           

               Milliseconds since Unix epoch time            

 

request_date_pst

See request_date

request_date_ms

See request_date

version_external_identifier

An arbitrary number that identifies a revision of your app. In the sandbox, this key’s value is “0”.

responseBody.latest_receipt_info

An array of JSON object(s) found at latest_receipt_info contains in-app purchase transactions for auto-renewable subscriptions. This field is only returned if the app receipts contains an auto-renewable subscriptions.

ProTip: If your verifyReceipt request includes the field exclude-old-transactions set to true, only the latest auto-renewable subscription transaction will be included in this field in the response.

   

             

                 

Key

             

             

               

Type

           

             

                 

Is Returned?

             

   

         

             

                 

cancellation_date

             

             

               

String

           

             

Only present for transactions refunded by the App Store, for Family Sharing access revocations, or upgrades to a different product.              

   

   

       

           

cancellation_date_pst

       

       

         

String

     

     

Only present for transactions refunded by the App Store, for Family Sharing access revocations, or upgrades to a different product.    

   

       

       

           

cancellation_date_ms

       

       

         

String

     

     

Only present for transactions refunded by the App Store, for Family Sharing access revocations, or upgrades to a different product.    

   

   

       

           

cancellation_reason

       

       

       

String

   

   

Only present for transactions refunded by the App Store, for Family Sharing access revocations, or upgrades to a different product.    

   

   

       

           

expires_date

       

       

       

String

   

   

✓    

   

   

       

           

expires_date_pst

       

       

       

String

   

   

✓    

   

   

       

           

expires_date_ms

       

       

       

String

   

   

✓    

   

   

       

           

in_app_ownership_type

       

       

       

String

   

   

Only present if Family Sharing is enabled.    

   

   

       

           

is_in_intro_offer_period

       

       

       

String

   

   

✓    

   

   

       

           

is_trial_period

       

       

       

String

   

   

✓    

   

   

       

           

is_upgraded

       

       

       

String

   

   

Only present for upgrade transactions.    

   

   

       

           

offer_code_ref_name

       

       

       

String

   

   

Only present when a customer redeemed a subscription offer code.    

   

   

       

           

original_purchase_date

       

       

       

String

   

   

✓    

   

   

       

           

original_purchase_date_pst

       

       

       

String

   

   

✓    

   

   

       

           

original_purchase_date_ms

       

       

       

String

   

   

✓    

   

   

       

           

original_transaction_id

       

       

       

String

   

   

✓    

   

   

       

           

product_id

       

       

       

String

   

   

✓    

   

   

       

           

original_transaction_id

       

       

       

String

   

   

✓    

   

   

       

           

promotional_offer_id

       

       

       

String

   

   

Only present when a customer redeemed a promotional offer.    

   

   

       

           

purchase_date

       

       

       

String

   

   

✓    

   

   

       

           

purchase_date_pst

       

       

       

String

   

   

✓    

   

   

       

           

purchase_date_ms

       

       

       

String

   

   

✓    

   

   

       

           

quantity

       

       

       

String

   

   

✓    

   

   

       

           

subscription_group_identifier

       

       

       

String

   

   

✓    

   

   

       

           

web_order_line_item_id

       

       

       

String

   

   

✓    

   

   

       

           

transaction_id

       

       

       

String

   

   

✓    

   

cancellation_date

The time of the subscription was canceled. This can happen for one of the following reasons:

  1. The App Store or Apple customer support refunds the subscription transaction
  2. A Family Sharing change causes the user to lose access to the subscription
  3. The user upgrades to different product.

   

             

                 

Key

             

             

                 

Format

             

   

         

             

                 

cancellation_date

             

             

 ISO 8601-like GMT              

   

   

     

         

cancellation_date_pst

     

     

         ISO 8601-like PST      

 

   

             

                 

cancellation_date_ms

             

             

                 Milliseconds since Unix epoch time              

   

 

cancellation_date_pst

See cancellation_date

cancellation_date_ms

See cancellation_date

cancellation_reason

The reason for a refunded transaction. When a customer cancels a transaction, the App Store gives them a refund and provides a value for this key.

   

             

                 

Value

             

             

                 

Description

             

   

       

           

               

“1”

           

           

The customer canceled due to an actual or perceived issue within your app.                          

   

   

       

           

“0”

       

       

The transaction was canceled for another reason; for example, if the customer made the purchase accidentally.                      

 

expires_date

The time a subscription expires or when it will renew.

   

             

                 

Key

             

             

                 

Format

             

   

         

             

                 

expires_date

             

             

 ISO 8601-like GMT              

   

   

     

         

expires_date_pst

     

     

         ISO 8601-like PST      

 

   

             

                 

expires_date_ms

             

             

                 Milliseconds since Unix epoch time              

   

 

expires_date_pst

See expires_date

expires_date_ms

See expires_date

in_app_ownership_type

Indicates whether the user is the purchaser of the product, or is a family member with access to the product through Family Sharing.

   

             

                 

Value

             

             

                 

Description

             

   

       

           

               

FAMILY_SHARED

           

           

The transaction belongs to a family member who benefits from service.                          

   

   

       

           

PURCHASED

       

       

The transaction belongs to the purchaser.                      

 

is_in_intro_offer_period

Indicates whether or not an auto-renewable subscription is in the introductory price period.

   

             

                 

Value

             

             

                 

Description

             

   

       

           

               

“true”

           

           

The customer’s subscription is in an introductory price period.                          

   

   

       

           

“false”

       

       

           The subscription is not in an introductory price period.                      

 

is_trial_period

An indicator of whether a subscription is in the free trial period.

   

             

                 

Value

             

             

                 

Description

             

   

       

           

               

“true”

           

           

The customer’s subscription is in a free trial period.                          

   

   

       

           

“false”

       

       

The customer’s subscription is not in a free trial period.                      

 

is_upgraded

Indicates a subscription has been canceled due to an upgrade. Only present for upgrade transactions.

Check cancellation_date to determine when the cancellation occurred.

   

             

                 

Value

             

             

                 

Description

             

   

       

           

               

“true”

           

           

The customer’s subscription has been canceled due to an upgrade                          

   

 

offer_code_ref_name

The reference name of a subscription offer that you configured in App Store Connect. Present when a customer redeemed a subscription offer code.

original_purchase_date

The time of the original auto-renewable subscription purchase.

   

             

                 

Key

             

             

                 

Format

             

   

         

             

                 

original_purchase_date

             

             

 ISO 8601-like GMT              

   

   

     

         

original_purchase_date_pst

     

     

         ISO 8601-like PST      

 

   

             

                 

original_purchase_date_ms

             

             

                 Milliseconds since Unix epoch time              

   

 

original_purchase_date_pst

See original_purchase_date

original_purchase_date_ms

See original_purchase_date

original_transaction_id

The transaction identifier of the original purchase.

product_id

The unique identifier for the product purchased as configured for that product in App Store Connect.

promotional_offer_id

The identifier of the subscription offer redeemed by the user.

purchase_date

The time the App Store charged the user’s account for a subscription purchase or renewal.

 

           

               

Key

           

           

               

Format

           

 

     

           

               

purchase_date

           

           

ISO 8601-like GMT            

 

 

   

       

purchase_date_pst

   

   

       ISO 8601-like PST    

 

           

               

purchase_date_ms

           

           

               Milliseconds since Unix epoch time            

 

purchase_date_pst

See purchase_date

purchase_date_ms

See purchase_date

quantity

The number of consumable products purchased. Included but meaningless in the latest_receipt_info data structure, which is for auto-renewable subscriptions only. Likely an artifact from Apple internally sharing data structures between responseBody.receipt.in_app and responseBody.latest_receipt_info.

subscription_group_identifier

The identifier of the subscription group to which the subscription belongs.

web_order_line_item_id

A unique identifier for purchase events across devices, including subscription-renewal events. This value is the primary key for identifying subscription purchases.

transaction_id

A unique identifier for a transaction such as a purchase, restore, or renewal.

If transaction_id and original_transaction_id match, the transaction is a purchase. Otherwise, the transaction is a restore or renewal.

responseBody.pending_renewal_info

The array of JSON object(s) found at pending_renewal_info for open (pending) or failed auto-renewable subscription renewals includes a number of possible fields. Let’s take a look.

   

             

                 

Key

             

             

               

Type

           

             

                 

Is Returned?

             

   

         

             

                 

auto_renew_product_info

             

             

               

String

           

             

Only present if the user downgrades or crossgrades to a subscription of a different duration for the subsequent subscription period.              

   

   

       

           

auto_renew_status

       

       

         

String

     

     

✓    

   

       

       

           

expiration_intent

       

       

         

String

     

     

Only present for a receipt containing an expired auto-renewable subscription.    

   

   

       

           

grace_period_expires_date

       

       

       

String

   

   

Only present if user experiences a billing error at the time of renewal.    

   

   

       

           

grace_period_expires_date_pst

       

       

       

String

   

   

Only present if user experiences a billing error at the time of renewal.    

   

   

       

           

grace_period_expires_date_ms

       

       

       

String

   

   

Only present if user experiences a billing error at the time of renewal.    

   

   

       

           

is_in_billing_retry_period

       

       

       

String

   

   

Only present if an auto-renewable subscription is in the billing retry state.    

   

   

       

           

offer_code_ref_name

       

       

       

String

   

   

Only present when a customer redeemed a subscription offer code.    

   

   

       

           

original_transaction_id

       

       

       

String

   

   

✓    

   

   

       

           

price_consent_status

       

       

       

String

   

   

Only present if the customer was notified of the price increase.    

   

   

       

           

product_id

       

       

       

String

   

   

✓    

   

auto_renew_product_id

The product identifier for the customer’s pending subscription renewal, if the user has downgraded or crossgraded to a subscription of a different duration for the subsequent subscription period.

auto_renew_status

The renewal status for the auto-renewable subscription.

If this value is “0”, the customer is showing a likelihood to churn out of the subscription at the end of the current renewal period. A best practice is to present the user with an attractive upgrade or downgrade offer in the same subscription group.

   

             

                 

Value

             

             

                 

Description

             

   

         

             

                 

“1”

             

             

The subscription will renew at the end of the current subscription period.              

   

     

             

                 

“0”

             

             

The customer has turned off automatic renewal for the subscription.              

   

     

expiration_intent

The reason a subscription expired.

   

             

                 

Value

             

             

                 

Description

             

   

         

             

                 

“1”

             

             

The customer voluntarily canceled their subscription.              

   

   

       

           

“2”

       

       

Billing error; for example, the customer’s payment information was no longer valid.        

   

       

“3”

   

   

The customer did not agree to a recent price increase.    

   

       

“4”

   

             

The product was not available for purchase at the time of renewal.    

   

             

                 

“5”

             

             

Unknown error.              

   

 

If this value is “1”, you may want to offer the user an alternative subscription or a “winback” offer.

If the value is “2”, you may want to prompt them to subscribe to the same product again since they churned involuntarily.

grace_period_expires_date

The time at which the grace period for subscription renewals expires.

 

           

               

Key

           

           

               

Format

           

 

     

           

               

grace_period_expires_date

           

           

ISO 8601-like GMT            

 

 

   

       

grace_period_expires_date_pst

   

   

       ISO 8601-like PST    

 

           

               

grace_period_expires_date_ms

           

           

               Milliseconds since Unix epoch time            

 

grace_period_expires_date_pst

See grace_period_expires_date

grace_period_expires_date_ms

See grace_period_expires_date

is_in_billing_retry_period

Indicates whether the user’s auto-renewable subscription is in the billing retry period.

   

             

                 

Value

             

             

                 

Description

             

   

         

             

                 

“1”

             

             

The App Store is attempting to renew the subscription.              

   

     

             

                 

“0”

             

             

The App Store has stopped attempting to renew the subscription.              

   

     

If the value is “1”, consider prompting the user to update their billing information.

offer_code_ref_name

The reference name of a subscription offer that you configured in App Store Connect. Present when a customer redeemed a subscription offer code.

original_transaction_id

The transaction identifier of the original purchase.

The price consent status for a subscription price increase.

   

             

                 

Value

             

             

                 

Description

             

   

         

             

                 

“1”

             

             

The customer has consented to the price increase.              

   

     

             

                 

“0”

             

             

The customer has been notified of the pending price increase              

   

     

product_id

The unique identifier for the product purchased as configured for that product in App Store Connect.

Receipt Date Formats

Throughout the Apple decoded receipt payload, you will find three different date formats expressed whenever a date is provided:****

  • ISO 8601-like GMT
  • ISO 8601-like PST
  • UNIX Epoch Time in milliseconds.

What is a date-time format similar to the ISO 8601?

What is ISO 8601-like? Apple documentation refers to “a date-time format similar to the ISO 8601” and “an RFC 3339 date”. In reality it’s not strictly either. The timezone part of the string make it difficult to convert these string-based date formats into date time object using common libraries which except ISO 8601 or other common standards.

Why Apple uses this specific non-standard format is probably an esoteric story related to the origins of NeXT or WebObjects.

   

             

                 

Key Example

             

             

               

Date Format

           

             

                 

Date Example

             

   

         

             

                 

receipt_creation_date

             

             

               

ISO 8601-like GMT

           

             

2021-01-14 17:11:10 Etc/GMT              

   

   

       

           

receipt_creation_date_pst

       

       

         

ISO 8601-like PST

     

       

2021-01-14 17:11:10 America/Los_Angeles        

   

   

       

           

receipt_creation_date_ms

       

       

           

Milliseconds since UNIX epoch time

       

   

16106442700000    

   

To dive deeper into these three formats, let’s take a look at the date found at responseBody.receipt.receipt_creation_date. This is date the receipt was created expressed in the ISO 8601-like format relative to Greenwich Meantime (GMT).

Each base date key has two modifiers: _pst* and *_ms with the ISO 8601-like PST and Milliseconds since Unix epoch representations.

responseBody.receipt.receipt_creation_date_pst* is an ISO 8601-like date relative to Pacific Standard Time (PST). *Please note, even though the timezone modifier is provided in the format America/Los_Angeles instead of a UTC offset, the date provided is indeed Pacific Standard Time. Your code needs to be careful during Pacific Daylight Time (PDT), since this value will still be a Pacific Standard Time date.

responseBody.receipt.receipt_creation_date_ms is when the receipt was created in the number of milliseconds since the Unix epoch time.  The _ms modifier for all date fields in an Apple decoded receipt is the far easiest to work with. Most modern languages can convert this to a native date time object out of the box or with commonly used libraries.

What’s VPP?

VPP stands for Volume Purchase Program. It’s essentially how Apple provides an App Store-like experience for educational institutions or businesses to acquire and deploy apps at scale. As it relates to this article, some receipt fields only show up or contain values for transactions that occur on the VPP store.

There is a Better Way to Manage Receipts: Don’t

As this guide illustrates, understanding the decoded receipt is complicated. So is the code necessary to store, validate, and make meaning out of the decoded receipt so you can properly manage your paid customer base.

The esoteric and sometimes arcane receipt response is the result of years of change and new features bolted on to the App Store year after year. It’s incredibly complex to write rock solid code to manage receipts and leverage all the App Store ecosystem has to offer related to in-app purchase and subscription monetization.

That’s where Nami can help. We’ve built a complete service to help you sell in-app purchases or subscriptions. In addition to a variety of features, we handle all client and server side details involving the receipt. In fact, you also don’t have to write a single line of StoreKit 1 or StoreKit 2 code.

Our code has encountered many of the hard to test edge cases you’ll see encounter over time if you do-it-yourself. Our service has extensive test coverage and operates at scale.

This means you can stay focused on building your app experience. If you’re interested in learning more, we’d love to chat.

{  “@context”: “https://schema.org”,  “@type”: “HowTo”,  “name”: “Verify App Store Receipt Data”,  “step”: [    {      “@type”: “HowToStep”,      “name”: “Obtain the Receipt Data”,      “text”: “Obtain the receipt data from the app using SKReceiptRefreshRequest or from the receipt URL in the app’s bundle.”,      “itemListElement”: {        “@type”: “HowToDirection”,        “text”: “Fetch receipt data using Swift code snippet.”,        “url”: “https://www.namiml.com/blog/app-store-verify-receipt-definitive-guide”      }    },    {      “@type”: “HowToStep”,      “name”: “Send Receipt Data to Your Server”,      “text”: “Send the obtained receipt data securely to your server.”,      “itemListElement”: {        “@type”: “HowToDirection”,        “text”: “Send receipt data using Swift code snippet.”,        “url”: “https://www.namiml.com/blog/app-store-verify-receipt-definitive-guide”      }    },    {      “@type”: “HowToStep”,      “name”: “Verify Receipt with Apple”,      “text”: “Send a request to Apple’s verification server with the receipt data and your app’s shared secret.”,      “itemListElement”: {        “@type”: “HowToDirection”,        “text”: “Verify receipt using Swift code snippet.”,        “url”: “https://www.namiml.com/blog/app-store-verify-receipt-definitive-guide”      }    },    {      “@type”: “HowToStep”,      “name”: “Handle the Response”,      “text”: “Process the verification response from Apple and take appropriate actions based on the receipt status and details.”,      “itemListElement”: {        “@type”: “HowToDirection”,        “text”: “Handle response using Swift code snippet.”,        “url”: “https://www.namiml.com/blog/app-store-verify-receipt-definitive-guide”      }    }  ] }

Ready to unify your subscription experience?

Join leading media and app brands using Nami's subscription orchestration platform.