Mini Program Virtual Payment Integration Guide
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 itxpay-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.
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-commonHTTP cloud function → returnssignDatawith dual signatures - Checkout: the client calls
wx.requestVirtualPaymentwith 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:
| Role | Responsibility |
|---|---|
| Mini Program | Obtains signed parameters via callHTTPFunction, then calls wx.requestVirtualPayment |
| xpay-common | Builds signData and dual signatures; provides order query; handles idempotency and delivery on callbacks |
| WeChat Virtual Payment | Executes the actual payment (WeChat Pay on Android, Apple IAP on iOS) |
| Integration Center | Hosts credentials and environment variables; receives callbacks, verifies and decrypts them, then forwards plaintext |
| Your business | Maintains the item catalog (productId + price), order state machine, idempotent delivery, and risk control |
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
| Dimension | WeChat Pay (physical) | Virtual Payment |
|---|---|---|
| Goods | Physical goods and offline services | Virtual goods: items, coins, subscriptions |
| Client API | wx.requestPayment | wx.requestVirtualPayment |
| Order placement | Server calls the V3 API for prepay_id | Server builds signData + dual signatures locally; the client opens checkout |
| Server APIs | /v3/pay/transactions/* | /xpay/* (Midas) |
| Callback channel | WeChat Pay callback + AES-GCM | Mini Program message push + AES-256-CBC |
| Core credentials | Merchant ID, APIv3 key, merchant certificate, public key | OfferID, AppKey, AppID / AppSecret |
| Fees and settlement | About 0.6%, T+1 | Android 1%, T+3; iOS 12%, about 45–60 days |
Prerequisites
| Item | Requirement |
|---|---|
| Mini Program | WeChat-verified and virtual payment enabled (category and entity requirements follow the WeChat Official Platform) |
| Virtual payment merchant info | OfferID and AppKey obtained from the MP console under "Virtual Payment → Basic Configuration" |
| Items | Items 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) |
| DevTools | Latest stable version; payments must be tested on a real device |
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.
| # | Credential | Description |
|---|---|---|
| 1 | offerId | Virtual payment merchant ID (Midas app ID) |
| 2 | appKey | Payment signing key used to compute paySig; server-side only |
1.2 Mini Program Basic Settings
Path: MP console → Settings → Basic Settings.
| # | Credential | Description |
|---|---|---|
| 3 | appId | Mini Program AppID |
| 4 | secret | Mini 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.
| # | Credential | Description |
|---|---|---|
| 5 | token | Message push token used for signature verification |
| 6 | msgSecret | EncodingAESKey (43 characters) used for decryption in secure mode |
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 variable | Source |
|---|---|---|
| 1 | offerId | 1.1 |
| 2 | appKey | 1.1 |
| 3 | appId | 1.2 |
| 4 | secret | 1.2 |
| 5 | token | 1.3 |
| 6 | msgSecret | 1.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:
| Field | Description |
|---|---|
| Integration name | Custom, for example xpay-demo. Determines the function name prefix and callback subdomain |
| Environment | The CloudBase environment that owns the resources |
| Credentials | offerId / appKey / appId / secret from 1.4 |
| Callback target | The cloud function that receives callbacks; without it, pushes have nowhere to go |
| Token / EncodingAESKey | token / msgSecret from 1.3, character-for-character identical to the MP console |
| Encryption / Data format | Plaintext / compatible / secure; XML / JSON. Must match the MP console |
| Event subscription | Select the xpay_* events you want to receive |
2.3 Generated Resources
Resources are provisioned asynchronously, which takes about 1–2 minutes:
| Resource | Description |
|---|---|
| HTTP cloud function | Named <integration-name>-<random-string>, for example xpay-demo-rwmx67sc |
| Environment variables | Credentials and callback URLs injected by the platform; no plaintext in code |
| Callback domain | https://<integration-id>.integration-callback.tcloudbase.com |
| Callback route | Preset by the platform; do not change the path |
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
- Open the MP console → Virtual Payment → Basic Configuration → Basic Settings → Delivery Push Configuration.
- Enter the callback URL provided by the Integration Center.
- Enter the Token and EncodingAESKey (identical to the Integration Center form).
- Select the same data format (JSON / XML) and encryption mode (plaintext / compatible / secure).
- 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
| Route | Auth | Description |
|---|---|---|
POST /xpay/create_order | Upstream gateway (requires code) | Returns payData for wx.requestVirtualPayment |
POST /xpay/query_order | Upstream gateway (requires code + x-wx-openid) | Order query; fallback delivery when pushes are lost |
POST /xpay/callback | Gateway verification; self-verified when using custom callbacks | Single entry for all xpay_* events |
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 })
},
})
callHTTPFunctiondoes 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
signDatathrough unchanged: anyJSON.parse→stringifythat 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.
goodsPriceis in cents and must match the item price published in the MP console.- Base library:
callHTTPFunctionrequires ≥ 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:
| Location | Template state | Required for production |
|---|---|---|
| Idempotency | In-process Set with a size cap | Database unique index (cold starts lose memory state) |
| Delivery | Updates in-memory order state only | Your order table plus item/coin balances |
| iOS refund inquiry | Approves refunds when no delivery is found | Persistent order lookup + real risk rules, or delivered orders may be refunded |
6.1 Callback Events
| Event | Handling |
|---|---|
xpay_goods_deliver_notify | Core: idempotent delivery |
xpay_coin_pay_notify | Update coin balance |
xpay_refund_notify | Update order + reclaim items |
xpay_complaint_notify | Record and follow up manually |
xpay_wxpay_callback_notify | Record for risk control, appeal as needed |
xpay_subscribe_ios_refund_query_notify | Answer 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
outTradeNoalone is not enough to deliver — you must know which item and how many, that isgoods.productIdandgoods.quantity.
6.3 Idempotency and Responses
- Use the platform order ID
wxOrderId(WeChatPayInfo.MchOrderNoin the push) as the idempotency key, falling back tooutTradeNo. 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
productIdshould mark the order as abnormal (retries cannot fix it); a missingquantitycan default to 1 withgoodsValid: false; an illegalquantity(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_orderand 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:
- The push URL in the WeChat console points to the Integration Center callback domain.
- The "callback target" in the Integration Center is bound to the intended cloud function.
- 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.
- 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 includesEncrypt), notsignature. This is the most common mistake. - With Integration Center hosting (
systemmode), 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.
| Variable | Source | Description |
|---|---|---|
appId | Injected | Mini Program AppID |
secret | Injected | Mini Program AppSecret |
offerId | Injected | Virtual payment merchant ID |
appKey | Injected | Payment signing key |
token | Injected | Message push token |
msgSecret | Injected | EncodingAESKey (43 characters) |
notifyUrlCallbackType | Injected | system = gateway verification; absent = self-verify |
msgEncryptType | Injected | plaintext / aes / compatible |
sandboxAppKey | Injected | Sandbox key; the template uses live mode, leave it empty |
notifyFormat | Configure yourself | json (default) / xml; must match the WeChat console |
trustBearerIdentity | Configure yourself | Whether to trust Authorization: Bearer JWTs; off by default |
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
| Topic | Link |
|---|---|
| Example project (cloud function template + mini program demo) | https://github.com/1225zhangqian/awesome-cloudbase-examples/tree/master/integration/cloudbase-virtualpay |
wx.requestVirtualPayment API | https://developers.weixin.qq.com/miniprogram/dev/api/payment/wx.requestVirtualPayment.html |
wx.cloud.callHTTPFunction | https://developers.weixin.qq.com/miniprogram/dev/wxcloudservice/wxcloud/HTTPCloudFunction.html |
| WeChat Mini Program Pay integration (physical goods) | WeChat Mini Program Payment Integration Guide |