
After you have processed your In-App Purchases you may want to verify the receipt with the App Store directly. You’ll get more details about the purchase so you can store that information in your database. There are two ways: first connecting to the Apple App Store directly within your app, and second send the receipt data to your own server and having your server perform the validation with the App Store server. The second is the easiest and most secure method when verifying receipts since it protects against Man-in-the-middle (MITM) attacks where its possible to get free in-app purchases in any app.
Apple recommends you use a trusted server to communicate with the App Store. Using your own server lets you design your app to recognize and trust only your server, and lets you ensure that your server connects with the App Store server. It is not possible to build a trusted connection between a user’s device and the App Store directly because you don’t control either end of that connection.
Validate In-App Purchases
To validate an in-app purchase, your application performs the following tests, in order:
- Parse and validate the application’s receipt, as described in the previous sections. If the receipt is not valid, none of the in-app purchases are valid.
- Parse the in-app purchase receipts. Each in-app purchase receipt consists of a set of attributes, like the application’s receipt does.
- Examine the product identifier for each in-app purchase receipt and enable the corresponding functionality or content in your app.
If validation of an in-app purchase receipt fails, your application simply does not enable the functionality or content.
To begin, be sure you already have In-App Purchases implement in your app. You can follow the steps in this article to integrate In-App Purchases: Tutorial: In-App Purchases (IAP) in Swift.
Send Receipt Data to your Server
Use the validateReceipt() method below to retrieve the receipt data, then send this data to your server. Submit the receipt object as the payload of an HTTP POST request, and encode the data using base64 encoding. Make sure you change the request URL to point to the PHP script on your server. Next, if the PHP script communicates correctly with Apple’s server you will need to parse the response with all of the receipt and transaction information.
func validateReceipt() { var response: NSURLResponse? var error: NSError? var receiptUrl = NSBundle.mainBundle().appStoreReceiptURL var receipt: NSData = NSData(contentsOfURL:receiptUrl!, options: nil, error: nil)! var receiptdata: NSString = receipt.base64EncodedStringWithOptions(NSDataBase64EncodingOptions(rawValue: 0)) //println(receiptdata) var request = NSMutableURLRequest(URL: NSURL(string: "http://www.brianjcoleman.com/code/verifyReceipt.php")!) var session = NSURLSession.sharedSession() request.HTTPMethod = "POST" var err: NSError? request.HTTPBody = receiptdata.dataUsingEncoding(NSASCIIStringEncoding) var task = session.dataTaskWithRequest(request, completionHandler: {data, response, error -> Void in var err: NSError? var json = NSJSONSerialization.JSONObjectWithData(data, options: .MutableLeaves, error: &err) as? NSDictionary if(err != nil) { println(err!.localizedDescription) let jsonStr = NSString(data: data, encoding: NSUTF8StringEncoding) println("Error could not parse JSON: '\(jsonStr)'") } else { if let parseJSON = json { println("Receipt \(parseJSON)") } else { let jsonStr = NSString(data: data, encoding: NSUTF8StringEncoding) println("Receipt Error: \(jsonStr)") } } }) task.resume() }
In-App Purchase Shared Secret
If your application provides Auto-Renewable Subscriptions you will need to send a password along with the receipt data to Apple during validation. If you’re validating a receipt for an auto-renewing receipt, go to iTunes Connect and get the hexadecimal shared secret for your app.
The secret is typically a 32 digit, alpha-numeric string that looks something like this e4b2dd30b7rt4a7382b5173hg790elk7 (this one is fake).
In iTunes Connect go to My Apps > (select your app) > In-App Purchases > View or generate a shared secret
Send Receipt Data to Apple using PHP
Use the following PHP script to take the receipt data and send it to Apple’s server to validate. The script takes two values, the receipt-data and a password (if needed for auto-renewable subscriptions). You must replace this text
Save this file as “verifyReceipt.php”, upload it to your web server and change the permissions to 755.
$value){ $newcontent .= $key.' '.$value; } $new = trim($newcontent); $new = trim($newcontent); $new = str_replace('_','+',$new); $new = str_replace(' =','==',$new); if (substr_count($new,'=') == 0){ if (strpos('=',$new) === false){ $new .= '='; } } $new = '{"receipt-data":"'.$new.'","password":""}'; $info = getReceiptData($new); ?>
Testing
To test the receipt validation, run your application and buy one of the in-app purchases, call the validateReceipt() method to initiate the receipt-data to send to your server where the PHP will pass the receipt (and possible the shared secret password), then get the response back from Apple and send it back to the iOS app where you can parse the JSON and get the receipt information.
During testing you may see some status codes returned from Apple, here is a table that describes each of them:
Status Code | Description |
21000 | The App Store could not read the JSON object you provided. |
21002 | The data in the receipt-data property was malformed or missing. |
21003 | The receipt could not be authenticated. |
21004 | The shared secret you provided does not match the shared secret on file for your account. Only returned for iOS 6 style transaction receipts for auto-renewable subscriptions. |
21005 | The receipt server is not currently available. |
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. |
21007 | This receipt is from the test environment, but it was sent to the production environment for verification. Send it to the test environment instead. |
21008 | This receipt is from the production environment, but it was sent to the test environment for verification. Send it to the production environment instead. |
Receipt Data
Below is a sample of the data you will receive for the receipt. You may want to parse this JSON and store the values into a database so you can track your users status.
"latest_receipt_info" = ( { "is_trial_period" = false; "original_purchase_date" = "2015-05-20 17:41:12 Etc/GMT"; "original_purchase_date_ms" = 1432143672000; "original_purchase_date_pst" = "2015-05-20 10:41:12 America/Los_Angeles"; "original_transaction_id" = 1000000156014803; "product_id" = "com.brianjcoleman.testiap1"; "purchase_date" = "2015-05-20 17:41:12 Etc/GMT"; "purchase_date_ms" = 1432143672000; "purchase_date_pst" = "2015-05-20 10:41:12 America/Los_Angeles"; quantity = 1; "transaction_id" = 1000000156014803; }, { "expires_date" = "2015-05-23 17:05:59 Etc/GMT"; "expires_date_ms" = 1432400759000; "expires_date_pst" = "2015-05-23 10:05:59 America/Los_Angeles"; "is_trial_period" = false; "original_purchase_date" = "2015-05-23 17:03:00 Etc/GMT"; "original_purchase_date_ms" = 1432400580000; "original_purchase_date_pst" = "2015-05-23 10:03:00 America/Los_Angeles"; "original_transaction_id" = 1000000156451343; "product_id" = "com.brianjcoleman.testiap3"; "purchase_date" = "2015-05-23 17:02:59 Etc/GMT"; "purchase_date_ms" = 1432400579000; "purchase_date_pst" = "2015-05-23 10:02:59 America/Los_Angeles"; quantity = 1; "transaction_id" = 1000000156451343; "web_order_line_item_id" = 1000000029801713; } ); receipt = { "adam_id" = 0; "app_item_id" = 0; "application_version" = 1; "bundle_id" = "com.brianjcoleman.iqtest"; "download_id" = 0; "in_app" = ( { "is_trial_period" = false; "original_purchase_date" = "2015-05-24 01:06:58 Etc/GMT"; "original_purchase_date_ms" = 1432429618000; "original_purchase_date_pst" = "2015-05-23 18:06:58 America/Los_Angeles"; "original_transaction_id" = 1000000156455961; "product_id" = "com.brianjcoleman.testiap2"; "purchase_date" = "2015-05-24 01:06:58 Etc/GMT"; "purchase_date_ms" = 1432429618000; "purchase_date_pst" = "2015-05-23 18:06:58 America/Los_Angeles"; quantity = 1; "transaction_id" = 1000000156455961; }, { "is_trial_period" = false; "original_purchase_date" = "2015-05-20 17:41:12 Etc/GMT"; "original_purchase_date_ms" = 1432143672000; "original_purchase_date_pst" = "2015-05-20 10:41:12 America/Los_Angeles"; "original_transaction_id" = 1000000156014803; "product_id" = "com.brianjcoleman.testiap1"; "purchase_date" = "2015-05-20 17:41:12 Etc/GMT"; "purchase_date_ms" = 1432143672000; "purchase_date_pst" = "2015-05-20 10:41:12 America/Los_Angeles"; quantity = 1; "transaction_id" = 1000000156014803; } ); "original_application_version" = "1.0"; "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"; "receipt_type" = ProductionSandbox; "request_date" = "2015-05-24 16:31:18 Etc/GMT"; "request_date_ms" = 1432485078143; "request_date_pst" = "2015-05-24 09:31:18 America/Los_Angeles"; "version_external_identifier" = 0; }; status = 0; }
You can learn more about the Receipt Fields in the Apple Validation Programming Guide.
Note that while testing auto-renewable subscriptions the “expires_date” in the sandbox will not act like it does in production. When testing auto-renewable subscriptions in the test environment, the duration times are compressed. Additionally, test subscriptions only auto-renew a maximum of six times.
You can grab the full source code for this tutorial. Note: Created using XCode 6.3 (Swift 1.2).