One of the features available to iOS developers when creating iPhone or iPad applications is the ability to sell items within their application (called In App Purchases).

In many cases, the In App Purchase will be fulfilled by downloading data from a third-party server. Before the iOS application can download the purchased item, the third-party server must check with Apple to ensure the item has been successfully purchased.

The following diagram demonstrates how this works in principle.

Figure 1 The basic workflow of In App purchases and receipt verification
Figure 1: The basic workflow of In App purchases and receipt verification

In this PhpRiot snippet I will show you how to verify a purchase receipt that was submitted by the iOS application with the Apple receipt verification service. We will achieve this using PHP and cURL. Referring to the above diagram, the code in this article will deal with steps 3, 4 and 5.

Note: This article assumes your iOS application already has the ability to submit the StoreKit receipt data in Base-64 encoding to your PHP web site.

To verify a receipt, we will define a function called getReceiptData(). This function will return information about the transaction being verified if the receipt is valid, and it will throw an exception if the receipt is invalid or if the receipt cannot be verified.

The following listing demonstrates the basic skeleton for this script. The first argument ($receipt) is the base-64 encoded receipt data exactly as supplied from the iOS application.

The second argument ($isSandbox) is a boolean indicating whether the receipt being verified is from a real transaction or a transaction from a test user.

Listing 1 Basic skeleton for verifying receipts (listing-1.php)
  1. <?php
  2.     function getReceiptData($receipt, $isSandbox = false)
  3.     {
  4.  
  5.     }
  6.  
  7.     $receipt   = $_POST['receipt'];
  8.     $isSandbox = (bool) $_POST['sandbox'];
  9.  
  10.     try {
  11.         $info = getReceiptData($receipt, $isSandbox);
  12.  
  13.         // receipt is valid
  14.     }
  15.     catch (Exception $ex) {
  16.         // unable to verify receipt, or receipt is not valid
  17.     }
  18. ?>
<?php
    function getReceiptData($receipt, $isSandbox = false)
    {

    }

    $receipt   = $_POST['receipt'];
    $isSandbox = (bool) $_POST['sandbox'];

    try {
        $info = getReceiptData($receipt, $isSandbox);

        // receipt is valid
    }
    catch (Exception $ex) {
        // unable to verify receipt, or receipt is not valid
    }
?>
Note: This code assumes your iOS application performs a POST request which includes data in receipt and sandbox.

If you're testing a receipt for the sandbox, the endpoint URL to check with is https://sandbox.itunes.apple.com/verifyReceipt. The endpoint URL for real transactions is https://buy.itunes.apple.com/verifyReceipt.

As such, we can set the URL based on the value of $isSandbox.

Listing 2 Determining the App Store endpoint (listing-2.php)
  1. <?php
  2.     function getReceiptData($receipt, $isSandbox = false)
  3.     {
  4.         if ($isSandbox) {
  5.             $endpoint = 'https://sandbox.itunes.apple.com/verifyReceipt';
  6.         }
  7.         else {
  8.             $endpoint = 'https://buy.itunes.apple.com/verifyReceipt';
  9.         }
  10.     }
  11. ?>
<?php
    function getReceiptData($receipt, $isSandbox = false)
    {
        if ($isSandbox) {
            $endpoint = 'https://sandbox.itunes.apple.com/verifyReceipt';
        }
        else {
            $endpoint = 'https://buy.itunes.apple.com/verifyReceipt';
        }
    }
?>

The verifyReceipt web service requires a post request consisting only of a JSON-encoded string. This string should correspond to a JavaScript object with a single key called receipt-data. This can be defined in PHP using array('receipt-data' => $receipt).

Once this array has been defined, json_encode() can be used to build the JSON string.

Listing 3 Build the cURL request POST data (listing-3.php)
  1. <?php
  2.     function getReceiptData($receipt, $isSandbox = false)
  3.     {
  4.         // ... previous code
  5.  
  6.         $postData = json_encode(
  7.             array('receipt-data' => $receipt)
  8.         );
  9.     }
  10. ?>
<?php
    function getReceiptData($receipt, $isSandbox = false)
    {
        // ... previous code

        $postData = json_encode(
            array('receipt-data' => $receipt)
        );
    }
?>

Next we build the cURL request using the $endpoint and $postData variables we have defined.

Note: Your cURL installation must support SSL in order to communicate with the Apple web service.

In addition to setting the request post data (including setting the request to use POST instead of GET), we must also instruct cURL to return the response. This is achieved by setting the CURLOPT_RETURNTRANSFER to true. If you don't do this, the response from the HTTP request will be output directly and you will be unable to parse it.

The listing below shows how we build the cURL request. Additionally, this code also performs the request (and assigns the response data to$response), retrieves any error codes or messages and finally closes the connection. We will make use of this data shortly.

Listing 4 Creating and executing the cURL request (listing-4.php)
  1. <?php
  2.     function getReceiptData($receipt, $isSandbox = false)
  3.     {
  4.         // ... previous code
  5.  
  6.         $ch = curl_init($endpoint);
  7.         curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  8.         curl_setopt($ch, CURLOPT_POST, true);
  9.         curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
  10.  
  11.         $response = curl_exec($ch);
  12.         $errno    = curl_errno($ch);
  13.         $errmsg   = curl_error($ch);
  14.         curl_close($ch);
  15.     }
  16. ?>
<?php
    function getReceiptData($receipt, $isSandbox = false)
    {
        // ... previous code

        $ch = curl_init($endpoint);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);

        $response = curl_exec($ch);
        $errno    = curl_errno($ch);
        $errmsg   = curl_error($ch);
        curl_close($ch);
    }
?>

Finally, we must check the response data. If the request failed for some reason, the $errno value will be non-zero. In this case, we throw an exception that includes the error number and message. This will be handled by the code that calls getReceiptData().

Note: You can see a list of cURL error codes at http://curl.haxx.se/libcurl/c/libcurl-errors.html
Listing 5 Ensuring the cURL request was successful (listing-5.php)
  1. <?php
  2.     function getReceiptData($receipt, $isSandbox = false)
  3.     {
  4.         // ... previous code
  5.  
  6.         if ($errno != 0) {
  7.             throw new Exception($errmsg, $errno);
  8.         }
  9.     }
  10. ?>
<?php
    function getReceiptData($receipt, $isSandbox = false)
    {
        // ... previous code

        if ($errno != 0) {
            throw new Exception($errmsg, $errno);
        }
    }
?>

At this point in the code we know we know the HTTP request was successful, so we must parse the response and ensure the submitted receipt was successful. This web service returns a JSON object with two keys, status and receipt.

We can turn the JSON string into a PHP object using json_decode(). This function will return null if the string wasn't a valid JSON string.

Listing 6 Ensuring the response data is valid (listing-6.php)
  1. <?php
  2.     function getReceiptData($receipt, $isSandbox = false)
  3.     {
  4.         // ... previous code
  5.  
  6.         $data = json_decode($response);
  7.  
  8.         if (!is_object($data)) {
  9.             throw new Exception('Invalid response data');
  10.         }
  11.     }
  12. ?>
<?php
    function getReceiptData($receipt, $isSandbox = false)
    {
        // ... previous code

        $data = json_decode($response);

        if (!is_object($data)) {
            throw new Exception('Invalid response data');
        }
    }
?>

If the status value is , the receipt was valid, otherwise it was not valid. As such, we throw an exception for non-zero values. If the receipt was valid, we build an array of data to return.

Listing 7 Checking if the receipt is valid and if so, building the response data (listing-7.php)
  1. <?php
  2.     function getReceiptData($receipt, $isSandbox = false)
  3.     {
  4.         // ... previous code
  5.  
  6.         if (!isset($data->status) || $data->status != 0) {
  7.             throw new Exception('Invalid receipt');
  8.         }
  9.  
  10.         return array(
  11.             'quantity'       =>  $data->receipt->quantity,
  12.             'product_id'     =>  $data->receipt->product_id,
  13.             'transaction_id' =>  $data->receipt->transaction_id,
  14.             'purchase_date'  =>  $data->receipt->purchase_date,
  15.             'app_item_id'    =>  $data->receipt->app_item_id,
  16.             'bid'            =>  $data->receipt->bid,
  17.             'bvrs'           =>  $data->receipt->bvrs
  18.         );
  19.     }
  20. ?>
<?php
    function getReceiptData($receipt, $isSandbox = false)
    {
        // ... previous code

        if (!isset($data->status) || $data->status != 0) {
            throw new Exception('Invalid receipt');
        }

        return array(
            'quantity'       =>  $data->receipt->quantity,
            'product_id'     =>  $data->receipt->product_id,
            'transaction_id' =>  $data->receipt->transaction_id,
            'purchase_date'  =>  $data->receipt->purchase_date,
            'app_item_id'    =>  $data->receipt->app_item_id,
            'bid'            =>  $data->receipt->bid,
            'bvrs'           =>  $data->receipt->bvrs
        );
    }
?>
Note: There are other values returned by the web service. See the link below to Apple's guide.

The following listing shows the entire code. You may want to move getReceiptData() into its own class or file so it can easily be reused.

Listing 8 The complete script for verifying a receipt (listing-8.php)
  1. <?php
  2.     /**
  3.      * Verify a receipt and return receipt data
  4.      *
  5.      * @param   string  $receipt    Base-64 encoded data
  6.      * @param   bool    $isSandbox  Optional. True if verifying a test receipt
  7.      * @throws  Exception   If the receipt is invalid or cannot be verified
  8.      * @return  array       Receipt info (including product ID and quantity)
  9.      */
  10.     function getReceiptData($receipt, $isSandbox = false)
  11.     {
  12.         // determine which endpoint to use for verifying the receipt
  13.         if ($isSandbox) {
  14.             $endpoint = 'https://sandbox.itunes.apple.com/verifyReceipt';
  15.         }
  16.         else {
  17.             $endpoint = 'https://buy.itunes.apple.com/verifyReceipt';
  18.         }
  19.  
  20.         // build the post data
  21.         $postData = json_encode(
  22.             array('receipt-data' => $receipt)
  23.         );
  24.  
  25.         // create the cURL request
  26.         $ch = curl_init($endpoint);
  27.         curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  28.         curl_setopt($ch, CURLOPT_POST, true);
  29.         curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
  30.  
  31.         // execute the cURL request and fetch response data
  32.         $response = curl_exec($ch);
  33.         $errno    = curl_errno($ch);
  34.         $errmsg   = curl_error($ch);
  35.         curl_close($ch);
  36.  
  37.         // ensure the request succeeded
  38.         if ($errno != 0) {
  39.             throw new Exception($errmsg, $errno);
  40.         }
  41.  
  42.         // parse the response data
  43.         $data = json_decode($response);
  44.  
  45.         // ensure response data was a valid JSON string
  46.         if (!is_object($data)) {
  47.             throw new Exception('Invalid response data');
  48.         }
  49.  
  50.         // ensure the expected data is present
  51.         if (!isset($data->status) || $data->status != 0) {
  52.             throw new Exception('Invalid receipt');
  53.         }
  54.  
  55.         // build the response array with the returned data
  56.         return array(
  57.             'quantity'       =>  $data->receipt->quantity,
  58.             'product_id'     =>  $data->receipt->product_id,
  59.             'transaction_id' =>  $data->receipt->transaction_id,
  60.             'purchase_date'  =>  $data->receipt->purchase_date,
  61.             'app_item_id'    =>  $data->receipt->app_item_id,
  62.             'bid'            =>  $data->receipt->bid,
  63.             'bvrs'           =>  $data->receipt->bvrs
  64.         );
  65.     }
  66.  
  67.     // fetch the receipt data and sandbox indicator from the post data
  68.     $receipt   = $_POST['receipt'];
  69.     $isSandbox = (bool) $_POST['sandbox'];
  70.  
  71.     // verify the receipt
  72.     try {
  73.         $info = getReceiptData($receipt, $isSandbox);
  74.  
  75.         // receipt is valid, now do something with $info
  76.     }
  77.     catch (Exception $ex) {
  78.         // unable to verify receipt, or receipt is not valid
  79.     }
  80. ?>
<?php
    /**
     * Verify a receipt and return receipt data
     *
     * @param   string  $receipt    Base-64 encoded data
     * @param   bool    $isSandbox  Optional. True if verifying a test receipt
     * @throws  Exception   If the receipt is invalid or cannot be verified
     * @return  array       Receipt info (including product ID and quantity)
     */
    function getReceiptData($receipt, $isSandbox = false)
    {
        // determine which endpoint to use for verifying the receipt
        if ($isSandbox) {
            $endpoint = 'https://sandbox.itunes.apple.com/verifyReceipt';
        }
        else {
            $endpoint = 'https://buy.itunes.apple.com/verifyReceipt';
        }

        // build the post data
        $postData = json_encode(
            array('receipt-data' => $receipt)
        );

        // create the cURL request
        $ch = curl_init($endpoint);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);

        // execute the cURL request and fetch response data
        $response = curl_exec($ch);
        $errno    = curl_errno($ch);
        $errmsg   = curl_error($ch);
        curl_close($ch);

        // ensure the request succeeded
        if ($errno != 0) {
            throw new Exception($errmsg, $errno);
        }

        // parse the response data
        $data = json_decode($response);

        // ensure response data was a valid JSON string
        if (!is_object($data)) {
            throw new Exception('Invalid response data');
        }

        // ensure the expected data is present
        if (!isset($data->status) || $data->status != 0) {
            throw new Exception('Invalid receipt');
        }

        // build the response array with the returned data
        return array(
            'quantity'       =>  $data->receipt->quantity,
            'product_id'     =>  $data->receipt->product_id,
            'transaction_id' =>  $data->receipt->transaction_id,
            'purchase_date'  =>  $data->receipt->purchase_date,
            'app_item_id'    =>  $data->receipt->app_item_id,
            'bid'            =>  $data->receipt->bid,
            'bvrs'           =>  $data->receipt->bvrs
        );
    }

    // fetch the receipt data and sandbox indicator from the post data
    $receipt   = $_POST['receipt'];
    $isSandbox = (bool) $_POST['sandbox'];

    // verify the receipt
    try {
        $info = getReceiptData($receipt, $isSandbox);

        // receipt is valid, now do something with $info
    }
    catch (Exception $ex) {
        // unable to verify receipt, or receipt is not valid
    }
?>

Now that you can verify the receipt data, you can fulfill the user's request. If you deem the receipt to be valid you can send back the requested data, otherwise you can send back an error message so your iOS application can communicate the problem back to the user.