Verifiy iOS Receipt with Node.js
Asked Answered
I

4

7

After struggling a few days trying to get something to work and getting no where, I was wondering if someone has gotten iOS Receipt Validation working on Node.js. I have tried the node module iap_verifier found here but I could not get it to work properly for me. the only response I received back form Apples servers is 21002, data was malformed.

One thing that has worked for me was a client side validation request to apples servers that I got directly from the tutorials provided by Apple here, with the code shown below.

// The transaction looks ok, so start the verify process.

// Encode the receiptData for the itms receipt verification POST request.
NSString *jsonObjectString = [self encodeBase64:(uint8_t *)transaction.transactionReceipt.bytes
                                         length:transaction.transactionReceipt.length];

// Create the POST request payload.
NSString *payload = [NSString stringWithFormat:@"{\"receipt-data\" : \"%@\", \"password\" : \"%@\"}",
                     jsonObjectString, ITC_CONTENT_PROVIDER_SHARED_SECRET];

NSData *payloadData = [payload dataUsingEncoding:NSUTF8StringEncoding];


// Use ITMS_SANDBOX_VERIFY_RECEIPT_URL while testing against the sandbox.
NSString *serverURL = ITMS_SANDBOX_VERIFY_RECEIPT_URL;

// Create the POST request to the server.
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:serverURL]];
[request setHTTPMethod:@"POST"];
[request setHTTPBody:payloadData];
NSURLConnection *conn = [[NSURLConnection alloc] initWithRequest:request delegate:self];
[conn start];

I have a bunch of different code I have been using to send a wide array of things to my node server. and all of my different attempts have failed. I have even tried just funneling the "payloadData" I constructed in the client side validation example above to my server and sending that to Apples servers with the following code:

function verifyReceipt(receiptData, responder)
{

var options = {
    host: 'sandbox.itunes.apple.com',
    port: 443,
    path: '/verifyReceipt',
    method: 'POST',
    headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
        'Content-Length': Buffer.byteLength(receiptData)
    }
};

var req = https.request(options, function(res) {
    res.setEncoding('utf8');
    res.on('data', function (chunk) {
        console.log("body: " + chunk);
    });
});

req.write(receiptData);
req.end();
}

Where the function is passed the payloadData. The response received from Apple is always 21002. I'm still basically a node novice,so I can't figure out what exactly is going wrong. I think there might be some data corruption happening when I am sending the data from ObjC to my Node server, so perhaps I am not transmitting right.

If anyone can point me in the right direction, or provide some example of how they got receipt validation to work in node for them, it would be a great help. It would be great if anyone has had any experience with the iap_verifier module, and exactly what data it requires. I'll provide any code example I need to, as I have been fighting this process for a few days now.

Thanks!

Incidental answered 25/10, 2013 at 22:36 Comment(2)
If you have it working one place, why not record the network traffic and then duplicate whatever is happening?Burmaburman
Did you ever get this working!? If so can you share..Zachariahzacharias
I
6

For anyone using the npm library "request", here's how to avoid that bothersome 21002 error.

formFields = {
  'receipt-data': receiptData_64
  'password': yourAppleSecret
}

verifyURL = 'https://buy.itunes.apple.com/verifyReceipt' // or 'https://sandbox.itunes.apple.com/verifyReceipt'

req = request.post({url: verifyURL, json: formFields}, function(err, res, body) {
    console.log('Response:', body);
})
Iodide answered 11/11, 2014 at 19:5 Comment(1)
verifyReceipt endpoint is deprecated : developer.apple.com/documentation/appstorereceipts/…Punjab
A
5

This is my working solution for auto-renewable subscriptions, using the npm request-promise library. Without JSON stringify-ing the body form, I was receiving 21002 error (The data in the receipt-data property was malformed or missing)

const rp = require('request-promise');

var verifyURL = 'https://sandbox.itunes.apple.com/verifyReceipt';
// use 'https://buy.itunes.apple.com/verifyReceipt' for production

var options = {
    uri: verifyURL,
    method: 'POST',
    headers: {
        'User-Agent': 'Request-Promise',
        'Content-Type': 'application/x-www-form-urlencoded',
    },
    json: true
};

options.form = JSON.stringify({
    'receipt-data': receiptData,
    'password': password
});

rp(options).then(function (resData) {
    devLog.log(resData); // 0
}).catch(function (err) {
    devLog.log(err);
});
Alert answered 28/12, 2017 at 10:13 Comment(0)
T
4

Do you have composed correctly receiptData? Accordlying with Apple specification it should have the format

{"receipt-data": "your base64 receipt"}

Modifying your code wrapping the base64 receipt string with receipt-data object the validation should works

function (receiptData_base64, production, cb)
{
    var url = production ? 'buy.itunes.apple.com' : 'sandbox.itunes.apple.com'
    var receiptEnvelope = {
        "receipt-data": receiptData_base64
    };
    var receiptEnvelopeStr = JSON.stringify(receiptEnvelope);
    var options = {
        host: url,
        port: 443,
        path: '/verifyReceipt',
        method: 'POST',
        headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
            'Content-Length': Buffer.byteLength(receiptEnvelopeStr)
        }
    };

    var req = https.request(options, function(res) {
        res.setEncoding('utf8');
        res.on('data', function (chunk) {
            console.log("body: " + chunk);
            cb(true, chunk);
        });
        res.on('error', function (error) {
            console.log("error: " + error);
            cb(false, error);
        });
    });
    req.write(receiptEnvelopeStr);
    req.end();
}
Tactile answered 25/10, 2013 at 22:36 Comment(1)
verifyReceipt endpoint is deprecated : developer.apple.com/documentation/appstorereceipts/…Punjab
P
0

New Apple library since 2021

The verifyReceipt endpoint has been deprecated. The "app store server library" has been released for different languages including NodeJs: See it on github

See this video (23min) for every details.

And detailed documentation here : https://apple.github.io/app-store-server-library-node/

We can extract a transaction id from receipt using this code :

import { AppStoreServerAPIClient, Environment, ReceiptUtility, Order, ProductType, HistoryResponse, TransactionHistoryRequest } from "@apple/app-store-server-library"
import fs from 'fs';

const issuerId = "99b16628-15e4-4668-972b-eeff55eeff55"
const keyId = "ABCDEFGHIJ"
const bundleId = "com.example"
const filePath = "/path/to/key/SubscriptionKey_ABCDEFGHIJ.p8"
const privateKey = fs.readFileSync(filePath, 'utf8');
const environment = isDev ? Environment.SANDBOX : Environment.PRODUCTION;

const client =
        new AppStoreServerAPIClient(privateKey, keyId, issuerId, bundleId, environment)

const appReceipt = "MI..."
const receiptUtil = new ReceiptUtility()
const transactionId = receiptUtil.extractTransactionIdFromAppReceipt(appReceipt)

See this example and more on documentation's Home page > Receipt Usage.

issuerId, keyId and privateKey file, are generated on App Store Connect > User and access > Integrations > Team Key > Add a key and give it a name. The private key file can be downloaded only once but new keys can be generated at will.

The transactionId can then be used to pull user's transaction history: getTransactionHistory

if (transactionId != null) {
    const transactionHistoryRequest: TransactionHistoryRequest = {
        sort: Order.ASCENDING,
        revoked: false,
        productTypes: [ProductType.AUTO_RENEWABLE]
    }
    let response: HistoryResponse | null = null
    let transactions: string[] = []
    do {
        const revisionToken = response !== null && response.revision !== null ? response.revision : null
        response = await client.getTransactionHistory(transactionId, revisionToken, transactionHistoryRequest)
        if (response.signedTransactions) {
            transactions = transactions.concat(response.signedTransactions)
        }
    } while (response.hasMore)
    console.log(transactions)
}
Punjab answered 8/7 at 8:14 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.