Validating appReceiptStoreURL Returning 21002 Status

com apple it's drm invaliddrmargumentexception
in-app purchase status
ios receipt validation
in-app purchase receipt validation tutorial
appstorereceipturl
apple iap password
latest_expired_receipt_info
apple iap error codes

I have created a class which handles the purchases on In-App Purchases and also the validating of receipts. A while ago I used to use the transactionReceipt property on an SKPaymentTransaction, but have updated my code a fair amount and now use appStoreReceiptURL on the [NSBundle mainBundle].

Basically it seems as though my receipt is being sent to Apple's server in an acceptable manner, but I keep getting the status code of 21002. In auto-renewable subscriptions I know that this means the receipt is not in an acceptable format, however I have no idea what this status means in regard to an in-app purchase receipt.

Here is the local method validating the receipt:

/**
 *  Validates the receipt.
 *
 *  @param  transaction                 The transaction triggering the validation of the receipt.
 */
- (void)validateReceiptForTransaction:(SKPaymentTransaction *)transaction
{
    //  get the product for the transaction
    IAPProduct *product                 = self.internalProducts[transaction.payment.productIdentifier];

    //  get the receipt as a base64 encoded string
    NSData *receiptData                 = [[NSData alloc] initWithContentsOfURL:[NSBundle mainBundle].appStoreReceiptURL];
    NSString *receipt                   = [receiptData base64EncodedStringWithOptions:kNilOptions];
    NSLog(@"Receipt: %@", receipt);

    //  determine the url for the receipt verification server
    NSURL *verificationURL              = [[NSURL alloc] initWithString:IAPHelperServerBaseURL];
    verificationURL                     = [verificationURL URLByAppendingPathComponent:IAPHelperServerReceiptVerificationComponent];
    NSMutableURLRequest *urlRequest     = [[NSMutableURLRequest alloc] initWithURL:verificationURL];
    urlRequest.HTTPMethod               = @"POST";
    NSDictionary *httpBody              = @{@"receipt"      : receipt,
                                            @"sandbox"      : @(1)};
    urlRequest.HTTPBody                 = [NSKeyedArchiver archivedDataWithRootObject:httpBody];
    [NSURLConnection sendAsynchronousRequest:urlRequest
                                       queue:[[NSOperationQueue alloc] init]
                           completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError)
    {
        //  create a block to be called whenever a filue is hit
        void (^failureBlock)(NSString *failureMessage)          = ^void(NSString *failureMessage)
        {
            [[NSOperationQueue mainQueue] addOperationWithBlock:
            ^{
                //  log the failure message
                NSLog(@"%@", failureMessage);
                //  if we have aready tried refreshing the receipt then we close the transaction to avoid loops
                if (self.transactionToValidate)
                    product.purchaseInProgress  = NO,
                    [[SKPaymentQueue defaultQueue] finishTransaction:transaction],
                    [self notifyStatus:@"Validation failed." forProduct:product],
                    self.transactionToValidate  = nil;
                //  if we haven't tried yet, we'll refresh the receipt and then attempt a second validation
                else
                    self.transactionToValidate  = transaction,
                    [self refreshReceipt];
            }];
        };

        //  check for an error whilst contacting the server
        if (connectionError)
        {
            failureBlock([[NSString alloc] initWithFormat:@"Failure connecting to server: %@", connectionError]);
            return;
        }

        //  cast the response appropriately
        NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;

        //  parse the JSON
        NSError *jsonError;
        NSDictionary *json              = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&jsonError];

        //  if the data did not parse correctly we fail out
        if (!json)
        {
            NSString *responseString        = [NSHTTPURLResponse localizedStringForStatusCode:httpResponse.statusCode];
            NSString *failureMessage        = [[NSString alloc] initWithFormat:@"Failure parsing JSON: %@\nServer Response: %@ (%@)",
                                               data, responseString, @(httpResponse.statusCode)];
            failureBlock(failureMessage);
            return;
        }

        //  if the JSON was successfully parsed pull out status code to check for verification success
        NSInteger statusCode            = [json[@"status"] integerValue];
        NSString *errorDescription      = json[@"error"];
        //  if the verification did not succeed we fail out
        if (statusCode != 0)
        {
            NSString *failureMessage    = [[NSString alloc] initWithFormat:@"Failure verifying receipt: %@", errorDescription];
            failureBlock(failureMessage);
        }
        //  otherwise we have succeded, yay
        else
            NSLog(@"Successfully verified receipt."),
            [self provideContentForCompletedTransaction:transaction productIdentifier:transaction.payment.productIdentifier];

    }];
}

The important PHP function on the server does this:

    /**
     *  Validates a given receipt and returns the result.
     *
     *  @param  receipt             Base64-encoded receipt.
     *  @param  sandbox             Boolean indicating whether to use sandbox servers or production servers.
     *
     *  @return Whether the reciept is valid or not.
     */
    function validateReceipt($receipt, $sandbox)
    {
        //  determine url for store based on if this is production or development
        if ($sandbox)
            $store                  = 'https://sandbox.itunes.apple.com/verifyReceipt';
        else
            $store                  = 'https://buy.itunes.apple.com/verifyReceipt';

        //  set up json-encoded dictionary with receipt data for apple receipt validator
        $postData                   = json_encode(array('receipt-data'  => $receipt));

        //  use curl library to perform web request
        $curlHandle                 = curl_init($store);
        //  we want results returned as string, the request to be a post, and the json data to be in the post fields
        curl_setopt($curlHandle, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($curlHandle, CURLOPT_POST, true);
        curl_setopt($curlHandle, CURLOPT_POSTFIELDS, $postData);
        $encodedResponse            = curl_exec($curlHandle);
        curl_close($curlHandle);

        //  if we received no response we return the error
        if (!$encodedResponse)
            return result(ERROR_VERIFICATION_NO_RESPONSE, 'Payment could not be verified - no response data. This was sandbox? ' . ($sandbox ? 'YES' : 'NO'));

        //  decode json response and get the data
        $response                   = json_decode($encodedResponse);
        $status                     = $response->{'status'};
        $decodedReceipt             = $response->{'receipt'};

        //  if status code is not 0 there was an error validation receipt
        if ($status)
            return result(ERROR_VERIFICATION_FAILED, 'Payment could not be verified (status = ' . $status . ').');

        //  log the returned receipt from validator
        logToFile(print_r($decodedReceipt, true));

        //  pull out product id, transaction id and original transaction id from infro trurned by apple
        $productID                  = $decodedReceipt->{'product_id'};
        $transactionID              = $decodedReceipt->{'transaction_id'};
        $originalTransactionID      = $decodedReceipt->{'original_transaction_id'};

        //  make sure product id has expected prefix or we bail
        if (!beginsWith($productID, PRODUCT_ID_PREFIX))
            return result(ERROR_INVALID_PRODUCT_ID, 'Invalid Product Identifier');

        //  get any existing record of this transaction id from our database
        $db                         = Database::get();
        $statement                  = $db->prepare('SELECT * FROM transactions WHERE transaction_id = ?');
        $statement->bindParam(1, $transactionID, PDO::PARAM_STR, 32);
        $statement->execute();

        //  if we have handled this transaction before return a failure
        if ($statement->rowCount())
        {
            logToFile("Already processed $transactionID.");
            return result(ERROR_TRANSACTION_ALREADY_PROCESSED, 'Already processed this transaction.');
        }

        //  otherwise we insert this new transaction into the database
        else
        {
            logToFile("Adding $transactionID.");
            $statement              = $db->prepare('INSERT INTO transactions(transaction_id, product_id, original_transaction_id) VALUES (?, ?, ?)');
            $statement->bindParam(1, $transactionID, PDO::PARAM_STR, 32);
            $statement->bindParam(2, $productID, PDO::PARAM_STR, 32);
            $statement->bindParam(3, $originalTransactionID, PDO::PARAM_STR, 32);
            $statement->execute();
        }


        return result(SUCCESS);
    }

The actual PHP script being executed is:

    $receipt            = $_POST['receipt'];
    $sandbox            = $_POST['sandbox'];
    $returnValue        = validateReceipt($receipt, $sandbox);

    header('content-type: application/json; charset=utf-8');

    echo json_encode($returnValue);

Comparing your PHP with mine (which I know works) is difficult because I am using HTTPRequest rather than the raw curl APIs. However, it seems to me that you are setting the "{receipt-data:..}" JSON string as merely a field in the POST data rather than as the raw POST data itself, which is what my code is doing.

curl_setopt($curlHandle, CURLOPT_POST, true);
curl_setopt($curlHandle, CURLOPT_POSTFIELDS, $postData); // Possible problem
$encodedResponse = curl_exec($curlHandle);

Compared to:

$postData = '{"receipt-data" : "'.$receipt.'"}'; // yay one-off JSON serialization!
$request = new HTTPRequest('https://sandbox.itunes.apple.com/verifyReceipt', HTTP_METH_POST);
$request->setBody($postData); // Relevant difference...
$request->send();
$encodedResponse = $request->getResponseBody();

I have changed my variable names a bit to make them match up with your example.

PHP Apple Receipt Verification Function · GitHub, 21002. The data in the receipt-data property was malformed or the service When this status code is returned to your server, the receipt data is also decoded​  By providing an app receipt or any transaction receipt for the subscription and checking these values, you can get information about the currently-active subscription period. If the receipt being validated is for the latest renewal, the value for latest_receipt is the same as receipt-data (in the request) and the value for latest_receipt_info

the code 21002 means "The data in the receipt-data property was malformed or missing."

you can find it in https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateRemotely.html

below code is my class for appstore in-app verifyRecepip, GuzzleHttp is required, you can install it by composer require guzzlehttp/guzzle https://github.com/guzzle/guzzle

<?php

namespace App\Libraries;

class AppStoreIAP
{
  const SANDBOX_URL    = 'https://sandbox.itunes.apple.com/verifyReceipt';
  const PRODUCTION_URL = 'https://buy.itunes.apple.com/verifyReceipt';

  protected $receipt = null;

  protected $receiptData = null;

  protected $endpoint = 'production';

  public function __construct($receipt, $endpoint = self::PRODUCTION_URL)
  {
      $this->receipt  = json_encode(['receipt-data' => $receipt]);
      $this->endpoint = $endpoint;
  }


  public function setEndPoint($endpoint)
  {
      $this->endpoint = $endpoint;
  }


  public function getReceipt()
  {
      return $this->receipt;
  }


  public function getReceiptData()
  {
      return $this->receiptData;
  }


  public function getEndpoint()
  {
      return $this->endpoint;
  }


  public function validate($bundle_id, $transaction_id, $product_code)
  {
      $http = new \GuzzleHttp\Client([
          'headers' => [
              'Content-Type' => 'application/x-www-form-urlencoded',
          ],
          'timeout' => 4.0,
      ]);
      $res               = $http->request('POST', $this->endpoint, ['body' => $this->receipt]);
      $receiptData       = json_decode((string) $res->getBody(), true);
      $this->receiptData = $receiptData;
      switch ($receiptData['status']) {
          case 0: // verify Ok
              // check bundle_id
              if (!empty($receiptData['receipt']['bundle_id'])) {
                  $receipt_bundle_id = $receiptData['receipt']['bundle_id'];
                  if ($receipt_bundle_id != $bundle_id) {
                      throw new \Exception('bundle_id not matched!');
                  }
              }
              // check transaction_id , product_id
              if (!empty($receiptData['receipt']['in_app'])) {
                  $in_app = array_combine(array_column($receiptData['receipt']['in_app'], 'transaction_id'), $receiptData['receipt']['in_app']);
                  if (empty($in_app[$transaction_id])) {
                      throw new \Exception('transaction_id is empty!');
                  }
                  $data = $in_app[$transaction_id];
                  if ($data['product_id'] != $product_code) {
                      throw new \Exception('product_id not matched!');
                  }
              } else {
                  $receipt_transaction_id = $receiptData['receipt']['transaction_id'];
                  $receipt_product_id     = $receiptData['receipt']['product_id'];
                  if ($receipt_transaction_id != $transaction_id || $product_id != $product_code) {
                      throw new \Exception('tranaction_id not matched!');
                  }
              }
              break;
          case 21007:// sandbox order validate in production will return 21007
              if ($this->getEndpoint() != self::SANDBOX_URL) {
                  $this->setEndPoint(self::SANDBOX_URL);
                  $this->validate($bundle_id, $transaction_id, $product_code);
              } else {
                  throw new \Exception('appstore error!');
              }
              break;
          default:
              throw new \Exception("[{$receiptData['status']}]appstore error!");
              break;
      }
      return $receiptData;
  }
}

status, Status Codes. 21000. The App Store could not read the JSON object you provided. 21002. The data in the receipt-data property was malformed or missing. 1 Validating appReceiptStoreURL Returning 21002 Status Sep 17 '18. 0 Importing dates to mysql by csv file problem Oct 9 '18. Badges (1) Gold

I think Morteza M is correct. I did a test and got reply(JSON) like:

{
'status':
'environment': 'Sandbox'
  'receipt':
  {
    'download_id':
    ....
    'in_app":
    {
      'product_id':
      ....
    }
    ....
  }
}

Apple Receipt Validation Tool, iOS7 - receipts not validating at sandbox - error 21002 (java.lang. IllegalArgumentException\n)\n sandbox - Array\n(\n [status] => 21002\n [​exception] => java.lang. true); if (!$contents) $contents = array(); curl_close($​resource); //fclose($fp); return $contents; }. New details after Validating appReceiptStoreURL Returnin. 3 Validating appReceiptStoreURL Returning 21002 Status Nov 23 '13 2 Devise Not Woking on Heroku Server Feb 25 '14 2 Website loads but pages are Blank, NO html code Jan 30 '14

iOS7 - receipts not validating at sandbox, IAP return {"status":21002}. Hi, I use unity iap to work in unity5.3. i have a problom​,. when i get receipt from payload,and Send Receipt Data to  Q: Validating appReceiptStoreURL Returning 21002 Status I have created a class which handles the purchases on In-App Purchases and also the validating of receipts. A while ago I used to use the transactionReceipt property on an SKPaymentTransaction, but have updated my code a fair amount and now use appStoreReceiptURL on the [NSBundle mainBundle].

IAP return {"status":21002}, Instead of returning false 10+ times, just return false at the end since the only case private boolean mapStatus(int status) { String message = status + ": " case 21002: message += "Data was malformed"; break; case 21003:  Проверка статуса приложения AppReceiptStoreURL 21002 Intereting Posts PHP file_get_contents очень медленный при использовании полного URL-адреса Как я могу защитить паролем загрузку бинарного файла?

Apple receipt validator, Therefore your client can just pass them to the server and the server can validate them and read their content. Validating using Apple’s verifyReceipt API The simplest way for your server to verify that the receipts are valid is to pass them to Apple’s API which verifies the signature validity and unpacks the ASN1 contents into a much

Comments
  • Now your $response->{'receipt'} object does not include product_id etc. There is an inapp array you should iterate on it. It contains all in app purchases the user made. If you found product_id in this array, then validation is succeeded. Otherwise it is not valid.