Huawei in-app purchases
Use this flow for an Android app selling a configured Huawei consumable or non-consumable product. The Huawei IAP SDK starts the purchase. Your backend sends the resulting evidence to NextAppMarket for verification, then delivers the product after payment is confirmed.
You do not call /payment-sessions with hms_iap. Product pricing and identity come from the configured Huawei product mapping. Recurring subscriptions are outside this guide.
1. Prepare your app and product mapping
1.1 Collect the integration settings
Obtain the registered Android application ID, release certificate fingerprint, matching agconnect-services.json, and activated product mappings from the owner of your Huawei app registration and NextAppMarket. If your app is already configured for HMS, reuse that registered app instead of creating another identity.
For each product, record the Huawei product ID, the platform product mapping ID, and its type: consumable or non-consumable. The native SDK uses the Huawei ID; platform verification uses the mapping ID. Your backend derives both from its catalog and authenticated business attempt.
Enable the configured IAP product in that registration and confirm it is available to your verification account/region. Your platform key needs purchases:write, purchases:read, payments:read, and operations:read. Keep the key on your server. Use an opaque stable customer ID for purchase ownership, rather than an email address.
1.2 Add the Android SDK
- Put the registration's
agconnect-services.jsonin your app module directory, besideapp/build.gradle. Confirm its package matches the variant you are building. - Add Huawei's repository for dependencies in
settings.gradle:
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven { url 'https://developer.huawei.com/repo/' }
}
}
- In the project-level
build.gradle, add the AppGallery Connect plugin alongside your existing Android build setup:
buildscript {
repositories {
google()
mavenCentral()
maven { url 'https://developer.huawei.com/repo/' }
}
dependencies {
classpath 'com.huawei.agconnect:agcp:1.9.6.300'
}
}
- In
app/build.gradle, apply the plugin after the Android application plugin and add the IAP dependency:
apply plugin: 'com.huawei.agconnect'
dependencies {
implementation 'com.huawei.hms:iap:6.16.6.305'
}
These versions were checked in Huawei's public Maven repository. The IAP AAR declares Android API 19 as its minimum; your app and other dependencies may require a higher version. Keep your supported Android Gradle Plugin configuration and confirm compatibility when syncing. Do not add a second plugin declaration if the project already applies it through another Gradle mechanism.
- Add Internet permission under
manifestinAndroidManifest.xmland confirm it appears in the merged manifest:
<uses-permission android:name="android.permission.INTERNET" />
Preserve the SDK's bundled consumer shrinker rules and metadata. Sign the installed build with the certificate whose fingerprint is registered for this app.
6. Import Iap, IapClient, and the request/result classes from com.huawei.hms.iap and com.huawei.hms.iap.entity.
Checkpoint: Gradle resolves the SDK, your signed app installs, and the next environment check succeeds on a device with supported HMS Core and a Huawei account.
1.3 Check the environment before showing Pay
import com.huawei.hms.iap.Iap
import com.huawei.hms.common.ApiException
Iap.getIapClient(activity).isEnvReady()
.addOnSuccessListener {
loadHuaweiProduct() // The product request below.
}
.addOnFailureListener { error ->
val status = (error as? ApiException)?.status
if (status != null && status.hasResolution()) {
try {
status.startResolutionForResult(activity, REQUEST_HUAWEI_ENVIRONMENT)
} catch (failure: android.content.IntentSender.SendIntentException) {
showPurchaseMessage("Huawei account or services need attention.")
}
} else {
showPurchaseMessage("Huawei purchases are unavailable on this device or account.")
}
}
Choose distinct request codes for environment resolution and purchase. After the environment activity returns, run isEnvReady() again. Do not loop account-resolution prompts automatically. showPurchaseMessage is your UI text renderer; it does not change financial state.
1.4 Load the price and product
Load providerProductId and configuredPriceType from your application catalog endpoint into Activity/coordinator fields before this step. Use the configured product type and Huawei ID. This function supplies loadHuaweiProduct above:
fun loadHuaweiProduct() {
val request = ProductInfoReq().apply {
priceType = configuredPriceType
productIds = listOf(providerProductId)
}
Iap.getIapClient(activity).obtainProductInfo(request)
.addOnSuccessListener { result ->
val product = result.productInfoList?.firstOrNull { it.productId == providerProductId }
if (product == null) {
showPurchaseMessage("This product is unavailable. Please try again later.")
} else {
showPurchaseMessage("Price: " + product.price)
// Enable your Pay button for this successfully loaded product.
}
}
.addOnFailureListener { showPurchaseMessage("Cannot load this product right now.") }
}
Set configuredPriceType to IapClient.PriceType.IN_APP_CONSUMABLE or IN_APP_NONCONSUMABLE from your catalog mapping. Show Huawei's localized price. Do not request payment using a price or product type chosen by untrusted client input. The backend validates the purchased product against the saved attempt.
Target evidence format
The current public API accepts a bounded evidence string but its Huawei adapter has not finalized a wire encoding. The format below is the proposed v1 target for this documentation. It makes the client/server handoff explicit for implementation. Do not send it to a live adapter until NextAppMarket confirms that adapter implements this version. The client purchase, callback, and recovery steps remain useful while adapter work is in progress.
Encode a JSON object as the purchase_evidence string. Preserve the original signed purchase-data string without parsing and reserializing it:
{
"format": "huawei-iap-v1",
"inAppPurchaseData": "ORIGINAL_HUAWEI_PURCHASE_DATA_STRING",
"inAppDataSignature": "ORIGINAL_HUAWEI_SIGNATURE_STRING",
"signatureAlgorithm": "ALGORITHM_REPORTED_BY_THE_SDK_IF_PRESENT"
}
format, inAppPurchaseData, and inAppDataSignature are required. signatureAlgorithm is optional and, if supplied, must be the SDK's original non-empty value. It is a hint to validate against the activated provider configuration, not authority to choose an untrusted verification algorithm.
Your backend can use the supplied encode-hms-evidence.mjs function. It checks the shape and 65,536-character limit and serializes the wrapper once. The provider verifier must validate the signature with the configured Huawei key and check application, product, purchase identity/state, ownership, amount/currency, and replay history. The wrapper itself provides no proof of payment.
Source basis for client integration: Huawei IAP codelab and the published SDK interfaces. SDK purchase types broader than this platform's one-time contract are not enabled by adding the dependency.
2. Create your business purchase attempt
Have your app call your own backend to begin a purchase of a known product. Authenticate the customer, validate the mapping, and save a business order such as shop_order_1044. Return the provider product ID, supported product type, and an opaque attempt reference to your app.
Persist that reference before opening Huawei. Use the mapping's product type rather than inferring it from the product name.
Implement an application-owned endpoint such as POST /api/purchases/huawei/attempts. It authenticates the customer and returns businessOrderId, providerProductId, and the catalog's priceType. This is not a Payment API endpoint. Keep the returned attempt ID in persistent app state; it is also how you recover after process termination.
Your evidence endpoint can be POST /api/purchases/huawei/verify. Accept the business attempt ID and original SDK evidence, authenticate the caller, then load the platform product mapping and customer ID from your database. Use the proposed encoder above only after adapter support is confirmed. Never trust a caller-supplied platform product mapping or client_user_id without resolving ownership yourself.
Checkpoint: before the SDK opens, the backend has a saved owned purchase attempt and the app has a way to recover its ID.
Use the Android HTTP and persistent-order helpers from Connect your app to checkout. Add this to the same checkout Activity after it has loaded and displayed the configured product:
private var savedBusinessOrderId = ""
private val REQUEST_HUAWEI_PURCHASE = 4101
private val REQUEST_HUAWEI_ENVIRONMENT = 4102
fun beginHuaweiPurchase(productMappingId: String) {
// Disable repeated Pay taps while this call or a purchase screen is active.
val body = org.json.JSONObject().put("productMappingId", productMappingId)
checkoutApi.request("/api/purchases/huawei/attempts", body) { outcome ->
outcome.onSuccess { attempt ->
if (attempt.getString("providerProductId") != providerProductId ||
attempt.getInt("priceType") != configuredPriceType) {
showPurchaseMessage("Product configuration changed. Reload the product before paying.")
return@onSuccess
}
savedBusinessOrderId = attempt.getString("businessOrderId")
if (!pending.edit().putString("businessOrderId", savedBusinessOrderId).commit()) {
showPurchaseMessage("Cannot save this purchase. Open your order history before paying.")
return@onSuccess
}
launchHuaweiPurchase()
}.onFailure { showPurchaseMessage("Cannot prepare this purchase. Check your existing orders.") }
}
}
fun refreshBusinessOrder() = refreshPendingOrder()
providerProductId and configuredPriceType are the product values loaded from your catalog in step 1. pending, checkoutApi, and refreshPendingOrder come from the shared Android example. Your Pay button calls beginHuaweiPurchase with the selected platform mapping ID. The backend reuses an unresolved attempt and authorizes that mapping for the signed-in customer.
3. Launch Huawei IAP on Android
With com.huawei.hms.iap classes imported, build the purchase request:
fun launchHuaweiPurchase() {
val request = PurchaseIntentReq().apply {
productId = providerProductId
priceType = configuredPriceType
developerPayload = savedBusinessOrderId
}
Iap.getIapClient(activity).createPurchaseIntent(request)
.addOnSuccessListener { result ->
val status = result.status
if (status.hasResolution()) {
try {
status.startResolutionForResult(activity, REQUEST_HUAWEI_PURCHASE)
} catch (error: android.content.IntentSender.SendIntentException) {
recoverPendingPurchase()
}
} else {
recoverPendingPurchase()
}
}
.addOnFailureListener {
recoverPendingPurchase()
}
}
providerProductId and configuredPriceType come from the saved catalog mapping; savedBusinessOrderId is the opaque business attempt reference sent as developerPayload. Keep the request codes distinct from other Activity results. recoverPendingPurchase is implemented in step 6. Use IN_APP_NONCONSUMABLE for an activated non-consumable mapping. Handle environment/account resolution and provider errors in your checkout UI before retrying.
In your existing onActivityResult override, call super.onActivityResult and add this branch to parse the SDK response:
if (requestCode == REQUEST_HUAWEI_PURCHASE && data != null) {
val result = Iap.getIapClient(this).parsePurchaseResultInfoFromIntent(data)
when (result.returnCode) {
OrderStatusCode.ORDER_STATE_SUCCESS -> {
submitEvidenceToYourBackend(
result.inAppPurchaseData,
result.inAppDataSignature,
result.signatureAlgorithm
)
}
OrderStatusCode.ORDER_PRODUCT_OWNED -> recoverPendingPurchase()
else -> refreshBusinessOrder()
}
}
submitEvidenceToYourBackend is implemented below; step 6 implements recovery. refreshBusinessOrder uses your authenticated order-status endpoint. submitEvidenceToYourBackend sends the unmodified purchase data and signature over your authenticated HTTPS connection, together with the saved business attempt reference. It does not grant the product. Handle null/missing activity results by recovering on app resume. See Huawei's client purchase flow.
The following function completes the app-to-backend handoff using CheckoutApi.kt. savedBusinessOrderId is the persisted attempt and checkoutApi authenticates as the current customer:
fun submitEvidenceToYourBackend(rawData: String, signature: String, algorithm: String?) {
val body = org.json.JSONObject()
.put("businessOrderId", savedBusinessOrderId)
.put("inAppPurchaseData", rawData)
.put("inAppDataSignature", signature)
if (!algorithm.isNullOrEmpty()) body.put("signatureAlgorithm", algorithm)
checkoutApi.request("/api/purchases/huawei/verify", body) { outcome ->
outcome.onSuccess { refreshBusinessOrder() }
.onFailure { showPurchaseMessage("Verification is pending. Your purchase can be recovered.") }
}
}
refreshBusinessOrder queries the owned business order through the shared status endpoint. If the verification is still pending and has no platform order yet, your endpoint returns that pending business state while its server worker polls the verification resource. Preserve the evidence securely until the server acknowledges durable receipt; a retry must reuse the same purchase and verification command.
4. Verify the purchase on your backend
Authenticate the caller and load their saved business attempt. After the activated adapter confirms the proposed v1 format above, encode the original SDK data and signature with the supplied helper, then persist the verification body and idempotency key before sending it. Save this request as verification.json:
{
"app_id": "app_demo",
"product_id": "product_coins_100",
"client_user_id": "customer_4821",
"external_order_id": "shop_order_1044",
"purchase_evidence": "{\"format\":\"huawei-iap-v1\",\"inAppPurchaseData\":\"ORIGINAL_HUAWEI_PURCHASE_DATA_STRING\",\"inAppDataSignature\":\"ORIGINAL_HUAWEI_SIGNATURE_STRING\"}"
}
curl --fail-with-body --request POST \
'https://pay.appconnect.cn/payment/v1/hms/verifications' \
--header "Authorization: Bearer $PAYMENT_API_KEY" \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: verify-shop-order-1044-v1' \
--data-binary @verification.json
Save data.id as the verification ID. Query it using purchases:read:
curl --fail-with-body \
'https://pay.appconnect.cn/payment/v1/hms/verifications/ver_demo_001' \
--header "Authorization: Bearer $PAYMENT_API_KEY"
For pending or unknown, keep checking the original verification. For rejected, retain its failure_code and show an appropriate failure/support path. For verified, read the associated order_id through GET /orders/{order_id}. Validate its application, external reference, currency, product-derived amount, and confirmed payment before delivery.
5. Deliver and attest fulfillment
Apply the credit or unlock in a durable, idempotent transaction using your business purchase key. For consumables, fulfillment means the credits have been granted, not that the customer has spent them.
After delivery is durable, read the channel confirmation:
curl --fail-with-body \
'https://pay.appconnect.cn/payment/v1/orders/ord_demo_004/channel-confirmation' \
--header "Authorization: Bearer $PAYMENT_API_KEY"
If the flow requires an attestation, save this as fulfillment.json using the confirmation's current version:
{
"expected_version": 2,
"fulfillment_reference": "fulfillment_shop_order_1044",
"fulfilled_at": "2026-10-28T10:03:00+08:00"
}
curl --fail-with-body --request POST \
'https://pay.appconnect.cn/payment/v1/orders/ord_demo_004/fulfillment-attestations' \
--header "Authorization: Bearer $PAYMENT_API_KEY" \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: attest-shop-order-1044-v1' \
--data-binary @fulfillment.json
This write requires purchases:write. Monitor the original confirmation: awaiting_attestation, pending, confirmed, unknown, manual_review, or not_required. expected_version here is the channel-confirmation version, not the order version.
In this platform flow, provider confirmation or consumption is coordinated on the server after attestation when required by the activated adapter. Do not also call Huawei consumption independently from the app. Non-consumables must not be consumed. Confirm the adapter's ownership of this step during activation; do not infer the entire confirmation policy from product type alone.
6. Restore and recover purchases
On app resume and through a Restore purchases action, call obtainOwnedPurchases. Huawei returns purchase data and matching signatures in parallel lists. Process every page, and preserve each pair unchanged. Recover an unfinished consumable before offering another purchase of the same product.
Add these imports to your checkout Activity:
import com.huawei.hms.iap.Iap
import com.huawei.hms.iap.entity.OwnedPurchasesReq
Use this recovery function. recoveryRunning is an Activity/coordinator field. The example queues one evidence item at a time on your backend before requesting another page:
private var recoveryRunning = false
fun recoverPendingPurchase() {
if (recoveryRunning) return
recoveryRunning = true
fun finishRecovery(message: String) {
recoveryRunning = false
showPurchaseMessage(message)
refreshBusinessOrder()
}
fun readPage(token: String?, seen: Set<String>) {
val request = OwnedPurchasesReq().apply {
priceType = configuredPriceType
if (!token.isNullOrEmpty()) continuationToken = token
}
Iap.getIapClient(activity).obtainOwnedPurchases(request)
.addOnSuccessListener { result ->
val purchases = result.inAppPurchaseDataList ?: emptyList()
val signatures = result.inAppSignature ?: emptyList()
if (purchases.size != signatures.size) {
finishRecovery("Cannot read complete purchase evidence. Try restoring later.")
return@addOnSuccessListener
}
fun submitNext(index: Int) {
if (index >= purchases.size) {
val next = result.continuationToken
if (next.isNullOrEmpty()) finishRecovery("Purchase recovery submitted. Check your orders.")
else if (next in seen) finishRecovery("Recovery needs support. Your purchases are retained.")
else readPage(next, seen + next)
return
}
val body = org.json.JSONObject()
.put("inAppPurchaseData", purchases[index])
.put("inAppDataSignature", signatures[index])
if (!result.signatureAlgorithm.isNullOrEmpty()) {
body.put("signatureAlgorithm", result.signatureAlgorithm)
}
checkoutApi.request("/api/purchases/huawei/restore", body) { outcome ->
outcome.onSuccess { submitNext(index + 1) }
.onFailure { finishRecovery("Recovery is pending. Try Restore purchases again later.") }
}
}
submitNext(0)
}
.addOnFailureListener { finishRecovery("Cannot load purchases yet. Check your account and connection.") }
}
readPage(null, emptySet())
}
Implement POST /api/purchases/huawei/restore on your backend. It authenticates the customer and durably queues each original evidence pair. The endpoint must not trust the app to select an account, product mapping, or existing order. Use the verified purchase identity and saved records to find the original business attempt. If recovery follows an app reinstall, the backend still retains this mapping; do not assume the newest local attempt owns every returned purchase.
For each item, the backend sends POST /hms/restorations using purchases:write and the same VerifyPurchase body shape from step 4. Persist a restoration command and its own idempotency key before sending. For example, after saving the resolved body as restoration.json:
curl --fail-with-body --request POST \
'https://pay.appconnect.cn/payment/v1/hms/restorations' \
--header "Authorization: Bearer $PAYMENT_API_KEY" \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: restore-original-purchase-1044-v1' \
--data-binary @restoration.json
Save the returned verification ID and reconcile it exactly as in step 4. A retry reuses that command, not a fresh order or key. The proposed v1 evidence encoding still requires adapter support.
A restored non-consumable restores the existing entitlement. A repeated consumable verification must not credit it twice. Bind a unique provider purchase identity to one durable fulfillment record. If a purchase belongs to a different application account, follow your account-recovery policy instead of silently moving ownership. Run a separate recovery pass for each enabled product type; do not mix types in one request.
Checkpoint: kill the app after paying but before your backend acknowledges verification, reopen it, and recover the purchase once. Repeat after clearing local app data; the server must still recognize the original purchase.
7. Refund and test
For an eligible paid platform order, follow Refund a payment and track execution to completion. Apply your entitlement policy separately. Do not use client-side consumption as a refund, and do not remove an entitlement merely because a refund request was accepted for review.
Use an approved test account and provider test configuration coordinated with NextAppMarket. The Payment API remains on its configured production origin; provider test-account setup does not create a separate public sandbox API. Follow Test your integration before initiating any financial transaction.
Test these cases before release:
- A consumable purchase is verified, credited once, attested, and then confirmed/consumed by the server-owned adapter.
- A non-consumable purchase unlocks once, survives reinstall, and restores to the same application account.
- Customer cancellation, missing activity data, pending payment, and a network failure lead to reconciliation without a second purchase.
- An already-owned purchase is recovered, including more than one owned-purchase page.
- Repeated callbacks, repeated restoration, and a server restart between delivery and attestation do not duplicate delivery.
- A rejected signature, wrong product, wrong application, or another account's evidence does not grant access.
- A refund can be retrieved through its final outcome, with the resulting entitlement decision recorded separately.
8. Common problems and how to fix them
| Symptom or SDK result | Check and fix |
|---|---|
| SDK dependency does not resolve | Add Huawei's repository in both plugin and dependency repository management. Check the pinned versions, proxy, and Gradle repository policy. Do not solve it by adding arbitrary third-party mirrors. |
ORDER_HWID_NOT_LOGIN or an environment resolution is required | Run isEnvReady() and launch its resolution from the foreground Activity. Recheck readiness after the result. Do not open the purchase screen while account setup is unresolved. |
ORDER_STATE_IAP_NOT_ACTIVATED | Confirm that IAP is activated for the registered application and that the installed build matches its configuration. Check agconnect-services.json, package name, and signing certificate with NextAppMarket. |
ORDER_STATE_PRODUCT_INVALID or the product list is empty | Check the provider product ID, publication/availability, enabled country, and product type. The SDK receives the Huawei ID; the Payment API receives the platform mapping ID. |
ORDER_STATE_PMS_TYPE_NOT_MATCH | Use the catalog's consumable/non-consumable type consistently in product lookup, purchase, and recovery. |
ORDER_ACCOUNT_AREA_NOT_SUPPORTED or ORDER_STATE_PRODUCT_COUNTRY_NOT_SUPPORTED | Check account region and product availability. Do not repeatedly launch payment or change the customer's region programmatically. |
ORDER_PRODUCT_OWNED | Recover the existing purchase before offering another. For a consumable, check whether delivery/attestation/provider consumption is unfinished. |
ORDER_STATE_CANCEL or no activity result | Refresh the saved attempt and query owned purchases when appropriate. Client cancellation is not a durable server-side payment result. |
ORDER_STATE_PENDING, a network error, or app termination | Keep the original purchase and verification reference. Retry status checks and owned-purchase recovery; do not issue another purchase automatically. |
| Evidence verification is rejected | Preserve original SDK strings; check the activated evidence version, product mapping, app identity, signature configuration, and purchase ownership. Capture the verification ID and failure code, not raw evidence, in logs. |
| Consumable cannot be bought again | Check fulfillment and channel confirmation. The server adapter owns consumption after attestation in this flow; do not add a competing app-side consume call. |
| The purchase was credited twice | Enforce uniqueness on the verified provider purchase and business fulfillment key. Callback, verification, and restoration are different paths to the same purchase. |
| Restore purchases finds nothing | Check the signed-in Huawei account, selected product type, pagination, and whether a consumable has already been consumed. Previously delivered consumables are recovered from your backend ledger, not re-granted from an empty SDK result. |
VERSION_CONFLICT during attestation | Retrieve channel confirmation and use its current version. Do not substitute the payment order's version. Reassess whether attestation is still required. |
| SDK succeeds but the order remains pending | Query the saved verification, inspect its associated order, and keep reconciliation running. A successful client callback alone cannot authorize delivery. |
For shared authentication, refund, webhook, and retry errors, use Errors and recovery. Include app/package ID, SDK version, product mapping, verification/order IDs, error code, and UTC timestamp in a support report. Keep original evidence private.