Skip to main content

Fixing Order Verification Error 60051 in Huawei In-App Purchases (IAP)

 Implementing server-side AppGallery receipt validation often introduces unexpected friction. For backend developers managing Huawei IAP server integration, one of the most persistent roadblocks is encountering Huawei IAP error 60051.

This error strictly indicates an order verification failure. When your server attempts to validate a purchase token or verify the cryptographic signature of the receipt, the Huawei Mobile Services (HMS) environment rejects the payload. Resolving this requires isolating flaws in your cryptographic key formatting, payload encoding, or geographic endpoint routing.

Root Cause Analysis of Error 60051

In the context of HMS in-app purchases, error 60051 surfaces during two distinct backend processes: local cryptographic signature verification and remote token validation via the Huawei Order Service API.

The underlying failures typically stem from three architectural missteps:

  1. Malformed Public Keys: Huawei provides a Base64-encoded RSA public key in the AppGallery Connect console. Standard cryptographic libraries in Node.js and Java process raw Base64 strings differently. Node.js strictly requires Privacy-Enhanced Mail (PEM) formatting, complete with specific headers and 64-character line breaks.
  2. Geographic Site Routing Mismatches: Huawei enforces strict data residency. User data is partitioned across four primary sites (Germany, Singapore, Russia, China). If an application routes a validation request for a European user to the Singapore endpoint, the server cannot locate the transaction and returns an error.
  3. Payload String Mutation: The signature is generated against the raw, stringified JSON of the inAppPurchaseData. Any intermediate parsing, formatting, or whitespace modification before passing the string to the verifier alters the hash, causing the SHA256withRSA validation to fail.

The Fix: Local Signature Validation

Performing local signature validation avoids the latency and routing complexities of hitting Huawei's remote APIs. You must verify the inAppPurchaseData string against the inAppPurchaseDataSignature using your AppGallery public key.

Node.js Implementation

The native Node.js crypto module requires transforming the raw Base64 key into a standard X.509 PEM format. Attempting to pass the raw string from the developer console directly into crypto.createVerify guarantees a failure.

import crypto from 'crypto';

/**
 * Transforms a raw Base64 string into a valid PEM formatted public key.
 * @param {string} base64Key - The raw public key from AppGallery Connect.
 * @returns {string} The PEM formatted public key.
 */
function formatHuaweiPublicKey(base64Key) {
  // Split the key into chunks of 64 characters per PEM specifications
  const keyChunks = base64Key.match(/.{1,64}/g);
  if (!keyChunks) {
    throw new Error('Invalid Base64 public key provided.');
  }
  const formattedKey = keyChunks.join('\n');
  return `-----BEGIN PUBLIC KEY-----\n${formattedKey}\n-----END PUBLIC KEY-----`;
}

/**
 * Verifies the AppGallery receipt signature.
 * @param {string} inAppPurchaseData - The RAW JSON string from the client. Do not parse it.
 * @param {string} signature - The Base64 signature from the client.
 * @param {string} publicKey - The raw Base64 public key from your console.
 * @returns {boolean} True if the signature is valid.
 */
export function verifyHuaweiReceipt(inAppPurchaseData, signature, publicKey) {
  try {
    const pemKey = formatHuaweiPublicKey(publicKey);
    const verifier = crypto.createVerify('RSA-SHA256');
    
    // Crucial: Update the verifier with the exact unparsed string and specify utf8
    verifier.update(inAppPurchaseData, 'utf8');
    
    return verifier.verify(pemKey, signature, 'base64');
  } catch (error) {
    console.error('Cryptographic verification failed:', error.message);
    return false;
  }
}

Java Implementation

Unlike Node.js, Java's KeyFactory operates directly on byte arrays. Appending PEM headers in Java will actually cause a InvalidKeySpecException. The solution relies on decoding the Base64 string directly into an X509EncodedKeySpec.

import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;

public class HuaweiIapValidator {

    /**
     * Verifies the HMS in-app purchase signature.
     *
     * @param inAppPurchaseData The unparsed JSON string from the HMS client.
     * @param signature         The Base64 encoded signature.
     * @param publicKeyBase64   The raw public key from AppGallery Connect.
     * @return true if the signature is valid, false otherwise.
     */
    public static boolean verifySignature(String inAppPurchaseData, String signature, String publicKeyBase64) {
        try {
            // Decode the raw base64 public key
            byte[] keyBytes = Base64.getDecoder().decode(publicKeyBase64);
            X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            PublicKey publicKey = keyFactory.generatePublic(keySpec);

            // Initialize the SHA256withRSA signature verifier
            Signature sig = Signature.getInstance("SHA256withRSA");
            sig.initVerify(publicKey);
            
            // Crucial: Use exact UTF-8 byte representation of the raw JSON
            sig.update(inAppPurchaseData.getBytes(StandardCharsets.UTF_8));

            byte[] signatureBytes = Base64.getDecoder().decode(signature);
            return sig.verify(signatureBytes);
            
        } catch (Exception e) {
            // Catching GeneralSecurityException and IllegalArgumentException
            System.err.println("Huawei IAP Signature verification failed: " + e.getMessage());
            return false;
        }
    }
}

Resolving Remote API Endpoint Routing

If your architecture requires querying the Huawei Order Service API (e.g., to check the status of an auto-renewable subscription via the purchases.subscriptions.get endpoint), resolving Huawei IAP error 60051 requires routing the request to the correct geographical server.

Huawei provides four regional root URLs:

  • Germany (Europe): https://subscr-at-dre.iap.dbankcloud.com
  • Singapore (Asia, Africa, Latin America): https://subscr-drru.iap.dbankcloud.com
  • Russia: https://subscr-drru.iap.dbankcloud.ru
  • China: https://subscr-drcn.iap.dbankcloud.cn

Dynamic Endpoint Selection Pattern

To prevent routing failures, your backend should dynamically determine the endpoint. The client SDK receives an accountFlag inside the inAppPurchaseData payload. However, because user accounts can migrate, the most resilient backend architecture implements a cascading fallback mechanism.

/**
 * Cascading fetch wrapper to resolve the correct Huawei Site endpoint.
 */
async function fetchHuaweiOrderData(purchaseToken, subscriptionId, accessToken) {
  const endpoints = [
    'https://subscr-at-dre.iap.dbankcloud.com',
    'https://subscr-drru.iap.dbankcloud.com',
    'https://subscr-drru.iap.dbankcloud.ru',
    'https://subscr-drcn.iap.dbankcloud.cn'
  ];

  for (const baseUrl of endpoints) {
    const url = `${baseUrl}/sub/applications/v2/purchases/get`;
    
    const response = await fetch(url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${accessToken}`
      },
      body: JSON.stringify({
        purchaseToken,
        subscriptionId
      })
    });

    const data = await response.json();

    // If the error is not a site routing issue (e.g., success or expired token), return it.
    // Error 60051 or 60052 typically indicates the order does not exist on this regional site.
    if (data.responseCode !== "60051" && data.responseCode !== "60052") {
      return data;
    }
  }

  throw new Error("Order verification failed across all geographical endpoints.");
}

Common Pitfalls and Edge Cases

URL Encoding of the Signature

Mobile clients frequently send the inAppPurchaseDataSignature to the backend via an HTTP POST or GET parameter. Because Base64 strings can contain + and = characters, failing to properly URL-decode the payload before passing it to the cryptographic library will result in a silent verification failure. Ensure your framework's body-parser is correctly configured to handle application/x-www-form-urlencoded if you aren't passing the data as strict JSON.

Stripped Whitespace in JSON Payload

The inAppPurchaseData must remain exactly as the client SDK delivered it. A common mistake in Node.js environments is passing the data through JSON.parse() for logging, and then passing JSON.stringify(parsedData) into the verifier. JSON stringification is non-deterministic regarding whitespace and key ordering. The resulting string will have a different SHA256 hash than the original payload, immediately triggering Huawei IAP error 60051. Always cache the raw, unparsed string for the verification step.