WeChat Pay Integration Walkthrough: From Order Creation to Callback Confirmation

This article walks through a production‑grade WeChat Pay V2 integration, detailing the multi‑layer architecture, required configuration, unified order parameters, thread‑safe service usage, signature generation, and a four‑step callback verification process—including signature check, idempotency, amount validation, and active order query—to ensure reliable payment confirmation.

samdeepthink
samdeepthink
samdeepthink
WeChat Pay Integration Walkthrough: From Order Creation to Callback Confirmation

System Overview

The production payment system is split into four logical layers (five microservices): business layer (order system), payment orchestration layer (payment center), channel layer (WeChat channel service), and data layer (payment data service). The same call chain and protection logic apply to monolithic deployments.

Preparation

Merchant ID (mch_id) and corresponding appid from the WeChat merchant platform.

API key (32‑character secret) used for signing and verification – distinct from appSecret and V3 APIv3 key.

Callback URL – a publicly reachable HTTPS endpoint without parameters or redirects.

SDK – use WxJava to handle XML serialization, signing, and verification. Example Maven dependency:

<dependency>
    <groupId>com.github.binarywang</groupId>
    <artifactId>weixin-java-pay</artifactId>
    <version>3.8.0</version>
</dependency>

Unified Order

Six Core Parameters

tradeType – transaction type (JSAPI for Mini‑Program/Public Account, APP for native app, MWEB for H5). Wrong value triggers an error.

body – product description. Keep it simple; avoid marketing copy.

outTradeNo – merchant order number, limited to 32 characters and unique per merchant. In this system the value is a payment‑center‑generated payment order number, not the business order ID, to avoid duplicate‑order rejections.

totalFee – total amount in **cents** (not yuan). Mis‑unit conversion leads to financial loss.

notifyUrl – the callback URL; must be a plain, publicly reachable address.

openid – user identifier; required for JSAPI, sub_openid for service‑provider mode.

Amount conversion from yuan to cents is performed with

int totalFee = amount.multiply(BigDecimal.valueOf(100)).intValue();

to avoid double‑precision errors.

Multi‑Merchant Configuration Selection

A helper method picks the correct WxPayConfig based on request characteristics, supporting normal merchants, service‑provider mode, and APP payments. Example:

public WxPayConfig pickConfig(PayProperties properties) {
    WxPayConfig config = new WxPayConfig();
    if (paymentConfig != null) {
        config.setAppId(paymentConfig.getAppId());
        config.setMchId(paymentConfig.getMchId());
        config.setMchKey(paymentConfig.getKey());
        return config;
    }
    if (isSub()) {
        config.setAppId(properties.getSpAppId());
        config.setMchId(properties.getSpMchId());
        config.setMchKey(properties.getSpKey());
        config.setSubAppId(properties.getSubAppId());
        config.setSubMchId(properties.getSubMchId());
        return config;
    }
    config.setAppId(properties.getAppId());
    config.setMchId(properties.getMchId());
    config.setMchKey(properties.getKey());
    return config;
}

Thread‑Safe Service Usage

Each request creates a new WxPayServiceImpl instance and sets the selected config; the service must not be a singleton because its internal config is mutable and would cause cross‑merchant signature errors under concurrency.

// WxPayService must not be a global singleton; config is per‑request
WxPayService wxPayService = new WxPayServiceImpl();
wxPayService.setConfig(request.pickConfig(payProperties));
WxPayUnifiedOrderResult result = wxPayService.unifiedOrder(request.toWxPayUnifiedOrderRequest());

After a successful order, the system stores the returned prepay_id together with a snapshot of the merchant configuration to be used later during callback verification.

Second Signature for Front‑End

The front‑end cannot receive the raw prepay_id directly; it needs six parameters signed with the merchant key. WxJava builds the result object and then generates the paySign:

WxPayMpOrderResult payResult = WxPayMpOrderResult.builder()
        .appId(appId)
        .timeStamp(timestamp)
        .nonceStr(nonceStr)
        .packageValue("prepay_id=" + prepayId)
        .signType("MD5")
        .build();
payResult.setPaySign(SignUtils.createSign(payResult, "MD5", config.getMchKey(), null));

The front‑end invokes wx.requestPayment with these values. The success callback only indicates that the user completed the UI flow; actual order fulfillment must wait for server‑side confirmation.

Callback Handling – Four Defense Lines

Entry Point

@PostMapping("/callback/wechat")
public String wechatPayCallback(@RequestBody String xmlData) {
    boolean handled = payCallbackHandler.doCallback(PayConstant.CHANNEL_WECHAT, xmlData);
    return handled ? WxPayNotifyResponse.success("OK") : WxPayNotifyResponse.fail("FAIL");
}

The raw XML is kept unchanged for signature verification, and the response must be the exact XML format required by WeChat.

1. Signature Verification

WxJava parses and verifies the signature in one step; failure throws an exception.

WxPayService wxPayService = new WxPayServiceImpl();
WxPayConfig config = new WxPayConfig();
config.setMchKey(configSnapshot.getMchKey()); // use snapshot key
wxPayService.setConfig(config);
WxPayOrderNotifyResult notifyResult = wxPayService.parseOrderNotifyResult(xmlData);

2. Idempotency

Before processing, the system checks whether the payment record is already marked as successful. If so, it returns success immediately to stop further retries.

if (PayConstant.STATUS_SUCCESS.equals(paymentTrade.getPayStatus())) {
    return true;
}

3. Order Number and Amount Validation

The callback’s outTradeNo must match a unique payment detail in the database, and the amount (in cents) must equal the stored order amount.

BigDecimal notifyAmount = BigDecimal.valueOf(notifyResult.getTotalFee())
        .divide(BigDecimal.valueOf(100), 2, RoundingMode.DOWN);
if (notifyAmount.compareTo(tradeDetail.getAmount()) != 0) {
    log.error("Callback amount mismatch, tradeNo={}", notifyResult.getOutTradeNo());
    return false;
}

4. Active Order Query

Even after the previous checks, the system actively queries WeChat’s order‑query API to obtain the authoritative payment state.

WxPayOrderQueryResult queryResult = wxPayService.queryOrder(null, outTradeNo);
boolean reallyPaid = "SUCCESS".equals(queryResult.getReturnCode()) &&
                    "SUCCESS".equals(queryResult.getResultCode()) &&
                    "SUCCESS".equals(queryResult.getTradeState());

If all four defenses pass, the payment status is updated to successful and the transaction ID from WeChat is recorded.

Business Notification

After confirming the payment, the callback publishes a Spring event so that downstream business logic (order fulfillment, points awarding, etc.) runs asynchronously and does not block the callback response.

applicationEventPublisher.publishEvent(
        new PaymentCallbackEvent(xmlData, paymentTrade, payChannel, notifyResult));

Checklist for Callback Implementation

Signature verification – prevents forged callbacks.

Idempotency – avoids duplicate processing when WeChat retries.

Order number & amount validation – catches mismatched or tampered amounts.

Active order query – provides a fallback when the push path fails.

Return success or fail XML – ensures WeChat’s retry logic works correctly.

Asynchronous business notification – prevents callback timeout and enables independent retries.

Common Pitfalls and Quick‑Fix Table

Callback never received : check that notifyUrl has no parameters, is publicly reachable, uses HTTPS with a complete certificate chain.

Signature verification fails : ensure the correct API key is used (not appSecret or V3 key) and that the right merchant configuration is selected in multi‑merchant scenarios.

Duplicate callbacks : normal behavior; verify idempotency logic.

Order not advanced after payment : avoid returning success when processing fails; otherwise WeChat will stop retrying and the order stays stuck.

totalFee mismatch : remember WeChat expects cents while most systems store yuan.

Frontend reports success but server cannot find the order : frontend success only means UI flow completed; always rely on server‑side confirmation.

Order creation signature error : first verify the selected merchant configuration, then check that the key contains no stray spaces or line breaks.

Conclusion

Successful payment integration hinges less on calling the API correctly and more on designing a robust state‑machine and reconciliation process. Each state transition must answer “why do I trust that the money arrived?” – signature verification proves the source, idempotency prevents double accounting, amount checks guard against tampering, and an active order query provides the ultimate authority from WeChat. Separating the volatile channel‑specific logic into its own service layer makes future upgrades or additional payment providers straightforward.

WeChat Pay flow diagram
WeChat Pay flow diagram
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

javamicroservicesWeChat PayPayment IntegrationWxJavaCallback Security
samdeepthink
Written by

samdeepthink

Knowledge Planet: Old Dock's Tech Chronicles Zhihu: SamDeepThinking A technical manager who still codes heavily on the front line. From junior developer to tech lead, then tech manager, now leading the whole front‑ and back‑end development team—leveling up along the way. I have some insights on programming, career development, and tech management.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.