Skip to main content

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-common HTTP 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:

AspectDifference 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
appidOfficial Account AppID or Mini Program AppID (just needs to be associated with the merchant ID)
Pay completion awarenessNo frontend invocation callback — must poll the query API or server-push
Runtime environmentAny 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 _action differs, and routing internally uses different ordering strategies.


Prerequisites

ItemRequirement
Official Account / Mini Program / Mobile AppAt least one is verified; its AppID will be used as the order appid
WeChat Pay Native capabilityNative Pay enabled in "Product Center" on the merchant platform (usually enabled by default after verification)
WeChat Pay merchant IDApplied for, and associated with the above AppID
Merchant super-admin permissionUsed to download the API certificate and set the APIv3 key
Frontend QR code renderingAny QR Code library (qrcode / qrcode.js / backend-generated PNG)

Step 1 · Prepare Merchant Credentials

#FieldDescription
1appIdOfficial Account / Mini Program / Mobile App AppID (associated with merchant ID)
2merchantId10-digit merchant ID
3apiV3Key32-char APIv3 key
4merchantSerialNumber40-char hex certificate serial number
5privateKeyFull content of apiclient_key.pem
6wxPayPublicKeyWeChat Pay public key PEM
7wxPayPublicKeyIdPublic Key ID (PUB_KEY_ID_...)

Under Native Pay, appId can 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.

Native shares the same set of pay-common routes as other payment methods; only the order _action differs:

_actionCategoryDescription
wxpay_order_nativeOrderNative QR code ordering (used in this guide)
wxpay_query_order_by_out_trade_noQueryQuery order by merchant order number (required for Native, used by frontend polling)
wxpay_query_order_by_transaction_idQueryQuery order by WeChat order number
wxpay_close_orderCloseClose order (recommend proactive close after 10 minutes unpaid)
wxpay_refundRefundApply for refund
wxpay_refund_queryRefundQuery refund

Biggest difference from JSAPI/Mini Program: Native ordering uses wxpay_order_native and 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, with out_trade_no / transaction_id / amount etc. 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 ID
  • FN_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), no appid (pay-common auto-injects the AppID from integration config), no notify_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_url as-is (like weixin://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_url is 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 SUCCESS is 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:

  1. Open pc-cashier.html in any desktop browser (Chrome/Safari); seeing the QR code means ordering succeeded.
  2. Scan the QR code with phone WeChat "Scan"; the payment page pops up; enter password to complete payment.
  3. The PC side should display "Payment success" and redirect to the result page within 2–4 seconds (depending on poll interval).
  4. Also check cloud function logs: tcb fn log <FN_NAME> should show handlerUnifiedTrigger - 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.

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.

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:

  1. The merchant ID does not have Native Pay capability (merchant platform "Product Center" → Native Pay → Apply)
  2. The appId is not associated with the merchant ID
  3. 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:

  1. WebSocket / SSE: After receiving the callback in handlerUnifiedTrigger, the business backend pushes to the PC session corresponding to that out_trade_no via ws/sse.
  2. 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 appId filled in Integration Center determines the appid used 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

AspectMini ProgramJSAPI WebNative (this guide)
_actionwxpay_orderwxpay_orderwxpay_order_native
Order API/v3/pay/transactions/jsapi/v3/pay/transactions/jsapi/v3/pay/transactions/native
openid required?YesYesNo
Order responseprepay_id + 5 fieldsprepay_id + 5 fieldscode_url
Invocation methodwx.requestPaymentWeixinJSBridge.invokeQR code + user scan
Pay completion awarenessFrontend success callbackJSBridge ok callbackPoll query / server push
Runtime environmentWeChat Mini ProgramWeChat built-in browserAny browser/client
Callback structureTRANSACTION.SUCCESSTRANSACTION.SUCCESSTRANSACTION.SUCCESS (identical)

Further Reading

TopicLink
Native Orderinghttps://pay.weixin.qq.com/doc/v3/merchant/4012791900
Query Orderhttps://pay.weixin.qq.com/doc/v3/merchant/4012791898
Close Orderhttps://pay.weixin.qq.com/doc/v3/merchant/4012791899
Pay Callback Protocolhttps://pay.weixin.qq.com/docs/merchant/apis/mini-program-payment/payment-notice.html
CloudBase Auth · Web SDKhttps://docs.cloudbase.net/api-reference/webv2/authentication
QRCode.jshttps://github.com/soldair/node-qrcode