Native (PC QR Code) Pay Integration Guide
After creating a WeChat Pay integration in the CloudBase console's Integration Center, the platform automatically generates an HTTP cloud function for you, with all capabilities — placing orders, querying orders, refunds, and callbacks — already encapsulated. This guide describes how to complete Native Pay based on that cloud function — 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.
About the function name: The cloud function generated by Integration Center is named like
<integration-name>-<random-string>(e.g.miniapp-wxpay-rwmx67sc). For ease of explanation, this document uniformly refers to it aspay-common— replace it with the actual function name in your own code.
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
The flow consists of two pipelines: outbound ordering and asynchronous callback (Native Pay needs no openid; the order returns a code_url for the frontend to render a QR code, and payment completion is detected via polling/push):
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
Native Pay shares the same callback address as JSAPI/Mini Program, but the configuration entries in the merchant platform are separate — you must configure both independently. Missing either one means you won't receive callbacks for that payment method.
Configure the Native Pay Callback URL in the Merchant Platform
The full pay callback URL = callback base domain + /wechatpay/order, for example:
https://<integration-id>.integration-callback.tcloudbase.com/wechatpay/order
Configuration path: WeChat Pay merchant platform → Product Center → My Products → Native Pay → Product Settings → Development Settings → Payment Configuration → Native Pay → Native Payment Callback URL. Click "Modify" on the right and enter the URL above.
💡 Difference from the JSAPI/Mini Program "Payment Notification URL"
- Native Pay: The "Native Payment Callback URL" highlighted in this section — only after configuration will you receive callbacks for successful Native QR code payments.
- JSAPI / Mini Program Pay: Merchant platform "Product Center → Development Settings → Payment Notification URL" — only after configuration will you receive callbacks for H5 and WeChat Mini Program payments.
The two entries are independent; configure as needed. If you're only doing Native Pay, you only need to configure the URL in this section.

Step 3 · Understand the pay-common Cloud Function
Fully reuses the source structure and env-var injection logic described in the Mini Program WeChat Pay Integration 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
Native Pay and JSAPI / Mini Program share the same callback processing logic. No need to differentiate code by payment method. See the Mini Program WeChat Pay Integration Guide for full implementation; this section only lists the 4 core handlers.
| Handler | Trigger Scenario | Principle |
|---|---|---|
handlerUnified | Synchronous order response | Return early, idempotency |
handlerUnifiedTrigger | Async payment result callback | Amount cross-check, atomic state update |
handlerRefund | Synchronous refund response | Return early, idempotency |
handlerRefundTrigger | Async refund result callback | Amount cross-check, atomic state update |
💡 Native Pay and JSAPI / Mini Program callback structures are 100% identical — both
event_type: TRANSACTION.SUCCESS, without_trade_no/transaction_id/amountetc. in plaintext. The Integration Center handles signature verification and decryption automatically, so the business side receives plaintext.
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.
The following constraints are consistent with pay-common source behavior; see GitHub pay-common.
Basic Configuration
Two variables to prepare before calling pay-common from the frontend:
| Variable | Description |
|---|---|
ENV_ID | CloudBase env ID |
FN_NAME | Auto-generated HTTP cloud function name after integration creation (like <integration-id>-<random-string>) |
Order Request Body
3 required fields (others are auto-injected by pay-common):
| Field | Type | Rule |
|---|---|---|
description | string | Product description, ≤ 127 chars |
amount.total | number | Positive integer, in cents (e.g., 0.2 yuan → 20) |
out_trade_no | string | 6–32 chars, alphanumeric/underscore/hyphen, globally unique |
Fields NOT required for Native Pay (pay-common handles them automatically):
| Field | Reason |
|---|---|
payer.openid | Only needed for JSAPI / Mini Program |
appid | pay-common auto-injects the AppID from integration config |
notify_url | pay-common auto-uses Integration Center's callback URL |
Authentication
⚠️ accessToken must come from "authenticated login"
Anonymous login (
signInAnonymously) tokens have no permission to call cloud functions; the cloud API gateway will return 401.Available login methods: email, phone, WeChat Official Account/Open Platform, CloudBase custom login, etc. See CloudBase Web SDK · Authenticated Login for full code and instructions.
Return Structure (Three-layer Nesting)
All pay-common routes follow the same return structure:
{
code, // ① Cloud API Gateway outer layer (business code)
msg,
data: {
status, // ② wechatpay-node-v3 middle layer (HTTP status)
data: { ... } // ③ WeChat Pay raw business data
}
}
code === 0 from the outer layer means success; business data is accessed at data.data. The unwrap() function in the example below encapsulates this logic.
UI Implementation Notes
- Use
code_urlas-is for QR code content (likeweixin://wxpay/bizpayurl?pr=...); do not URL-encode it - Polling frequency: Recommended 2–3 second intervals; close the order via
wxpay_close_orderafter 10 minutes without payment - Payment state is determined by the server callback; frontend polling to
SUCCESSis only for UI navigation
<!-- 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 |