WeChat App
Use wechat_app to open WeChat Pay from a native Android or iOS app. Your backend creates the order and returns signed SDK fields. Your app maps those fields to PayReq; it does not generate a second signature.
1. Prepare your app
Follow the Android or iOS setup below, then continue with the shared order request in step 2. You need an activated wechat_app entry, a platform application ID, and a server Payment API key with payments:write, payments:read, and operations:read.
1.1 Confirm the native app identity
Obtain the configured WeChat AppID and the registered application identity from NextAppMarket. Record the Android application ID and signing-certificate fingerprint, or the iOS bundle ID and Universal Link. These values must describe the app actually installed on the customer's phone. A debug signature, release signature, and store-managed signing signature can be different.
The platform application ID selects your payment account. The WeChat AppID registers your native client. They are not interchangeable. The platform returns an already signed native payment request; no merchant API secret belongs in your app.
1.2 Install OpenSDK on Android
- Make Maven Central available in
settings.gradle:
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
}
- Add the pinned published SDK to
app/build.gradle:
dependencies {
implementation 'com.tencent.mm.opensdk:wechat-sdk-android:6.8.40'
}
If your app already uses WeChat Login or sharing, use one compatible OpenSDK dependency for all three features. Remove duplicate SDK binaries from the dependency graph. Keep the SDK's bundled consumer rules enabled when shrinking release builds.
- Merge these entries into
app/src/main/AndroidManifest.xml:
<uses-permission android:name="android.permission.INTERNET" />
<queries>
<package android:name="com.tencent.mm" />
</queries>
queries is a direct child of manifest. It lets package-visibility-aware Android versions detect WeChat. Inside application, register the payment callback:
<activity
android:name=".wxapi.WXPayEntryActivity"
android:exported="true"
android:launchMode="singleTask"
android:taskAffinity="${applicationId}"
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
Do not add an intent filter to this Activity. Its class must resolve to the installed application's package followed by .wxapi.WXPayEntryActivity. If your namespace differs from applicationId, use the fully qualified class name and ensure each build variant has the matching callback class.
- Add a string resource in
res/values/strings.xml:
<string name="wechat_app_id" translatable="false">YOUR_ASSIGNED_WECHAT_APP_ID</string>
- Sync Gradle, build, and initialize the API after your app's normal SDK/privacy initialization gate:
val wechat = com.tencent.mm.opensdk.openapi.WXAPIFactory.createWXAPI(
applicationContext, getString(R.string.wechat_app_id), true
)
wechat.registerApp(getString(R.string.wechat_app_id))
Checkpoint: the SDK imports resolve, isWXAppInstalled detects a device installation, and the release build's package/signature matches activation. A simulator or an unrelated debug signature cannot establish payment readiness.
1.3 Install OpenSDK on iOS
- Add the published payment-capable pod to your app target's
Podfile. This SDK requires iOS 12 or later:
platform :ios, '12.0'
target 'YourApp' do
pod 'WechatOpenSDK', '2.0.9'
end
- Run
pod install, commitPodfile.lock, and open the generated.xcworkspace. Do not also link a manually imported WeChat library or a NoPay variant. CocoaPods supplies the system libraries and framework dependencies. - Import
WXApi.hwhere you use the SDK. For a Swift project, add#import "WXApi.h"to your Objective-C bridging header and configure its path in Build Settings. The native examples below use Objective-C at the SDK boundary. - In Target → Info → URL Types, add the assigned WeChat AppID as the URL Scheme. Merge these query schemes into
Info.plist:
<key>LSApplicationQueriesSchemes</key>
<array>
<string>weixin</string>
<string>weixinULAPI</string>
<string>weixinURLParamsAPI</string>
</array>
- Use the Universal Link registered for your app, for example
https://links.example.com/wechat/. Add Signing & Capabilities → Associated Domains →applinks:links.example.com. Serve/.well-known/apple-app-site-associationon that host over HTTPS with no redirect:
{
"applinks": {
"apps": [],
"details": [{"appID": "ABCDE12345.com.example.myapp", "paths": ["/wechat/*"]}]
}
}
Replace the Team ID, bundle ID, host, and path with your app's configured values. Preserve other associated paths when this host also handles login or Alipay.
- Register at app initialization and retain the callback delegate used in step 3:
#import "WXApi.h"
BOOL registered = [WXApi registerApp:configuredWechatAppId
universalLink:@"https://links.example.com/wechat/"];
Checkpoint: registration succeeds, the signed build has the Associated Domains entitlement, and a matching link tapped from another app opens it. Then test a provider round trip. The pod's current simulator packaging excludes arm64; use a supported simulator architecture for build checks and a physical iPhone for payment validation.
The SDK versions are pinned for reproducibility, not a device-certification claim. Setup and payment callback requirements are based on WeChat's App integration guide, the published SDK packages, and Apple's associated-domain configuration.
2. Create the order on your backend
Authenticate the customer, calculate the price, and save the business order and payment attempt as described in Build your payment backend. Save the following body as payment.json, replacing the application and external order IDs:
{
"app_id": "app_demo",
"payment_entry": "wechat_app",
"amount_minor": 1000,
"currency": "CNY",
"external_order_id": "shop_order_1042",
"description": "100 app credits"
}
curl --fail-with-body --request POST \
'https://pay.appconnect.cn/payment/v1/payment-sessions' \
--header "Authorization: Bearer $PAYMENT_API_KEY" \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: create-shop-order-1042-v1' \
--data-binary @payment.json
Use the key and exact body persisted for this attempt. Save data.order.id before returning the business order ID and data.invocation to the client. The Payment API key stays on your server.
If the invocation is null, keep the same order and retrieve its payment session. Do not create another order while the first result is unresolved.
Connect your Pay button
Implement your authenticated POST /api/checkout endpoint, then copy the Android or iOS HTTP helper from Connect your app to checkout. That page defines beginCheckout, refreshPendingOrder, persistence, and the app-facing response used below.
Call beginCheckout with your cart ID and wechat_app. Save businessOrderId before launching the SDK. Your backend returns the platform invocation inside your app's checkout response. Check its kind, required fields, and expires_at; if the instructions are expired, query the existing order instead of opening the SDK. Do not hard-code the example order string or signed fields into a real payment.
Checkpoint: one tap produces one saved business order, one platform order ID, and the expected invocation. Repeat the request for the same unresolved attempt and verify that it does not create a second charge.
3. Open WeChat in your app
Expect invocation.kind === "sdk_fields" and invocation.sdk_fields.provider === "wechat". Check the invocation expiry and ensure the returned provider AppID matches your configured WeChat AppID. Persist the business order ID before launching payment.
Map the fields exactly:
| Payment API field | Android PayReq | iOS PayReq |
|---|---|---|
app_id | appId | Registered with WXApi |
merchant_id | partnerId | partnerId |
prepay_id | prepayId | prepayId |
package | packageValue | package |
nonce | nonceStr | nonceStr |
timestamp | timeStamp (string) | timeStamp (unsigned seconds) |
signature | sign | sign |
signature_algorithm describes the supplied signature. Do not replace it, sign the request again, or substitute platform IDs. The native SDK field names follow WeChat's payment parameter reference.
Android
With OpenSDK installed, import WXAPIFactory from com.tencent.mm.opensdk.openapi and PayReq from com.tencent.mm.opensdk.modelpay.
fun launchWechat(invocation: org.json.JSONObject) {
requireUsableInvocation(invocation, "sdk_fields")
val fields = invocation.getJSONObject("sdk_fields")
require(fields.getString("provider") == "wechat")
require(fields.getString("app_id") == configuredWechatAppId)
for (key in listOf("merchant_id", "prepay_id", "package", "nonce", "timestamp", "signature")) {
require(fields.getString(key).isNotEmpty())
}
val api = WXAPIFactory.createWXAPI(this, configuredWechatAppId, true)
api.registerApp(configuredWechatAppId)
val request = PayReq().apply {
appId = fields.getString("app_id")
partnerId = fields.getString("merchant_id")
prepayId = fields.getString("prepay_id")
packageValue = fields.getString("package")
nonceStr = fields.getString("nonce")
timeStamp = fields.getString("timestamp")
sign = fields.getString("signature")
}
if (!api.isWXAppInstalled || !api.sendReq(request)) {
showCheckoutMessage("WeChat could not open. Check your order or install WeChat.")
refreshPendingOrder()
}
}
// From your Pay button, inside the checkout Activity:
beginCheckout(cartId, "wechat_app", ::launchWechat)
configuredWechatAppId is your registered WeChat AppID; this function belongs in your checkout Activity. Import the two SDK classes above and use the shared requireUsableInvocation, beginCheckout, and refreshPendingOrder implementations from Connect your app to checkout. Disable Pay while launch is active. Never replace the returned timestamp with the device clock.
Create app/src/main/java/com/example/myapp/wxapi/WXPayEntryActivity.kt, replacing com.example.myapp with the installed app's package. This callback opens your app's launcher; your checkout coordinator refreshes the persisted order on resume.
package com.example.myapp.wxapi
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import com.example.myapp.R
import com.tencent.mm.opensdk.modelbase.BaseReq
import com.tencent.mm.opensdk.modelbase.BaseResp
import com.tencent.mm.opensdk.modelpay.PayResp
import com.tencent.mm.opensdk.openapi.IWXAPI
import com.tencent.mm.opensdk.openapi.IWXAPIEventHandler
import com.tencent.mm.opensdk.openapi.WXAPIFactory
class WXPayEntryActivity : Activity(), IWXAPIEventHandler {
private lateinit var api: IWXAPI
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
api = WXAPIFactory.createWXAPI(this, getString(R.string.wechat_app_id), true)
if (!api.handleIntent(intent, this)) finish()
}
override fun onNewIntent(newIntent: Intent) {
super.onNewIntent(newIntent)
setIntent(newIntent)
if (!api.handleIntent(newIntent, this)) finish()
}
override fun onReq(request: BaseReq) { }
override fun onResp(response: BaseResp) {
if (response !is PayResp) return
// No fulfillment here, including when response.errCode == 0.
packageManager.getLaunchIntentForPackage(packageName)?.let {
it.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
startActivity(it)
}
finish()
}
}
Keep WeChat Login's WXEntryActivity separate from this payment callback. Do not depend on PayResp carrying your business order ID; use the value saved before launch. If your app uses a dedicated order screen instead of its launcher, route there explicitly and run the same refresh.
iOS
Import WXApi.h, register your configured AppID with its approved Universal Link, and map the signed fields:
[WXApi registerApp:configuredWechatAppId universalLink:configuredUniversalLink];
if (![self isUsableInvocation:invocation kind:@"sdk_fields"]) {
[self refreshPendingOrder];
return;
}
NSDictionary *fields = invocation[@"sdk_fields"];
if (![fields isKindOfClass:[NSDictionary class]] || ![fields[@"provider"] isEqualToString:@"wechat"]) {
[self showPaymentConfigurationError];
return;
}
for (NSString *key in @[@"merchant_id", @"prepay_id", @"package", @"nonce", @"timestamp", @"signature"]) {
if (![fields[key] isKindOfClass:[NSString class]] || [fields[key] length] == 0) {
[self showPaymentConfigurationError];
return;
}
}
if (![fields[@"app_id"] isEqualToString:configuredWechatAppId]) {
[self showPaymentConfigurationError];
return;
}
NSString *timestamp = fields[@"timestamp"];
NSScanner *scanner = [NSScanner scannerWithString:timestamp];
unsigned long long seconds = 0;
if (![scanner scanUnsignedLongLong:&seconds] || !scanner.isAtEnd ||
seconds > UINT32_MAX) {
[self showPaymentConfigurationError];
return;
}
PayReq *request = [[PayReq alloc] init];
request.partnerId = fields[@"merchant_id"];
request.prepayId = fields[@"prepay_id"];
request.package = fields[@"package"];
request.nonceStr = fields[@"nonce"];
request.timeStamp = (UInt32)seconds;
request.sign = fields[@"signature"];
[WXApi sendReq:request completion:^(BOOL accepted) {
if (!accepted) [self refreshPendingOrder];
}];
The configuration values and UI methods belong to your app. Import <stdint.h> for UINT32_MAX. Validate non-empty string fields and expiry before this mapping. showPaymentConfigurationError displays an error; refreshPendingOrder is implemented in the client integration guide.
iOS callback wiring, including cold starts
Make your existing AppDelegate conform to WXApiDelegate. Keep its login/share response handling and add a payment branch:
- (void)onResp:(BaseResp *)response {
if ([response isKindOfClass:[PayResp class]]) {
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter]
postNotificationName:@"PaymentNeedsRefresh" object:nil];
});
return;
}
// Keep your existing WeChat Login/share response handling here.
}
- (BOOL)routeWechatURL:(NSURL *)url {
if (![url.scheme isEqualToString:configuredWechatAppId]) return NO;
return [WXApi handleOpenURL:url delegate:self];
}
- (BOOL)routeWechatActivity:(NSUserActivity *)activity {
NSURL *url = activity.webpageURL;
if (![activity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb] ||
![url.host isEqualToString:@"links.example.com"] ||
![url.path hasPrefix:@"/wechat/"]) return NO;
return [WXApi handleOpenUniversalLink:activity delegate:self];
}
Replace the configured ID, host, and path with the same values from step 1. In an AppDelegate-based app, forward application:openURL:options: to routeWechatURL: and application:continueUserActivity:restorationHandler: to routeWechatActivity:. Return NO for URLs this router does not own so your other routers can handle them.
For an app without scenes, these are the two AppDelegate entry points. Merge them with any existing provider/deep-link routing:
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url
options:(NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options {
return [self routeWechatURL:url];
}
- (BOOL)application:(UIApplication *)application
continueUserActivity:(NSUserActivity *)userActivity
restorationHandler:(void (^)(NSArray<id<UIUserActivityRestoring>> *))restorationHandler {
return [self routeWechatActivity:userActivity];
}
In a scene-based app, forward both live and launch-time events. These methods belong in your existing SceneDelegate; AppDelegate is your app delegate class containing the two routers above:
- (void)scene:(UIScene *)scene openURLContexts:(NSSet<UIOpenURLContext *> *)contexts {
AppDelegate *app = (AppDelegate *)UIApplication.sharedApplication.delegate;
for (UIOpenURLContext *context in contexts) [app routeWechatURL:context.URL];
}
- (void)scene:(UIScene *)scene continueUserActivity:(NSUserActivity *)activity {
AppDelegate *app = (AppDelegate *)UIApplication.sharedApplication.delegate;
[app routeWechatActivity:activity];
}
After creating your window and coordinator in scene:willConnectToSession:options:, process connectionOptions.URLContexts and connectionOptions.userActivities through those same methods. This handles a return that launches a terminated app. In sceneDidBecomeActive:, refresh saved pending orders regardless of whether a callback arrived.
Add this at the end of your existing scene:willConnectToSession:options: implementation, after creating the UI and payment coordinator (retain your existing window setup):
AppDelegate *app = (AppDelegate *)UIApplication.sharedApplication.delegate;
for (UIOpenURLContext *context in connectionOptions.URLContexts) {
[app routeWechatURL:context.URL];
}
for (NSUserActivity *activity in connectionOptions.userActivities) {
[app routeWechatActivity:activity];
}
Observe PaymentNeedsRefresh in your checkout coordinator and call the status refresh shown in the client integration guide. A notification is a refresh trigger, not a paid event. Keep a strong reference to the delegate for as long as payment callbacks can arrive.
sendReq completion reports dispatch. PayResp.errCode == 0 reports client success, -2 cancellation, and -1 a client/provider error. Every result leads to the backend status check below.
4. Confirm payment and deliver the purchase
When the customer returns, ask your backend for the business order's status. Your backend checks ownership and retrieves the saved platform order:
curl --fail-with-body \
'https://pay.appconnect.cn/payment/v1/orders/ord_demo_001' \
--header "Authorization: Bearer $PAYMENT_API_KEY"
Validate the returned identity, currency, and amount. Fulfill only after the authoritative status is paid, using a unique fulfillment record. A callback or a return to your page is a reason to check this order, not a reason to grant the purchase.
Implement webhooks and order reconciliation so confirmation works even when the customer closes the app or browser. Show a pending state for unresolved results and offer Check payment instead of immediately starting another charge.
5. Refund or close the order
For a paid order, follow Refund a payment to submit a full or partial refund and track it to completion. Use the saved platform order ID; your client does not refund through the provider SDK.
For an abandoned unpaid order, use the close-order flow. Removing the payment UI does not close the order. Run the integration checklist before release.
6. Verify the complete flow
On a real device with WeChat installed, create an approved verification payment and check the returned merchant and amount. Complete payment, return to the app, and verify that the backend confirms the order and delivers once. Repeat for cancellation, app termination, missing callback, and repeated Pay taps. Check both Android release signing and iOS Universal Link/scheme returns.
7. Common problems and how to fix them
| Symptom | Check and fix |
|---|---|
| Android cannot resolve OpenSDK | Check mavenCentral(), the coordinate and pinned version, and repository filters. Remove a duplicate old SDK dependency. |
| SDK says WeChat is not installed when it is | Check the merged manifest's queries entry on Android and LSApplicationQueriesSchemes on iOS. Test the normal installed WeChat app rather than a cloned/work-profile installation first. |
sendReq is rejected | Check app registration, WeChat availability, required non-empty fields, and the invocation expiry. Preserve and query the order before changing methods. |
errCode = -1 | Check each mapped field, especially provider AppID, prepay_id, package, seconds timestamp, and unchanged signature. Then compare installed package/signature or bundle/Universal Link with activation. Escalate the request ID if the payload is intact. |
errCode = -2 | Customer cancelled the client flow. Query the platform order and restore a useful order screen; do not label the server order closed without a close result. |
| Invalid AppID or merchant mismatch | Verify that you used sdk_fields.app_id for the native request and the assigned platform app_id only on your backend. Have the platform verify the activated merchant/provider binding. |
| Invalid prepay/order identifier | Use the returned prepay_id without rewriting it. Verify it belongs to this invocation and is not an older cached order. |
| Android does not return from WeChat | Check the exact .wxapi.WXPayEntryActivity class/package, exported, singleTask, task affinity, absence of an intent filter, and forwarding in both onCreate and onNewIntent. |
| iOS reports Universal Link verification failure | Compare the registered link, associated-domain host, AASA Team ID/bundle/path, and installed entitlements. The URL Scheme must also be configured. Retest after association changes have propagated. |
| Login works but payment callback does not | Login returns to WXEntryActivity; payment uses WXPayEntryActivity on Android. On iOS, keep a live shared delegate and branch on PayResp without removing login response handling. |
| Success callback but no credit | Check the backend order and fulfillment state. An SDK callback does not credit the account. Recover pending delivery through the same idempotent fulfillment function. |
| Payment was made before expiry but the code says expired on return | Expiry governs launching the invocation. On return, query the existing order even after expiry; do not replace a payment that may have succeeded. |
Start with client wiring for launch/return failures and the shared payment troubleshooting guide for financial-state problems. Send support the request ID, platform and business order IDs, SDK/OS versions, registered app identity, timestamp, and sanitized error code. Do not send API keys, signatures, or customer account data. Provider-specific diagnostics are cross-checked against WeChat's App FAQ.