Skip to main content

Mini Program Virtual Payment Integration Guide

Note

Mini Program Virtual Payment integration is coming soon. Stay tuned.

After you create a Mini Program Virtual Payment integration in the CloudBase console (Integration Center), the platform automatically generates an HTTP cloud function that packages signing, order query, and callback handling. This guide walks you through creating the integration, configuring WeChat message push, and invoking the checkout from your mini program.

About function names: The cloud function generated by the Integration Center is named <integration-name>-<random-string> (for example, xpay-demo-rwmx67sc). For readability, this guide calls it xpay-common — replace it with your actual function name in your code.

Virtual payment targets virtual goods (item purchases, coins, subscriptions). It solves iOS payment compliance: WeChat Pay and Apple IAP are bridged underneath, so the same wx.requestVirtualPayment call opens the checkout on both Android and iOS.

Difference from the WeChat Pay integration

For physical goods, use the WeChat Mini Program Payment Integration Guide. Virtual goods must use virtual payment; otherwise your app cannot pass App Store review.

Overall flow:

  • Forward request: Mini Program → wx.cloud.callHTTPFunction (platform handles auth and injects openid) → xpay-common HTTP cloud function → returns signData with dual signatures
  • Checkout: the client calls wx.requestVirtualPayment with the signed parameters (the server does not place the order)
  • Async callback: WeChat message push → Integration Center gateway (signature verification + decryption) → xpay-common → your business logic

Architecture Overview​

Responsibilities:

RoleResponsibility
Mini ProgramObtains signed parameters via callHTTPFunction, then calls wx.requestVirtualPayment
xpay-commonBuilds signData and dual signatures; provides order query; handles idempotency and delivery on callbacks
WeChat Virtual PaymentExecutes the actual payment (WeChat Pay on Android, Apple IAP on iOS)
Integration CenterHosts credentials and environment variables; receives callbacks, verifies and decrypts them, then forwards plaintext
Your businessMaintains the item catalog (productId + price), order state machine, idempotent delivery, and risk control
The server does not place orders

This is the biggest difference from regular WeChat Pay. With regular payments, the server calls the V3 API to obtain a prepay_id. With virtual payment, the server only produces signed parameters — the order is actually placed on the client by wx.requestVirtualPayment.


Differences from the WeChat Pay Integration​

DimensionWeChat Pay (physical)Virtual Payment
GoodsPhysical goods and offline servicesVirtual goods: items, coins, subscriptions
Client APIwx.requestPaymentwx.requestVirtualPayment
Order placementServer calls the V3 API for prepay_idServer builds signData + dual signatures locally; the client opens checkout
Server APIs/v3/pay/transactions/*/xpay/* (Midas)
Callback channelWeChat Pay callback + AES-GCMMini Program message push + AES-256-CBC
Core credentialsMerchant ID, APIv3 key, merchant certificate, public keyOfferID, AppKey, AppID / AppSecret
Fees and settlementAbout 0.6%, T+1Android 1%, T+3; iOS 12%, about 45–60 days

Prerequisites​

ItemRequirement
Mini ProgramWeChat-verified and virtual payment enabled (category and entity requirements follow the WeChat Official Platform)
Virtual payment merchant infoOfferID and AppKey obtained from the MP console under "Virtual Payment → Basic Configuration"
ItemsItems created and published in the MP console, with productId and price. Publishing takes minutes to half an hour to sync
Base library≥ 3.15.2 (required by wx.cloud.callHTTPFunction)
DevToolsLatest stable version; payments must be tested on a real device
About iOS

iOS collects payments through Apple IAP with a minimum amount of 1 CNY (100 cents). Item tiers below 1 CNY always fail on iPhone. Apple payments do not support the sandbox, so debug on Android or use a low-price live item.


Step 1 · Prepare Virtual Payment Credentials​

Sign in to the WeChat Official Platform and collect the values below. You will enter them in the Integration Center form in Step 2.

1.1 Virtual Payment Basic Configuration​

Path: MP console → Virtual Payment → Basic Configuration.

#CredentialDescription
1offerIdVirtual payment merchant ID (Midas app ID)
2appKeyPayment signing key used to compute paySig; server-side only

1.2 Mini Program Basic Settings​

Path: MP console → Settings → Basic Settings.

#CredentialDescription
3appIdMini Program AppID
4secretMini Program AppSecret, used to exchange for sessionKey; server-side only

1.3 Message Push Configuration (Callback)​

Path: MP console → Virtual Payment → Basic Configuration → Basic Settings → Delivery Push Configuration.

#CredentialDescription
5tokenMessage push token used for signature verification
6msgSecretEncodingAESKey (43 characters) used for decryption in secure mode
Note

Virtual payment delivery push reuses the signature and decryption capabilities of message push, but the settings live under Virtual Payment, not under "Development → Message Push". Both places point to the same Token and EncodingAESKey.

When callbacks are hosted by the Integration Center (system mode), these values are injected by the platform and used by the gateway, so your code does not implement any cryptography.

1.4 Credential Checklist​

#Integration Center field / Environment variableSource
1offerId1.1
2appKey1.1
3appId1.2
4secret1.2
5token1.3
6msgSecret1.3

Step 2 · Create the Integration in CloudBase​

2.1 Open the Integration Center​

Path: CloudBase console → Templates & Integrations → Integration Center → Mini Program Virtual Payment → Create Integration.

2.2 Fill in the Integration Details​

The wizard requires:

FieldDescription
Integration nameCustom, for example xpay-demo. Determines the function name prefix and callback subdomain
EnvironmentThe CloudBase environment that owns the resources
CredentialsofferId / appKey / appId / secret from 1.4
Callback targetThe cloud function that receives callbacks; without it, pushes have nowhere to go
Token / EncodingAESKeytoken / msgSecret from 1.3, character-for-character identical to the MP console
Encryption / Data formatPlaintext / compatible / secure; XML / JSON. Must match the MP console
Event subscriptionSelect the xpay_* events you want to receive

2.3 Generated Resources​

Resources are provisioned asynchronously, which takes about 1–2 minutes:

ResourceDescription
HTTP cloud functionNamed <integration-name>-<random-string>, for example xpay-demo-rwmx67sc
Environment variablesCredentials and callback URLs injected by the platform; no plaintext in code
Callback domainhttps://<integration-id>.integration-callback.tcloudbase.com
Callback routePreset by the platform; do not change the path
Launch order

Create the integration and wait until it is ready before submitting the push configuration in the WeChat console. WeChat immediately sends a GET probe when you save the configuration; if the integration is not ready, the probe fails.


Step 3 · Configure WeChat Message Push​

  1. Open the MP console → Virtual Payment → Basic Configuration → Basic Settings → Delivery Push Configuration.
  2. Enter the callback URL provided by the Integration Center.
  3. Enter the Token and EncodingAESKey (identical to the Integration Center form).
  4. Select the same data format (JSON / XML) and encryption mode (plaintext / compatible / secure).
  5. Save and confirm the GET probe succeeds (the gateway echoes echostr).

After configuration, check the callback status in the Integration Center, or confirm from the function logs that pushes reach /xpay/callback.


Step 4 · Understand the xpay-common Function​

When the integration is created, the platform injects the credentials and callback settings as environment variables; the function reads them via process.env. To change them, go to "Integration Center → your integration → Edit". Saving triggers an automatic redeploy (about 1–2 minutes).

4.1 Source Layout​

xpay-common/
├── index.js Function entry
├── app.js Express assembly (JSON and XML)
├── scf_bootstrap SCF custom runtime bootstrap
├── cloudbaserc.json CloudBase deploy config (env placeholders)
├── config/config.js Reads process.env
├── routes/xpay.js Route table
├── controllers/xpayController.js Create/query/callback responses
├── services/
│ ├── xpayService.js Order creation + event dispatch + idempotency
│ ├── signService.js Dual signing (paySig + signature)
│ └── strategies/xpayStrategy.js /xpay/* server APIs
├── utils/
│ ├── wechatApi.js code2Session / access_token
│ ├── xml.js XML push parsing
│ └── cloudbaseAuth.js Reads injected x-wx-openid / x-wx-appid
└── tests/ Unit tests

4.2 Routes​

RouteAuthDescription
POST /xpay/create_orderUpstream gateway (requires code)Returns payData for wx.requestVirtualPayment
POST /xpay/query_orderUpstream gateway (requires code + x-wx-openid)Order query; fallback delivery when pushes are lost
POST /xpay/callbackGateway verification; self-verified when using custom callbacksSingle entry for all xpay_* events
Auth boundary

The function has no auth middleware. It trusts that requests only come through callHTTPFunction or the Integration Center gateway. If you expose it publicly, add authentication upstream (API Gateway / CloudRun).

4.3 Response Structure​

Create and query APIs return:

{
"code": 0,
"msg": "success",
"data": {
"signData": "...",
"mode": "short_series_goods",
"paySig": "...",
"signature": "..."
}
}

code 0 means success; data holds the parameters needed to open checkout.


Step 5 · Mini Program Integration​

5.1 Initialize CloudBase​

// app.js
const ENV_ID = 'your-env-id' // Replace with your CloudBase environment ID
const FN_NAME = 'xpay-demo-xxxxxx' // Replace with the generated function name

App({
globalData: { envId: ENV_ID, fnName: FN_NAME },

onLaunch() {
wx.cloud.init({ env: ENV_ID, traceUser: true })
},
})

callHTTPFunction does not require enabling a CloudBase Auth identity provider, installing @cloudbase/js-sdk, or building npm.

5.2 Wrap the Call​

// utils/xpay.js
function callXpay(path, data) {
const { envId, fnName } = getApp().globalData

return new Promise((resolve, reject) => {
wx.cloud.callHTTPFunction({
name: fnName,
config: { env: envId },
method: 'POST',
header: { 'Content-Type': 'application/json' },
path, // e.g. /xpay/create_order
data,
success: (res) => {
// The body may be a string or an already-parsed object
const body = typeof res.data === 'string' ? JSON.parse(res.data) : res.data
resolve(body)
},
fail: (err) => reject(new Error(err.errMsg || 'Request failed')),
})
})
}

module.exports = { callXpay }

5.3 Start the Payment​

// pages/index/index.js
const { callXpay } = require('../../utils/xpay')

Page({
data: { paying: false },

async onTapPay() {
if (this.data.paying) return
this.setData({ paying: true })

try {
// 1. Get the login code
const { code } = await wx.login()

// 2. The server builds signData and dual signatures (no order is placed)
const res = await callXpay('/xpay/create_order', {
code,
productId: 'product_001', // Must match the item in the MP console
goodsPrice: 100, // In cents (100 = 1 CNY)
buyQuantity: 1,
})

if (res.code !== 0) {
wx.showToast({ title: res.msg || 'Failed to create order', icon: 'error' })
return
}

// 3. Open checkout (WeChat Pay on Android, Apple IAP on iOS)
const { signData, mode, paySig, signature } = res.data
await wx.requestVirtualPayment({ signData, mode, paySig, signature })

wx.showToast({ title: 'Payment successful', icon: 'success' })

// 4. Fallback order query (final state comes from the server callback)
const q = await callXpay('/xpay/query_order', {
code,
outTradeNo: signData.outTradeNo,
})
if (q.code === 0 && q.data?.paid) {
// Paid: trigger fallback delivery if the callback has not arrived yet
}
} catch (err) {
if (err?.errMsg?.includes('cancel')) return // User cancellation is not a failure
console.error('[xpay] Payment failed:', err)
wx.showToast({ title: 'Payment failed', icon: 'error' })
} finally {
this.setData({ paying: false })
}
},
})

Key points:

  • Pass signData through unchanged: any JSON.parse → stringify that reorders keys invalidates the signature.
  • Never deliver goods based on the client success callback: users may exit or lose network. Delivery must be driven by the server callback; order query is only a fallback.
  • goodsPrice is in cents and must match the item price published in the MP console.
  • Base library: callHTTPFunction requires ≥ 3.15.2. The simulator cannot complete a real payment — test on a real device.

Step 6 · Implement Delivery and Order Logic​

The template marks three places with TODO(业务侧) (business side). You must replace them before going live:

LocationTemplate stateRequired for production
IdempotencyIn-process Set with a size capDatabase unique index (cold starts lose memory state)
DeliveryUpdates in-memory order state onlyYour order table plus item/coin balances
iOS refund inquiryApproves refunds when no delivery is foundPersistent order lookup + real risk rules, or delivered orders may be refunded

6.1 Callback Events​

EventHandling
xpay_goods_deliver_notifyCore: idempotent delivery
xpay_coin_pay_notifyUpdate coin balance
xpay_refund_notifyUpdate order + reclaim items
xpay_complaint_notifyRecord and follow up manually
xpay_wxpay_callback_notifyRecord for risk control, appeal as needed
xpay_subscribe_ios_refund_query_notifyAnswer the refund decision within 3 seconds

6.2 Fields Required for Delivery​

The callback result already includes goods, so you do not need to parse the push structure yourself:

{
"ok": true,
"eventType": "xpay_goods_deliver_notify",
"biz": "deliver",
"outTradeNo": "ORDER20261020001",
"wxOrderId": "WXORDER...",
"openid": "o_xxx",
"goods": {
"productId": "goods_001",
"quantity": 2,
"attach": "uid=123"
},
"goodsValid": true,
"missingFields": []
}

An outTradeNo alone is not enough to deliver — you must know which item and how many, that is goods.productId and goods.quantity.

6.3 Idempotency and Responses​

  • Use the platform order ID wxOrderId (WeChatPayInfo.MchOrderNo in the push) as the idempotency key, falling back to outTradeNo. Deduplicating by merchant order number misjudges repeated order numbers after cancellations, causing missed deliveries.
  • The order must be "check duplicate → process → mark". If you mark first and processing fails, retries are ignored as duplicates and the order is lost for good.
  • Grade your responses: structural errors (missing Event) should return {"ErrCode":0,"ErrMsg":"success"} to avoid pointless retries; business errors should return non-zero to trigger a retry. Returning 0 for everything silently drops orders.
  • Do not always return success when goods info is incomplete: a missing productId should mark the order as abnormal (retries cannot fix it); a missing quantity can default to 1 with goodsValid: false; an illegal quantity (0, decimal, non-numeric) should throw so WeChat retries — never silently change it to 1, or "bought 5, delivered 1" becomes an invisible loss.
  • Configure a scheduled trigger every 5 minutes to call /xpay/query_order and deliver orders that were paid but not yet delivered.

FAQ​

Q1: 41004 appsecret missing​

secret is empty or not injected.

Check the environment variable listing in the startup log (lengths only, no values) to confirm secret has a length. When using the Integration Center, do not run tcb fn deploy separately and overwrite the injected variables.

Q2: 41002 appid missing​

appId is empty or not injected; troubleshoot the same way.

Q3: Callback returns 404 or no push arrives​

Check in order:

  1. The push URL in the WeChat console points to the Integration Center callback domain.
  2. The "callback target" in the Integration Center is bound to the intended cloud function.
  3. The data format (JSON / XML) and encryption mode match the WeChat console — a mismatch parses to an empty object, leaving no matching route and returning 404.
  4. The launch order is correct: create the integration and wait until ready, then submit the WeChat console configuration.

Q4: Callback signature verification fails​

  • Token and EncodingAESKey must match the WeChat console character for character.
  • In secure mode, verify msg_signature (whose signed string includes Encrypt), not signature. This is the most common mistake.
  • With Integration Center hosting (system mode), the gateway verifies and decrypts; do not verify again in your code.

Q5: signData signature is invalid​

Signing must use the original string. Parsing and re-serializing, trimming, or padding fields changes the signed content. The template's signService.js accepts strings only — keep that constraint in your own code.

Q6: iOS payment fails / amount restrictions​

  • iOS uses Apple IAP with a minimum of 1 CNY; lower tiers cannot be purchased.
  • Apple payments have no sandbox. The sandbox only works in the development/trial build, and real-device previews may be blocked with PAYMENT_ILLEGAL_IN_SANDBOX.
  • The template always uses live mode (env=0) and offers no sandbox switch.

Q7: COIN_OR_PRODUCT_ID_CREATED_IN_RECENTLY​

The item was just created or published and WeChat has not finished syncing. Retry after a few minutes to half an hour.

Q8: query_order cannot find the order​

query_order only queries cash orders, not coin orders; use the balance API with idempotent retries for coins. Also verify order ownership (the mapping between openid and the order); otherwise anyone can query others' orders with their own code and someone else's order number.

Q9: Can I rename the function or change the callback path?​

No. The random suffix in the function name is bound to the callback subdomain and injected environment variables — renaming breaks callbacks. The callback route is preset by the platform; do not change the path.

Q10: Does the cloud function remain after deleting the integration?​

Deleting an integration does not delete the cloud function. Deletion releases the callback domain and route (irreversibly), but the function and its environment variables remain and must be cleaned up manually.


Appendix · Environment Variables​

The Integration Center injects the following variables automatically — no manual maintenance is required. They are listed here for troubleshooting and extension.

VariableSourceDescription
appIdInjectedMini Program AppID
secretInjectedMini Program AppSecret
offerIdInjectedVirtual payment merchant ID
appKeyInjectedPayment signing key
tokenInjectedMessage push token
msgSecretInjectedEncodingAESKey (43 characters)
notifyUrlCallbackTypeInjectedsystem = gateway verification; absent = self-verify
msgEncryptTypeInjectedplaintext / aes / compatible
sandboxAppKeyInjectedSandbox key; the template uses live mode, leave it empty
notifyFormatConfigure yourselfjson (default) / xml; must match the WeChat console
trustBearerIdentityConfigure yourselfWhether to trust Authorization: Bearer JWTs; off by default
Risk of standalone deployment

functions[].envVariables in cloudbaserc.json uses whole-object replacement. Running tcb fn deploy with empty credentials overwrites the platform-injected values with empty strings (41004 appsecret missing). When using the Integration Center, redeploy by editing the integration, or complete the configuration first.


Further Reading​

TopicLink
Example project (cloud function template + mini program demo)https://github.com/1225zhangqian/awesome-cloudbase-examples/tree/master/integration/cloudbase-virtualpay
wx.requestVirtualPayment APIhttps://developers.weixin.qq.com/miniprogram/dev/api/payment/wx.requestVirtualPayment.html
wx.cloud.callHTTPFunctionhttps://developers.weixin.qq.com/miniprogram/dev/wxcloudservice/wxcloud/HTTPCloudFunction.html
WeChat Mini Program Pay integration (physical goods)WeChat Mini Program Payment Integration Guide