Native (PC QR Code) Pay Integration Guide
After creating a "WeChat Pay" integration in the CloudBase console, the system automatically generates an HTTP cloud function (function name like <integration-name>-<random-string>, hereinafter referred to as pay-common). This guide describes how to use that cloud function to complete Native Pay — i.e., the integration loop where a PC web page / desktop app shows a QR code and the user scans it with their phone WeChat to complete payment.
Applicable scenarios: PC checkout, desktop client, self-service terminals, ad screens — all "non-WeChat-browser" QR code payment needs. Not applicable: H5 inside WeChat → use JSAPI web pay; Mini Program → use Mini Program pay; mobile browser outside WeChat → use H5 pay.
End-to-end pipeline:
- Outbound requests: Web → CloudBase cloud API gateway (accessToken auth) →
pay-commonHTTP cloud function → WeChat Pay API (/v3/pay/transactions/native) - Asynchronous callbacks: WeChat Pay → Integration Center (verify + decrypt) →
pay-common→ business side - State sync: Web polls the query API or frontend WebSocket/SSE pushes; redirects to the result page upon detecting payment completion
Architecture Overview
┌──────────────┐ ┌──────────────────┐ ┌────────────────────┐
│ PC Checkout │ │ CloudBase Cloud │ │ pay-common │
│ Web/Client │ │ API Gateway │ HTTP │ (HTTP cloud fn) │
│ │ Bearer │ accessToken │ invoke │ SDK self-sign │
│ fetch ─────►│ Token │ auth │─────────►│ /v3/pay/transactions/native
│ │ └──────────────────┘ └─────────┬──────────┘
│ │ │ Unified order
│ │ ▼
│ │ ┌────────────────────────┐
│ │ ◄──── Returns code_url ──────────── │ WeChat Pay API │
│ │ │ api.mch.weixin.qq.com │
│ Render QR │ └──────────┬─────────────┘
│ (qrcode.js) │ │ Async pay callback
│ │ ┌── User scans with phone WeChat ──┐ ▼
│ │ │ │ ┌────────────────────────┐
│ Poll query │ ▼ │ │ Integration Center │
│ or push state│ Phone WeChat completes payment │ │ (verify + decrypt) │
│ │ └─────────────────────────────────────┘ └──────────┬─────────────┘
│ │ │ Plaintext forward
│ │ ▼
│ │ ┌────────────────────────┐
│ │ │ pay-common │
│ │ │ /wechatpay/order │
│ │ │ → orderService │
└──────────────┘ └────────────────────────┘
Key characteristics of Native Pay:
| Aspect | Difference from JSAPI / Mini Program |
|---|---|
| Order API | /v3/pay/transactions/native (different endpoint) |
| Order response | { code_url: "weixin://wxpay/bizpayurl?pr=..." } — frontend must generate the QR code |
| openid required? | Not required, no payer.openid field |
appid | Official Account AppID or Mini Program AppID (just needs to be associated with the merchant ID) |
| Pay completion awareness | No frontend invocation callback — must poll the query API or server-push |
| Runtime environment | Any browser/client; no WeChat container dependency |
The server-side logic of Native Pay shares the same pay-common cloud function template with Mini Program / JSAPI; only the
_actiondiffers, and routing internally uses different ordering strategies.
Prerequisites
| Item | Requirement |
|---|---|
| Official Account / Mini Program / Mobile App | At least one is verified; its AppID will be used as the order appid |
| WeChat Pay Native capability | Native Pay enabled in "Product Center" on the merchant platform (usually enabled by default after verification) |
| WeChat Pay merchant ID | Applied for, and associated with the above AppID |
| Merchant super-admin permission | Used to download the API certificate and set the APIv3 key |
| Frontend QR code rendering | Any QR Code library (qrcode / qrcode.js / backend-generated PNG) |
Step 1 · Prepare Merchant Credentials
| # | Field | Description |
|---|---|---|
| 1 | appId | Official Account / Mini Program / Mobile App AppID (associated with merchant ID) |
| 2 | merchantId | 10-digit merchant ID |
| 3 | apiV3Key | 32-char APIv3 key |
| 4 | merchantSerialNumber | 40-char hex certificate serial number |
| 5 | privateKey | Full content of apiclient_key.pem |
| 6 | wxPayPublicKey | WeChat Pay public key PEM |
| 7 | wxPayPublicKeyId | Public Key ID (PUB_KEY_ID_...) |
Under Native Pay,
appIdcan be an Official Account or Mini Program AppID (or even an Open Platform Mobile App AppID), as long as it is associated with the merchant ID under the merchant platform's "Native Pay" product.
Step 2 · Create the Integration in CloudBase
Identical to the Mini Program Pay guide. After integration creation, the system automatically generates:
- HTTP cloud function
pay-common - Callback base domain
https://<integration-id>.integration-callback.tcloudbase.com - Pay callback path
/wechatpay/order - Refund callback path
/wechatpay/refund
The full pay callback URL must be filled in the WeChat Pay merchant platform → Product Center → Development Settings → Pay Notification URL.
Step 3 · Understand the Cloud Function and Environment Variables
Fully reuses the source structure and env-var injection logic described in the Mini Program guide. Native ordering is already implemented in pay-common, corresponding to _action: wxpay_order_native.
Step 4 · Integrate with the PC Checkout
After pay-common is deployed, PC-side integration has four steps: call pay-common to get code_url → frontend renders QR code → poll query API → redirect to result page.
4.1 Payment-related Routes
Native shares the same set of pay-common routes as other payment methods; only the order _action differs:
_action | Category | Description |
|---|---|---|
wxpay_order_native | Order | Native QR code ordering (used in this guide) |
wxpay_query_order_by_out_trade_no | Query | Query order by merchant order number (required for Native, used by frontend polling) |
wxpay_query_order_by_transaction_id | Query | Query order by WeChat order number |
wxpay_close_order | Close | Close order (recommend proactive close after 10 minutes unpaid) |
wxpay_refund | Refund | Apply for refund |
wxpay_refund_query | Refund | Query refund |
Biggest difference from JSAPI/Mini Program: Native ordering uses
wxpay_order_nativeand returns{ code_url }instead of{ timeStamp, paySign, ... }.
All routes are dispatched by the _action field in the body. Except for callback routes, all must carry Authorization: Bearer {access_token}.
4.2 Callback Handling
Fully reuses the methods (handlerUnified / handlerUnifiedTrigger / handlerRefund / handlerRefundTrigger) in services/orderService.js described in the Mini Program guide; same principles apply: return early, idempotency, amount cross-check, atomic state update.
Native Pay and JSAPI/Mini Program have 100% identical callback structures — both
event_type: TRANSACTION.SUCCESS, without_trade_no/transaction_id/amountetc. in plaintext. The business side does not need to differentiate callback code by payment method.
4.3 Complete Invocation Example
Suitable for PC web scenarios. Below is an end-to-end example from ordering to displaying the QR code and polling the query API.
Key constraints (consistent with pay-common source behavior; see GitHub pay-common):
ENV_ID: CloudBase env IDFN_NAME: the auto-generated HTTP cloud function name after integration creation (like<integration-id>-<random-string>)- The order request body has 3 required fields:
description(product description, ≤ 127 chars),amount.total(positive integer, in cents),out_trade_no(6–32 chars alphanumeric/underscore/hyphen, globally unique) - No
payer.openid(only JSAPI/Mini Program need it), noappid(pay-common auto-injects the AppID from integration config), nonotify_url(pay-common auto-uses Integration Center's callback URL) - accessToken must come from "authenticated login" — anonymous login (
signInAnonymously) tokens have no permission to call cloud functions; the gateway will return 401. For login methods (email, phone, WeChat Official Account/Open Platform, CloudBase custom login, etc.), see CloudBase Web SDK · Authenticated Login. - Three-layer nested return structure:
{ code, msg, data: { status, data: { code_url } } }— outer{ code, msg, data }from cloud API gateway, middle{ status, data }from wechatpay-node-v3, innermost is WeChat's{ code_url } - The QR code content uses
code_urlas-is (likeweixin://wxpay/bizpayurl?pr=...); do not URL-encode it - Payment state is determined by the server callback; frontend polling is only for UI hints
<!-- pc-cashier.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Scan to Pay</title>
<style>
.cashier { text-align: center; padding: 40px; }
#qrcode { margin: 24px auto; }
.status { color: #888; font-size: 14px; margin-top: 12px; }
</style>
</head>
<body>
<div class="cashier">
<h2>Please scan the QR code with WeChat to pay</h2>
<h3>Amount: <span id="amount">¥ 0.20</span></h3>
<div id="qrcode"></div>
<div class="status" id="status">Waiting for scan…</div>
</div>
<script src="https://cdn.bootcdn.net/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
<script src="https://static.cloudbase.net/cloudbase-js-sdk/latest/cloudbase.full.js"></script>
<script>
const ENV_ID = 'your-env-id'
const FN_NAME = 'pay-common' // actual function name generated after integration creation
const POLL_INTERVAL = 2000 // poll every 2s
const POLL_TIMEOUT = 600000 // stop polling and close order after 10 min
const app = cloudbase.init({ env: ENV_ID })
const auth = app.auth({ persistence: 'local' })
// Get accessToken on Web side
// ⚠️ accessToken from anonymous login (signInAnonymously) cannot call cloud functions via the gateway;
// the cloud function side will return 401. "Authenticated login" must be used to obtain accessToken.
// For optional login methods (email, phone, WeChat Official Account/Open Platform, CloudBase custom login, etc.) and full code,
// see CloudBase Web SDK docs:
// https://docs.cloudbase.net/api-reference/webv2/authentication#%E8%AE%A4%E8%AF%81%E7%99%BB%E5%BD%95
async function getAccessToken() {
// Example: use email + verification code login to get an accessToken that can access the cloud function
// const loginState = await auth.signIn({
// username: 'user@example.com',
// verification_code: '...',
// verification_token: '...',
// })
// return loginState.credential.accessToken
throw new Error('Implement authenticated login per CloudBase Web SDK docs and return accessToken')
}
// Unified wrapper: call any pay-common route
async function callPayCommon(token, payload) {
const res = await fetch(
`https://${ENV_ID}.api.tcloudbasegateway.com/v1/functions/${FN_NAME}?webfn=true`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify(payload),
}
)
return res.json()
}
// Unwrap business data from the pay-common return structure
// pay-common returns: { code: 0, msg: 'success', data: { status: 200, data: <business> } }
function unwrap(body) {
if (!body || body.code !== 0) return null
const inner = body.data
return inner && inner.data ? inner.data : inner
}
;(async () => {
const token = await getAccessToken()
const out_trade_no = 'ORDER_' + Date.now()
// 1. Native order (no openid / appid / notify_url needed)
const orderRes = await callPayCommon(token, {
_action: 'wxpay_order_native',
description: 'Test product',
out_trade_no,
amount: { total: 20, currency: 'CNY' }, // 0.2 yuan
})
const order = unwrap(orderRes)
if (!order || !order.code_url) {
document.getElementById('status').textContent = 'Order failed: ' + (orderRes.msg || '')
return
}
// order.code_url like 'weixin://wxpay/bizpayurl?pr=...'
// 2. Render QR code (use code_url as QR content as-is; do not URL-encode)
QRCode.toCanvas(document.getElementById('qrcode'), order.code_url, { width: 240 })
// 3. Poll query API
const startedAt = Date.now()
const timer = setInterval(async () => {
// Timeout: close the order
if (Date.now() - startedAt > POLL_TIMEOUT) {
clearInterval(timer)
await callPayCommon(token, {
_action: 'wxpay_close_order',
out_trade_no,
})
document.getElementById('status').textContent = 'Order timed out and closed'
return
}
const queryRes = await callPayCommon(token, {
_action: 'wxpay_query_order_by_out_trade_no',
out_trade_no,
})
const detail = unwrap(queryRes)
const state = detail && detail.trade_state
if (state === 'SUCCESS') {
clearInterval(timer)
document.getElementById('status').textContent = 'Payment success, redirecting…'
location.href = `/order/result?out_trade_no=${out_trade_no}`
} else if (state === 'CLOSED' || state === 'PAYERROR' || state === 'REVOKED') {
clearInterval(timer)
document.getElementById('status').textContent = 'Order ' + state
} else if (state === 'USERPAYING') {
document.getElementById('status').textContent = 'User paying…'
}
// NOTPAY: keep waiting
}, POLL_INTERVAL)
})()
</script>
</body>
</html>
Implementation notes:
code_urlis used directly as the QR content — do not URL-encode it, do not embed an HTTPS link; after scanning, WeChat will jump to the payment page itself.- Three-layer unwrap: The
unwrap()function in the example wraps the fixed three-layer structure — the cloud API gateway's{ code, msg, data }→ wechatpay-node-v3's{ status, data }→ WeChat's raw business data. All pay-common route returns follow this structure. - Polling the query API is the conventional awareness mechanism for Native Pay; recommend every 2–3 seconds; if 10 minutes have passed without payment, the frontend should proactively call
wxpay_close_order. - Better UX: After receiving the callback in
handlerUnifiedTrigger, the server actively pushes the result via WebSocket / SSE to the PC session corresponding to that user, eliminating polling. Not demonstrated in this guide for simplicity. - Final business side effects like shipping are determined by the server callback; the frontend polling reaching
SUCCESSis only for redirecting UI.
Multi-AppID Scenarios
pay-common supports dynamically switching appid via the useServiceAccount parameter (requires additional service_app_id env var configured in the cloud function). Typical scenario: the integration default is the Mini Program AppID, but some Native scenarios want to use the Service Account AppID for ordering:
// Pass useServiceAccount: true at order time
await callPayCommon(token, {
_action: 'wxpay_order_native',
useServiceAccount: true, // use service_app_id for ordering
description: 'Test product',
out_trade_no,
amount: { total: 20, currency: 'CNY' },
})
If service_app_id env var is not configured, pay-common will directly error out with useServiceAccount=true but service_app_id env var is not configured.
4.4 Real-device Test
Native Pay is simpler to test than JSAPI/Mini Program:
- Open
pc-cashier.htmlin any desktop browser (Chrome/Safari); seeing the QR code means ordering succeeded. - Scan the QR code with phone WeChat "Scan"; the payment page pops up; enter password to complete payment.
- The PC side should display "Payment success" and redirect to the result page within 2–4 seconds (depending on poll interval).
- Also check cloud function logs:
tcb fn log <FN_NAME>should showhandlerUnifiedTrigger - payment result: ... SUCCESS, indicating the callback pipeline also succeeded.
Test amount recommendation: same as Mini Program — use 0.1–1 yuan to avoid 0.01 yuan triggering risk control; same-account daily QR scan limits also apply.
FAQ
Q1: The returned code_url starts with weixin:// — can I put it directly in img src?
No. weixin:// is a private schema, not an HTTPS URL. You must use a frontend QR Code library (qrcode.js / canvas) to draw the string into a QR code image.
Q2: After scanning, WeChat shows "This link cannot be accessed"
Usually the code_url was URL-encoded or wrapped in a redirect link. The code_url must be used as-is as the QR content; not even leading/trailing whitespace removal.
Q3: After scanning, WeChat shows "Payment link expired"
code_url expires by default after 2 hours; if the user takes too long to scan, you need to re-order to generate a new link. The frontend should also provide a "Refresh QR Code" button.
Q4: After scanning, nothing happens; no payment page
Possible reasons:
- The merchant ID does not have Native Pay capability (merchant platform "Product Center" → Native Pay → Apply)
- The
appIdis not associated with the merchant ID - The QR code was generated with wrong encoding (e.g. base64), corrupting the content
Q5: PC web polling reports 401 / token expired
The accessToken obtained by CloudBase Web SDK expires after 2 hours by default. Simple approach: get the token before each call; better approach: listen for it in auth.onLoginStateChanged and refresh.
Q6: Can I avoid polling and rely solely on server callbacks to notify the frontend?
Yes — common approaches:
- WebSocket / SSE: After receiving the callback in
handlerUnifiedTrigger, the business backend pushes to the PC session corresponding to thatout_trade_novia ws/sse. - CloudBase realtime database: The server writes the order status to the cloud database; the PC frontend uses the realtime SDK
db.collection('orders').doc(id).watch()to listen for changes.
Both eliminate frontend polling and provide the best UX. Polling is still recommended as a fallback.
Q7: Can the same Native integration also support Mini Program payment?
Yes. In the pay-common template, Native and JSAPI/Mini Program ordering use the same SDK; only the route (_action) differs. Note:
- The
appIdfilled in Integration Center determines theappidused at order time. If that AppID has both Native and JSAPI enabled on the merchant platform, pay-common can support both at the same time. - If you want to use one AppID for Native (e.g. Open Platform Mobile App AppID) and another for Mini Program, you need two independent integrations or modify the pay-common code to switch dynamically.
Q8: How do Native Pay callbacks differ from Mini Program Pay?
No difference. WeChat Pay V3 callbacks have the same structure TRANSACTION.SUCCESS for all payment methods (JSAPI / Native / H5 / APP), and verification and decryption mechanisms are identical. Integration Center has done verification and decryption; the plaintext format received by pay-common's handlerUnifiedTrigger is independent of the ordering method.
Appendix · Differences Quick Reference vs Other Pay Methods
| Aspect | Mini Program | JSAPI Web | Native (this guide) |
|---|---|---|---|
_action | wxpay_order | wxpay_order | wxpay_order_native |
| Order API | /v3/pay/transactions/jsapi | /v3/pay/transactions/jsapi | /v3/pay/transactions/native |
| openid required? | Yes | Yes | No |
| Order response | prepay_id + 5 fields | prepay_id + 5 fields | code_url |
| Invocation method | wx.requestPayment | WeixinJSBridge.invoke | QR code + user scan |
| Pay completion awareness | Frontend success callback | JSBridge ok callback | Poll query / server push |
| Runtime environment | WeChat Mini Program | WeChat built-in browser | Any browser/client |
| Callback structure | TRANSACTION.SUCCESS | TRANSACTION.SUCCESS | TRANSACTION.SUCCESS (identical) |
Further Reading
| Topic | Link |
|---|---|
| Native Ordering | https://pay.weixin.qq.com/doc/v3/merchant/4012791900 |
| Query Order | https://pay.weixin.qq.com/doc/v3/merchant/4012791898 |
| Close Order | https://pay.weixin.qq.com/doc/v3/merchant/4012791899 |
| Pay Callback Protocol | https://pay.weixin.qq.com/docs/merchant/apis/mini-program-payment/payment-notice.html |
| CloudBase Auth · Web SDK | https://docs.cloudbase.net/api-reference/webv2/authentication |
| QRCode.js | https://github.com/soldair/node-qrcode |