Official Account JSAPI Web 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 on the WeChat Official Account web (H5 within WeChat) side, using JSAPI Pay to complete the full ordering-to-callback payment loop.
Applicable scenario: User opens an Official Account H5 page within the WeChat browser to initiate payment (most common Service Account pay). Not applicable: Browsers outside WeChat (Chrome/Safari/system browsers) → use the H5 ordering API; PC web → use Native (QR-code) Pay.
End-to-end pipeline:
- Outbound requests: Web → business backend obtains WeChat web authorization → CloudBase cloud API gateway (accessToken auth) →
pay-commonHTTP cloud function → WeChat Pay API - Asynchronous callbacks: WeChat Pay → Integration Center (verify + decrypt) →
pay-common→ business side
Architecture Overview
┌──────────────┐ ┌──────────────┐ ┌──────────────────┐
│ Off. Account │ │ Web auth │ │ CloudBase Cloud │
│ Web Page │───►│ Backend │ │ API Gateway │
│ (WeChat) │ │ code → openid│ │ accessToken auth │
│ │ └──────────────┘ │ │
│ fetch │ │ │ HTTP invoke │
│ ──────────►│ Bearer │ │ ────────────►│
│ │ Token │ │ │
│ │ ▼ │ │
│ │ ┌──────────────────────────────────────┴───┐
│ │ │ pay-common (HTTP cloud function) │
│ │ │ SDK self-sign → /v3/pay/transactions/jsapi
│ │ └────────────┬─────────────────────────────┘
│ │ │ Unified order
│ │ ▼
│ │ ┌──────────────────┐
│ │ │ WeChat Pay API │
│ │ │ (JSAPI/MiniProg)│
│ │ └────────┬─────────┘
│ WeixinJSBridge.invoke │ Async pay callback
│ ('getBrandWCPayRequest') ▼
│ │ ┌──────────────────┐
│ │ │ Integration │
└──────────────┘ │ Center │
│ (verify+decrypt) │
└────────┬─────────┘
│ Plaintext forward
▼
┌──────────────────┐
│ pay-common │
│ /wechatpay/order │
└──────────────────┘
Key differences between JSAPI and Mini Program payment:
| Aspect | Mini Program Pay | Official Account JSAPI Web Pay |
|---|---|---|
| Order API | /v3/pay/transactions/jsapi | /v3/pay/transactions/jsapi (same one) |
appid | Mini Program AppID | Official Account AppID |
| openid source | signInWithOpenId() directly | WeChat web authorization (OAuth 2.0) code → openid |
| Pay invocation API | wx.requestPayment | WeixinJSBridge.invoke('getBrandWCPayRequest', ...) |
| Runtime environment | WeChat Mini Program container | WeChat built-in browser (required) |
Therefore the pay-common function itself is identical; the differences are only in "openid acquisition" and "frontend invocation API".
Responsibility breakdown:
| Role | Responsibility |
|---|---|
| Official Account web | Obtains code via WeChat web authorization → business backend exchanges openid; calls pay-common with Bearer Token; invokes payment via WeixinJSBridge.invoke |
| Business backend (self-provided) | Holds the Official Account AppSecret; calls sns/oauth2/access_token to exchange code for openid (AppSecret must not appear in the frontend) |
| Cloud API gateway | Validates Bearer {accessToken}; forwards requests to the HTTP cloud function |
| pay-common cloud function | Handles ordering, querying, and refunding; receives and handles callbacks |
| WeChat Pay API | The party that actually executes payment business |
| Integration Center | Hosts credentials uniformly; performs signature verification and decryption on callbacks, then forwards plaintext to the cloud function |
Prerequisites
| Item | Requirement |
|---|---|
| Official Account | Service Account (Subscription Accounts do not support JSAPI Pay) WeChat-verified |
| Official Account JSAPI Pay capability | Enabled and approved in "WeChat Pay" on the Official Accounts platform |
| WeChat Pay merchant ID | Applied for, and bound to the above Official Account on the merchant platform |
| Official Account pay authorization directory | Merchant platform → Product Center → JSAPI Pay → Add authorization directory (must match the URL prefix of the ordering page, including trailing /) |
| Official Account web authorization domain | Official Accounts platform → Settings & Development → Official Account Settings → Functional Settings → Web Authorization Domain |
| Merchant super-admin permission | Used to download the API certificate and set the APIv3 key |
| Business backend (any) | A lightweight service for exchanging openid (existing Node/Java/Go service, or a standalone cloud function) |
Step 1 · Prepare Merchant Credentials
You need 7 credentials:
| # | Field | Description |
|---|---|---|
| 1 | appId | Official Account AppID (not Mini Program AppID) |
| 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_...) |
⚠️
appIdmust be the Official Account AppID, and that AppID must be bound to the merchant ID under "JSAPI Pay" on the merchant platform. See the AppID distinctions in the appendix of the Mini Program guide.
Step 2 · Create the Integration in CloudBase
Same as 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 merchant platform's "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. The /v3/pay/transactions/jsapi order implementation in pay-common is the same code for Mini Programs and Official Account web (route _action: wxpay_order).
The only difference: the payer.openid passed by the caller must be the openid of a user under that Official Account AppID.
Step 4 · Integrate with the Official Account Web Page
After pay-common is deployed, integrating Official Account web has four steps: web authorization to get openid → call pay-common to place order → invoke payment via JSBridge → server handles callback.
4.1 Configure Official Account Pay Authorization Directory and Web Authorization Domain
Pay authorization directory (required):
Path: Merchant platform → Product Center → JSAPI Pay → Add authorization directory.
Fill in the URL prefix of the page that invokes payment, must end with /. For example, if the page is https://shop.example.com/pay/cashier.html, fill the authorization directory as https://shop.example.com/pay/.
Web authorization domain (required):
Path: Official Accounts platform → Settings & Development → Official Account Settings → Functional Settings → Web Authorization Domain.
4.2 Get openid via WeChat Web Authorization
Official Account JSAPI Pay requires payer.openid, which must be obtained via WeChat web authorization (OAuth 2.0). The exchange step cannot be done in the frontend — it would leak the Official Account AppSecret. Below are two implementation approaches:
Recommended: Use the WeChat Official Account Integration to Get openid
The "WeChat Official Account" integration provided by the CloudBase platform (the offiaccount-common cloud function) has packaged the web authorization pipeline. The business side does not need to build a backend or store the AppSecret.
For detailed onboarding steps, see Official Account Integration Guide · Web Authorization.
✅ Advantages: AppSecret stays in the offiaccount-common cloud function env vars and never leaves Integration Center; capabilities like web authorization, user profile retrieval, and token refresh are out-of-the-box.
Alternative: Build Your Own Backend to Exchange openid
If the business side already has its own backend service and does not want to introduce the Official Account integration, you can call the WeChat web authorization API directly. Important: the openid exchange must happen on the backend; the frontend only handles redirects and callbacks.
1. Web redirect: https://open.weixin.qq.com/connect/oauth2/authorize
?appid=<Official Account AppID>
&redirect_uri=<URL-encoded business callback URL>
&response_type=code
&scope=snsapi_base ← Silent authorization, only gets openid; no user confirmation
&state=<custom>
#wechat_redirect
2. WeChat redirects back to redirect_uri with ?code=xxx&state=xxx
3. **Business backend** uses code to call WeChat API to get openid (do not call from frontend; would leak AppSecret):
GET https://api.weixin.qq.com/sns/oauth2/access_token
?appid=<Official Account AppID>
&secret=<Official Account AppSecret>
&code=<code from previous step>
&grant_type=authorization_code
Returns: { access_token, openid, refresh_token, expires_in, scope, ... }
4. Backend writes openid into session/cookie or returns it to the frontend
scope=snsapi_basedoes not require a user confirmation popup; as long as the user opens it within an Official Account session, openid is obtained the moment they redirect — best UX. If the business also needs nickname/avatar etc., usescope=snsapi_userinfo, which adds an authorization popup.
4.3 Payment-related Routes
JSAPI Web Pay and Mini Program Pay use the same set of routes; the only difference is the client-side invocation method. Common routes:
_action | Category | Description |
|---|---|---|
wxpay_order | Order | JSAPI / Mini Program ordering (used in this guide) |
wxpay_query_order_by_out_trade_no | Query | Query order by merchant order number |
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 |
All routes are dispatched by the _action field in the body. Except for callback routes, all must carry Authorization: Bearer {access_token}.
4.4 Callback Handling
Fully reuses the hook methods (handlerUnified / handlerUnifiedTrigger / handlerRefund / handlerRefundTrigger) in services/orderService.js described in the Mini Program guide; same principles apply: idempotency, amount cross-check, atomic state update.
JSAPI Pay and Mini Program Pay 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.5 Complete Invocation Example
Suitable for native H5 self-built projects. Below is the end-to-end example from getting openid to invoking payment.
Key constraints:
ENV_ID: CloudBase env IDFN_NAME: the auto-generated HTTP cloud function name after integration creation (like<integration-id>-<random-string>)- The page must be opened in the WeChat built-in browser; otherwise
WeixinJSBridgedoes not exist - The
payer.openidmust contain an openid under the Official Account AppID - 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, custom login, etc.), see CloudBase Web SDK · Authenticated Login.
<!-- pay.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Official Account Pay</title>
</head>
<body>
<button id="payBtn">WeChat Pay 0.2 yuan</button>
<script src="https://cdn.jsdelivr.net/npm/@cloudbase/js-sdk/dist/cloudbase.full.js"></script>
<script>
const ENV_ID = 'your-env-id'
const FN_NAME = 'pay-common'
// openid of the current user (Official Account AppID dimension), obtained via business backend
const OPENID = window.__USER_OPENID__ // injected by backend template or cookie
// 1. Initialize CloudBase Web SDK, get accessToken
const app = cloudbase.init({ env: ENV_ID })
const auth = app.auth({ persistence: 'local' })
async function getAccessToken() {
// ⚠️ Anonymous login (signInAnonymously) tokens cannot call cloud functions (401)
// Authenticated login is required. For login methods (email, phone, custom login, etc.), see:
// https://docs.cloudbase.net/api-reference/webv2/authentication#%E8%AE%A4%E8%AF%81%E7%99%BB%E5%BD%95
throw new Error('Implement authenticated login per CloudBase Web SDK docs and return accessToken')
}
document.getElementById('payBtn').onclick = async () => {
const accessToken = await getAccessToken()
const out_trade_no = 'ORDER_' + Date.now()
// 2. Call pay-common to place order
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 ${accessToken}`,
},
body: JSON.stringify({
_action: 'wxpay_order',
description: 'Test product',
out_trade_no,
amount: { total: 20, currency: 'CNY' }, // 0.2 yuan
payer: { openid: OPENID },
}),
}
)
const body = await res.json()
if (body.code !== 0) {
alert(body.msg || 'Order failed')
return
}
const p = body.data && body.data.data ? body.data.data : body.data
// p = { timeStamp, nonceStr, package, signType, paySign }
// 3. Invoke JSBridge payment
callJsBridgePay(p, out_trade_no)
}
function callJsBridgePay(p, outTradeNo) {
function onBridgeReady() {
WeixinJSBridge.invoke(
'getBrandWCPayRequest',
{
appId: '<Official Account AppID>', // must match the appId used at order time
timeStamp: p.timeStamp,
nonceStr: p.nonceStr,
package: p.package, // 'prepay_id=wx20...'
signType: p.signType || 'RSA',
paySign: p.paySign,
},
function (resp) {
if (resp.err_msg === 'get_brand_wcpay_request:ok') {
// Pay success (frontend only updates UI; final state determined by server callback)
// Recommended: 1-2 seconds later, call wxpay_query_order_by_out_trade_no for fallback
location.href = `/order/result?out_trade_no=${outTradeNo}`
} else if (resp.err_msg === 'get_brand_wcpay_request:cancel') {
alert('User canceled payment')
} else {
alert('Payment failed: ' + resp.err_msg)
}
}
)
}
if (typeof WeixinJSBridge === 'undefined') {
document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false)
} else {
onBridgeReady()
}
}
</script>
</body>
</html>
Implementation notes:
WeixinJSBridgeonly exists in the WeChat built-in browser; external browsers must switch to the H5 Pay scheme (out of scope for this guide).- The
appIdfield in thegetBrandWCPayRequestinput must exactly match the Official Account AppID used at order time, otherwise it will report "appid and openid mismatch". - Use the
packagevalue from the API response as-is; do not concatenateprepay_id=...yourself. - The final payment state is determined by the server callback; the frontend
okis only for redirecting to the result page; the server executes business side effects like shipping after receiving the callback.
4.6 Real-device Test
The debugging pipeline for JSAPI Pay is more cumbersome than for Mini Programs:
- Must visit the page in the WeChat built-in browser; desktop browsers cannot invoke
WeixinJSBridge. - WeChat Developer Tools has an "Official Account Web Debug" mode (menu → Official Account Web Project → WeChat Web Debug) that simulates the WeChat UA, but still cannot initiate real payment — must use a real device.
- Before accessing the page on a real device, confirm:
- Domain is in the Official Account "Web Authorization Domain" allowlist
- Page URL prefix is in the merchant platform "JSAPI Pay Authorization Directory" (with trailing
/) - User Official Account follow status doesn't matter (
snsapi_basedoes not require following)
Test amount recommendation: same as Mini Program — use 0.1–1 yuan to avoid 0.01 yuan triggering risk control.
FAQ
Q1: Invoking payment reports "appid and openid mismatch"
The most common error. The appId in the getBrandWCPayRequest input, the appId filled at order time, and the AppID used to exchange openid must be identical at all three places and all be the Official Account AppID.
Troubleshoot:
- Integration Center is filled with the Official Account AppID, not the Mini Program AppID
- The business backend's OAuth2 also uses the same Official Account AppID + AppSecret
- The frontend's
getBrandWCPayRequestinput does not hard-code the wrong appId
Q2: Reports APPID_MCHID_NOT_MATCH or "merchant does not exist"
The Official Account AppID is not bound to the merchant ID on the merchant platform. Merchant platform → Product Center → JSAPI Pay → Associate AppID; submit the application; takes effect after the Official Account admin approves.
Q3: Reports "Current URL is not in pay authorization directory"
The page URL invoking payment is not in the "JSAPI Pay Authorization Directory" allowlist. Merchant platform → Product Center → JSAPI Pay → Add authorization directory; fill in the URL prefix of the page (with trailing /). Takes effect in about 5 minutes.
Q4: No code received after web authorization redirect
The redirect_uri domain is not in the Official Account "Web Authorization Domain" allowlist, or the domain verification file is not online. Complete the web authorization domain verification and retry.
Q5: WeixinJSBridge is not defined
The page is opened in a non-WeChat built-in browser. JSAPI Pay can only run in the WeChat browser; external browsers must use H5 Pay (/v3/pay/transactions/h5).
Q6: Can desktop Chrome with modified UA spoofing WeChat invoke payment?
No. WeixinJSBridge is a global object natively injected by WeChat; UA spoofing cannot inject it. During development, you can use WeChat Developer Tools' "Official Account Web Project" mode to debug page rendering and requests; but pay invocation must use a real device.
Q7: Do you need wx.config or JSSDK auth?
JSAPI Pay (getBrandWCPayRequest) does not require wx.config. Only when using WeChat JSSDK capabilities like camera, location, sharing etc. is wx.config needed.
Q8: Can the same integration as Mini Program Pay be reused?
Yes, but it depends on your appId configuration. The pay-common integration only has one appId field; whichever is filled determines which app it serves. To support both Mini Program and Official Account JSAPI, you can either:
- Create two independent integrations (one filled with Mini Program AppID, the other with Official Account AppID) to serve the two ends respectively
- Modify the pay-common code to dynamically switch
appIdby caller, and add env vars likejsapiAppId
Appendix · Differences from Mini Program Pay Quick Reference
| Aspect | Mini Program | Official Account JSAPI Web |
|---|---|---|
Integration Center appId | Mini Program AppID | Official Account AppID |
| openid source | signInWithOpenId() | OAuth 2.0 web authorization + business backend exchange |
| Pay invocation API | wx.requestPayment | WeixinJSBridge.invoke('getBrandWCPayRequest') |
| Runtime environment | Mini Program container | WeChat built-in browser |
| Domain allowlist | Mini Program request legal domain | Official Account web authorization domain + pay authorization directory |
| pay-common route | _action: wxpay_order | _action: wxpay_order (same one) |
| Callback structure | TRANSACTION.SUCCESS | TRANSACTION.SUCCESS (identical) |
Further Reading
| Topic | Link |
|---|---|
| JSAPI Ordering (Mini Program/Official Account Pay) | https://pay.weixin.qq.com/doc/v3/merchant/4012791897 |
| Official Account Web Authorization | https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_webpage_authorization.html |
getBrandWCPayRequest API | https://pay.weixin.qq.com/doc/v3/merchant/4013080345 |
| 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 |