Skip to main content

PC and H5 Payments Integration Guide

Introduction

This document explains how to integrate AppInChina’s payment services in PC (desktop browsers) and H5 (mobile web browsers) environments.

Unlike mobile app payments handled through SDKs, PC and H5 payments require redirecting users to external platforms (WeChat Pay or Alipay), and managing the payment session securely.

The correct flow depends on both the payment method and the user’s device.

Architecture overview: backend vs frontend

Understanding the separation between server-side and client-side operations is crucial for secure payment integration:

Backend/server-side (secure operations)

  • API authentication (APP_ID, APP_SECRET must never reach the browser)
  • CreateOrder API calls (sensitive business logic)
  • Payment verification (critical security operations)
  • URL/parameter building (recommended for better security)

Frontend/client-side (JavaScript UX)

  • Device detection (PC vs mobile)
  • QR code display (visual user interface)
  • Page redirections (user navigation)
  • Form submissions (user interactions)
Security principle

Never expose API credentials or sensitive payment data to the client-side. Always verify payments server-side.


1. PC vs H5 — understanding the environment

It’s important to detect whether a user is accessing your application from a PC or mobile browser (H5), as this determines the correct payment flow.

The term H5 refers to mobile web environments, including:

  • Standard mobile browsers (e.g., Chrome, Safari)
  • Embedded app browsers (e.g., WeChat internal browser)

Payment behavior by environment

Payment MethodPCH5
AlipayRedirect to Alipay Web CashierRedirect to Alipay Web Cashier
WeChat PayDisplay QR Code for scanning in WeChat AppRedirect to WeChat H5 payment page

Frontend: device detection (JavaScript)

function detectEnvironment() {
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
return isMobile ? 'H5' : 'NATIVE';
}

// Set the paySource based on device
document.getElementById('paySource').value = detectEnvironment();

2. Payment flow overview

All flows follow a common sequence:

  1. Frontend: user submits payment form
  2. Backend: server calls CreateOrder API (secure, with credentials)
  3. Backend/frontend: process response based on payment method
  4. External: user completes payment in WeChat or Alipay
  5. External: user optionally redirected to your return URL
  6. Backend: server verifies payment using QuerySingleOrder API
caution

Your system temporarily loses control after redirecting to a payment platform. Always verify payment on your server.

API endpoint configuration

The CreateOrder API endpoint is:

  • Endpoint: https://api.appinchinaservices.com/order.json
  • Method: POST
  • Content-Type: application/json

API authentication (backend only)

All CreateOrder API calls require authentication headers:

  • APP_ID: Your assigned application identifier
  • APP_SECRET: Your application secret key
  • Content-Type: application/json; charset=utf-8
danger

Never include APP_ID or APP_SECRET in frontend JavaScript code. These credentials must remain server-side only.

3. Alipay integration (PC & H5)

Both PC and H5 use Alipay Web Cashier via URL redirection.

3.1 Backend: order creation (Java)

Call CreateOrder API with:

  • payChannel: ALIPAY
  • paySource: NATIVE for PC or H5 for mobile
  • Other fields: amount, bizNo, goodsTitle, customerIdentity, etc.
caution

Make sure to inform your integration contact at AppInChina about what pay source you want to use so that our system can be configured accordingly.

Backend: example implementation (Java)

@PostMapping("/pay")
public String processAlipayPayment(PayOrderDTO payOrderDTO, Model model) {
// Prepare API request
String jsonBody = JSON.toJSONString(payOrderDTO);

HttpHeaders headers = new HttpHeaders();
headers.set("APP_ID", appId); // From secure config
headers.set("APP_SECRET", appSecret); // From secure config
headers.setContentType(MediaType.APPLICATION_JSON);

HttpEntity<String> request = new HttpEntity<>(jsonBody, headers);

// Call CreateOrder API
ResponseEntity<String> response = restTemplate.exchange(
apiEndpoint + "/order.json",
HttpMethod.POST,
request,
String.class
);

if (response.getStatusCode() == HttpStatus.OK) {
JSONObject data = JSON.parseObject(response.getBody()).getJSONObject("data");
return redirectToAlipay(data, model);
} else {
model.addAttribute("error", "Payment initialization failed");
return "error";
}
}

Backend: example request body

{
"bizNo": "ORDER_001",
"goodsTitle": "Product Name",
"customerIdentity": "user123",
"payChannel": "ALIPAY",
"paySource": "NATIVE",
"amount": 100
}
note

amount is in cents (e.g., 100 = ¥1.00 RMB). If your UI collects a decimal amount (yuan), convert to cents on your backend before calling the CreateOrder API.

If you use a returnUrl in your own system, treat it as your UX configuration; it is not part of the Payments CreateOrder parameters unless AppInChina explicitly enabled/configured it for your account.

API response

{
"code": 0,
"msg": "success",
"data": {
"app_id": "2021001156621375",
"biz_content": "{...}",
"charset": "utf-8",
"format": "json",
"method": "alipay.trade.app.pay",
"notify_url": "https://api.appinchinaservices.com/payBackAlipay",
"return_url": "https://api.appinchinaservices.com/payBackAlipay",
"sign": "...",
"sign_type": "RSA2",
"timestamp": "2025-05-19 12:43:35",
"version": "1.0"
}
}

3.2 Payment redirection

You have two implementation options for Alipay redirection:

Advantages: More secure, parameters stay server-side, better control

private String redirectToAlipay(JSONObject data, Model model) {
StringBuilder url = new StringBuilder("https://openapi.alipay.com/gateway.do?");

for (Map.Entry<String, Object> entry : data.entrySet()) {
url.append(entry.getKey())
.append("=")
.append(URLEncoder.encode(entry.getValue().toString(), "utf-8"))
.append("&");
}

// Remove trailing &
String redirectUrl = url.substring(0, url.length() - 1);

// Direct server redirect (most secure)
return "redirect:" + redirectUrl;
}

Option B: client-side redirect

Advantages: More flexible frontend control
Disadvantages: Exposes payment parameters to client

function redirectToAlipay(alipayUrl) {
document.getElementById('loading').style.display = 'block';

setTimeout(() => {
window.location.href = alipayUrl;
}, 1000);
}

3.3 Return handling

After completing payment, the user may be redirected to your return_url, but this is not a confirmation of payment success.

Always verify the result via a server-side order query.

4. WeChat Pay integration

WeChat Pay behavior differs significantly between PC and H5 environments.

4.1 PC payments (QR code flow)

WeChat Pay on PC requires users to scan a QR code using their WeChat mobile app.

Backend: order creation (Java)

@PostMapping("/pay")
public String processWeChatPayment(PayOrderDTO payOrderDTO, Model model) {
payOrderDTO.setPayChannel("WECHAT");
payOrderDTO.setPaySource("NATIVE");

ResponseEntity<String> response = callCreateOrderAPI(payOrderDTO);

if (response.getStatusCode() == HttpStatus.OK) {
JSONObject data = JSON.parseObject(response.getBody()).getJSONObject("data");

// Extract QR code URL for frontend
String codeUrl = data.getString("code_url");
model.addAttribute("qrCodeUrl", codeUrl);
model.addAttribute("paymentMethod", "wechat_qr");

return "payment_result";
}

return "error";
}

Backend: example response for PC

{
"code": 0,
"msg": "success",
"data": {
"code_url": "weixin://wxpay/bizpayurl?pr=abc123"
}
}

The code_url field contains the payment URL that should be converted to a QR code for user scanning.

Frontend: QR code display (JavaScript)

function displayWeChatQR(codeUrl) {
new QRCode(document.getElementById("qrcode"), {
text: codeUrl,
width: 200,
height: 200,
codeWidth: 180,
codeHeight: 180,
colorDark: "#000000",
colorLight: "#ffffff"
});

// Optional: Add auto-refresh for expired QR codes
setTimeout(() => {
document.getElementById('refresh-btn').style.display = 'block';
}, 300000); // Show refresh after 5 minutes
}

4.2 H5 payments (mobile web)

For WeChat H5 payments, the API returns a mweb_url for direct browser redirection.

Backend: H5 order creation (Java)

@PostMapping("/pay")
public String processWeChatH5Payment(PayOrderDTO payOrderDTO, Model model) {
payOrderDTO.setPayChannel("WECHAT");
payOrderDTO.setPaySource("H5");

ResponseEntity<String> response = callCreateOrderAPI(payOrderDTO);

if (response.getStatusCode() == HttpStatus.OK) {
JSONObject data = JSON.parseObject(response.getBody()).getJSONObject("data");
String mwebUrl = data.getString("mweb_url");

// Option A: Direct server redirect
return "redirect:" + mwebUrl;
}

return "error";
}

Backend: example response for H5

{
"code": 0,
"msg": "success",
"data": {
"mweb_url": "https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkmweb?..."
}
}

Frontend: H5 redirect (JavaScript)

function redirectToWeChatH5(mwebUrl) {
document.body.innerHTML = '<div style="text-align:center; padding:50px;">Redirecting to WeChat Pay...</div>';
window.location.href = mwebUrl;
}

4.3 Notes on WeChat integration

  • QR codes expire after 5–10 minutes. Regenerate if needed.
  • Always verify the payment server-side.
  • Redirects (if any) are not sufficient for status confirmation.

5. Payment verification (backend only)

Payment verification must always happen server-side for security.

danger

Never rely on frontend callbacks or return URLs for payment confirmation. Always verify server-side.

Backend: payment status check (Java)

@Service
public class PaymentVerificationService {

public PaymentStatus verifyPayment(String customerIdentity, String bizNo) {
HttpHeaders headers = new HttpHeaders();
headers.set("APP_ID", appId);
headers.set("APP_SECRET", appSecret);
HttpEntity<Void> entity = new HttpEntity<>(headers);

try {
String url = apiEndpoint
+ "/detail.json"
+ "?bizNo=" + URLEncoder.encode(bizNo, "UTF-8")
+ "&customerIdentity=" + URLEncoder.encode(customerIdentity, "UTF-8");

ResponseEntity<String> response = restTemplate.exchange(
url,
HttpMethod.GET,
entity,
String.class
);

return parsePaymentStatus(response.getBody());

} catch (Exception e) {
log.error("Payment verification failed", e);
return PaymentStatus.ERROR;
}
}

private PaymentStatus parsePaymentStatus(String responseBody) {
JSONObject result = JSON.parseObject(responseBody);

if (result.getInteger("code") == 0) {
JSONObject order = result.getJSONObject("data");
if (order != null) {
String status = order.getString("paymentStatus");

switch (status) {
case "PAID": return PaymentStatus.PAID;
case "PENDING": return PaymentStatus.PENDING;
case "CLOSE": return PaymentStatus.CLOSED;
case "REFUND": return PaymentStatus.REFUNDED;
default: return PaymentStatus.UNKNOWN;
}
}
}

return PaymentStatus.ERROR;
}
}

6. Best practices

Security best practices

  • Backend

    • Never expose credentials (APP_ID, APP_SECRET) to the frontend
    • Always verify payments server-side before fulfillment
    • Use HTTPS for all payment-related communications
    • Validate return URLs to prevent open redirects
  • Frontend

    • Sanitize user inputs before sending to backend
    • Use CSP headers to reduce XSS risk
    • Implement rate limiting on payment endpoints

User experience best practices

  • Clear user guidance

    • Provide clear instructions for each payment method
    • Show loading states during redirects
    • Handle timeouts gracefully with retry options
    • Display QR code expiration warnings
  • Error handling

// Backend: comprehensive error handling
@PostMapping("/pay")
public String processPayment(PayOrderDTO payOrder, Model model) {
try {
// Payment processing...
return "payment_result";

} catch (PaymentTimeoutException e) {
model.addAttribute("error", "Payment service timeout. Please try again.");
return "payment_error";

} catch (InvalidParameterException e) {
model.addAttribute("error", "Invalid payment parameters.");
return "payment_error";

} catch (Exception e) {
log.error("Payment processing failed", e);
model.addAttribute("error", "Payment failed. Please contact support.");
return "payment_error";
}
}
// Frontend: user-friendly error handling
function handlePaymentError(error) {
const errorMsg = document.getElementById('error-message');

switch (error.type) {
case 'TIMEOUT':
errorMsg.textContent = '支付超时,请重试';
break;
case 'NETWORK':
errorMsg.textContent = '网络错误,请检查连接';
break;
default:
errorMsg.textContent = '支付失败,请稍后重试';
}

errorMsg.style.display = 'block';
}

Reliability best practices

  • Handle edge cases

    • QR code expiration (WeChat PC)
    • User abandonment (user closes browser)
    • Network failures (retry mechanisms)
    • Duplicate payments (idempotency)
  • Configure return URLs properly

PayOrderDTO order = new PayOrderDTO();
order.setReturnUrl("https://yoursite.com/payment/return");
// ... other fields
note

Return URLs are for user experience only. Always verify payment status server-side regardless of return URL callbacks.

  • Monitoring and logging
// Backend: structured logging
log.info("Payment initiated: bizNo={}, channel={}, amount={}",
payOrder.getBizNo(), payOrder.getPayChannel(), payOrder.getAmount());

log.info("Payment verified: bizNo={}, status={}, duration={}ms",
bizNo, status, duration);

7. Complete working example

Frontend: payment form (HTML)

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>AppInChina Payment Demo</title>
</head>
<body>
<form method="post" action="/pay" id="payment-form">
<input type="text" name="bizNo" placeholder="Order Number" required>
<input type="text" name="goodsTitle" placeholder="Product Name" required>
<input type="text" name="customerIdentity" placeholder="Customer ID" required>

<select name="payChannel" required>
<option value="ALIPAY">Alipay 支付宝</option>
<option value="WECHAT">WeChat Pay 微信支付</option>
</select>

<select name="paySource" id="paySource" required>
<option value="NATIVE">PC Desktop</option>
<option value="H5">Mobile H5</option>
</select>

<input type="number" name="amount" step="1" min="1" required>
<button type="submit">Pay Now 立即支付</button>
</form>

<script>
// Auto-detect device and set paySource
function detectDevice() {
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
document.getElementById('paySource').value = isMobile ? 'H5' : 'NATIVE';
}

// Enhanced form validation
document.getElementById('payment-form').addEventListener('submit', function(e) {
const amount = parseInt(document.querySelector('[name="amount"]').value, 10);
if (!Number.isFinite(amount) || amount < 1) {
e.preventDefault();
alert('Amount must be at least 1 (cent)');
}
});

detectDevice();
</script>
</body>
</html>

Backend: complete controller (Java)

@Controller
@Slf4j
public class PaymentController {

@Autowired private RestTemplate restTemplate;
@Value("${aic.endpoint}") private String apiEndpoint;
@Value("${aic.app-id}") private String appId;
@Value("${aic.app-secret}") private String appSecret;

@PostMapping("/pay")
public String processPayment(@Valid PayOrderDTO payOrder, Model model) {
try {
// Secure API call
ResponseEntity<String> response = callCreateOrderAPI(payOrder);

if (response.getStatusCode() != HttpStatus.OK) {
return handlePaymentError(model, "API call failed");
}

JSONObject data = JSON.parseObject(response.getBody()).getJSONObject("data");

// Route based on payment method
if ("ALIPAY".equals(payOrder.getPayChannel())) {
return handleAlipayResponse(data);
} else if ("WECHAT".equals(payOrder.getPayChannel())) {
return handleWeChatResponse(data, payOrder.getPaySource(), model);
}

} catch (Exception e) {
log.error("Payment processing failed: bizNo={}", payOrder.getBizNo(), e);
return handlePaymentError(model, "Payment processing failed");
}

return handlePaymentError(model, "Unknown payment method");
}

private ResponseEntity<String> callCreateOrderAPI(PayOrderDTO payOrder) {
HttpHeaders headers = new HttpHeaders();
headers.set("APP_ID", appId); // Secure server-side only
headers.set("APP_SECRET", appSecret); // Secure server-side only
headers.setContentType(MediaType.APPLICATION_JSON);

HttpEntity<String> request = new HttpEntity<>(JSON.toJSONString(payOrder), headers);

return restTemplate.exchange(
apiEndpoint + "/order.json",
HttpMethod.POST,
request,
String.class
);
}

private String handleAlipayResponse(JSONObject data) throws UnsupportedEncodingException {
StringBuilder url = new StringBuilder("https://openapi.alipay.com/gateway.do?");

for (Map.Entry<String, Object> entry : data.entrySet()) {
url.append(entry.getKey())
.append("=")
.append(URLEncoder.encode(entry.getValue().toString(), "utf-8"))
.append("&");
}

// Server-side redirect (secure)
return "redirect:" + url.substring(0, url.length() - 1);
}

private String handleWeChatResponse(JSONObject data, String paySource, Model model) {
if ("NATIVE".equals(paySource)) {
// PC: QR code display
model.addAttribute("qrCodeUrl", data.getString("code_url"));
model.addAttribute("paymentType", "wechat_qr");
return "payment_result";

} else if ("H5".equals(paySource)) {
// Mobile: direct redirect
return "redirect:" + data.getString("mweb_url");
}

return handlePaymentError(model, "Invalid payment source");
}

private String handlePaymentError(Model model, String message) {
model.addAttribute("error", message);
return "error";
}

@GetMapping("/payment/status/{bizNo}")
@ResponseBody
public Map<String, Object> checkPaymentStatus(@PathVariable String bizNo) {
// Secure verification (backend only)
PaymentStatus status = verifyPaymentSecurely(bizNo);

Map<String, Object> result = new HashMap<>();
result.put("status", status.toString());
result.put("timestamp", System.currentTimeMillis());

return result;
}
}