Skip to main content

WeChat Pay

Accept payments via WeChat Pay, integrated with WeChat's 1.3+ billion messaging app users in China and worldwide through WeChat Pay, one of China's two dominant mobile payment platforms with seamless integration into the WeChat ecosystem.

Overview​

WeChat Pay (Weixin Pay in China) is Tencent's mobile payment solution integrated directly into the WeChat messaging app. With integrated with WeChat messaging (1.3+ billion users), WeChat Pay is one of China's two dominant payment platforms, it's one of the two largest payment methods in China alongside Alipay. WeChat Pay enables instant payments through QR codes, in-app purchases, and mini-programs.

Key Features:

  • ✅ Integrated with WeChat - Access to 1.3B+ messaging app users across China and overseas Chinese communities
  • ✅ WeChat integration - Seamless payment within the messaging app ecosystem
  • ✅ QR code payments - Fast, contactless payment experience
  • ✅ Cross-border support - Accept payments from Chinese tourists worldwide
  • ✅ Multi-currency - Settle in THB, SGD, MYR, JPY, USD
  • ✅ Mini-programs - Build e-commerce experiences within WeChat

Supported Regions​

RegionCurrencyMin AmountMax AmountSettlement
ThailandTHB฿1.00฿100,000THB
SingaporeSGDS$0.10S$5,000SGD
MalaysiaMYRRM1.00RM20,000MYR
JapanJPYÂ¥100Â¥1,000,000JPY
Hong KongHKDHK$1.00HK$50,000HKD
Cross-Border Payments

WeChat Pay is particularly valuable for merchants targeting Chinese tourists or cross-border e-commerce. Customers pay in CNY, you receive settlement in your local currency.

How It Works​

Customer Experience:

  1. Customer selects "WeChat Pay" at checkout
  2. QR code is displayed on screen
  3. Customer opens WeChat app and scans QR code
  4. Reviews transaction details in WeChat
  5. Confirms payment with WeChat Pay password or biometric
  6. Returns to merchant website
  7. Receives payment confirmation

Typical completion time: 30-90 seconds

Implementation​

Step 1: Create WeChat Pay Source​

curl https://api.omise.co/sources \
-u skey_test_YOUR_SECRET_KEY: \
-d "type=wechat_pay" \
-d "amount=10000" \
-d "currency=THB"

Response:

{
"object": "source",
"id": "src_test_5rt6s9vah5lkvi1rh9c",
"type": "wechat_pay",
"flow": "redirect",
"amount": 10000,
"currency": "THB",
"scannable_code": {
"type": "qr",
"image": {
"uri": "https://omise.co/qr/...",
"download_uri": "https://api.omise.co/..."
}
}
}

Step 2: Create Charge​

curl https://api.omise.co/charges \
-u skey_test_YOUR_SECRET_KEY: \
-d "amount=10000" \
-d "currency=THB" \
-d "source=src_test_5rt6s9vah5lkvi1rh9c" \
-d "return_uri=https://yourdomain.com/payment/callback"

Step 3: Display QR Code​

app.post('/checkout/wechat-pay', async (req, res) => {
try {
const { amount, order_id } = req.body;

// Validate amount
if (amount < 100 || amount > 10000000) {
return res.status(400).json({
error: 'Amount must be between ฿1 and ฿100,000'
});
}

// Create source
const source = await omise.sources.create({
type: 'wechat_pay',
amount: amount,
currency: 'THB'
});

// Create charge
const charge = await omise.charges.create({
amount: amount,
currency: 'THB',
source: source.id,
return_uri: `${process.env.BASE_URL}/payment/callback`,
metadata: {
order_id: order_id
}
});

// Return QR code URL to display
res.json({
qr_code_url: charge.source.scannable_code.image.uri,
charge_id: charge.id,
expires_at: new Date(Date.now() + 5 * 60 * 1000) // 5 minutes
});

} catch (error) {
console.error('WeChat Pay error:', error);
res.status(500).json({ error: error.message });
}
});

Step 4: Display QR Code UI​

<!DOCTYPE html>
<html>
<head>
<title>WeChat Pay Checkout</title>
<style>
.wechat-pay-container {
max-width: 400px;
margin: 50px auto;
text-align: center;
padding: 30px;
border: 1px solid #e0e0e0;
border-radius: 10px;
}
.qr-code {
width: 300px;
height: 300px;
margin: 20px auto;
border: 1px solid #ddd;
padding: 10px;
background: white;
}
.wechat-logo {
width: 60px;
height: 60px;
margin-bottom: 15px;
}
.countdown {
font-size: 18px;
color: #666;
margin-top: 15px;
}
.instructions {
color: #666;
margin: 20px 0;
line-height: 1.6;
}
</style>
</head>
<body>
<div class="wechat-pay-container">
<img src="/images/wechat-logo.svg" class="wechat-logo" alt="WeChat Pay">
<h2>Scan to Pay with WeChat</h2>

<div class="qr-code">
<img id="qr-image" src="" alt="WeChat Pay QR Code" style="width: 100%; height: 100%;">
</div>

<div class="instructions">
<ol style="text-align: left;">
<li>Open WeChat app on your phone</li>
<li>Tap the "+" icon and select "Scan QR Code"</li>
<li>Scan the QR code above</li>
<li>Confirm payment in WeChat</li>
</ol>
</div>

<div class="countdown">
Time remaining: <span id="timer">5:00</span>
</div>

<p style="color: #999; font-size: 14px; margin-top: 20px;">
Payment amount: <strong id="amount"></strong>
</p>
</div>

<script>
// Countdown timer
let timeLeft = 300; // 5 minutes
const timerElement = document.getElementById('timer');

const countdown = setInterval(() => {
timeLeft--;
const minutes = Math.floor(timeLeft / 60);
const seconds = timeLeft % 60;
timerElement.textContent = `${minutes}:${seconds.toString().padStart(2, '0')}`;

if (timeLeft <= 0) {
clearInterval(countdown);
alert('QR code expired. Please try again.');
window.location.href = '/checkout';
}
}, 1000);

// Poll for payment status
const chargeId = new URLSearchParams(window.location.search).get('charge_id');

const checkStatus = setInterval(async () => {
try {
const response = await fetch(`/api/check-payment-status/${chargeId}`);
const data = await response.json();

if (data.status === 'successful') {
clearInterval(checkStatus);
clearInterval(countdown);
window.location.href = '/payment-success';
} else if (data.status === 'failed') {
clearInterval(checkStatus);
clearInterval(countdown);
window.location.href = '/payment-failed';
}
} catch (error) {
console.error('Status check error:', error);
}
}, 3000); // Check every 3 seconds

// Load QR code
fetch('/api/create-wechat-payment', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
amount: 10000,
order_id: 'ORD-12345'
})
})
.then(res => res.json())
.then(data => {
document.getElementById('qr-image').src = data.qr_code_url;
document.getElementById('amount').textContent = `฿${(data.amount / 100).toFixed(2)}`;
});
</script>
</body>
</html>

Step 5: Handle Webhook​

app.post('/webhooks/omise', (req, res) => {
const event = req.body;

if (event.key === 'charge.complete' && event.data.source.type === 'wechat_pay') {
const charge = event.data;

if (charge.status === 'successful') {
processOrder(charge.metadata.order_id);
sendConfirmationEmail(charge.metadata.customer_email);
} else if (charge.status === 'failed') {
handleFailedPayment(charge.metadata.order_id);
}
}

res.sendStatus(200);
});

Complete Implementation Example​

// Express.js server
const express = require('express');
const omise = require('omise')({
secretKey: process.env.OMISE_SECRET_KEY
});

const app = express();
app.use(express.json());

// Create WeChat Pay payment
app.post('/api/create-wechat-payment', async (req, res) => {
try {
const { amount, order_id, customer_email } = req.body;

// Validate amount (฿1 - ฿100,000)
if (amount < 100 || amount > 10000000) {
return res.status(400).json({
error: 'Amount must be between ฿1 and ฿100,000'
});
}

// Create source
const source = await omise.sources.create({
type: 'wechat_pay',
amount: amount,
currency: 'THB'
});

// Create charge
const charge = await omise.charges.create({
amount: amount,
currency: 'THB',
source: source.id,
return_uri: `${process.env.BASE_URL}/payment/callback`,
metadata: {
order_id: order_id,
customer_email: customer_email,
payment_method: 'wechat_pay'
}
});

// Return QR code
res.json({
charge_id: charge.id,
qr_code_url: charge.source.scannable_code.image.uri,
amount: charge.amount,
expires_at: new Date(Date.now() + 5 * 60 * 1000)
});

} catch (error) {
console.error('WeChat Pay error:', error);
res.status(500).json({ error: error.message });
}
});

// Check payment status (for polling)
app.get('/api/check-payment-status/:chargeId', async (req, res) => {
try {
const charge = await omise.charges.retrieve(req.params.chargeId);
res.json({
status: charge.status,
paid: charge.paid
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});

// Webhook handler (primary notification method)
app.post('/webhooks/omise', (req, res) => {
const event = req.body;

if (event.key === 'charge.complete') {
const charge = event.data;

if (charge.source.type === 'wechat_pay') {
if (charge.status === 'successful') {
updateOrderStatus(charge.metadata.order_id, 'paid');
sendConfirmationEmail(charge.metadata.customer_email);

// Log successful payment
console.log(`WeChat Pay payment successful: ${charge.id}`);
} else {
updateOrderStatus(charge.metadata.order_id, 'failed');

// Log failed payment
console.log(`WeChat Pay payment failed: ${charge.id}, reason: ${charge.failure_message}`);
}
}
}

res.sendStatus(200);
});

// Helper functions
async function updateOrderStatus(orderId, status) {
// Update order in database
await db.orders.update({ id: orderId }, { status: status });
}

async function sendConfirmationEmail(email) {
// Send email confirmation
// Implementation depends on email service
}

app.listen(3000, () => {
console.log('Server running on port 3000');
});

Refund Support​

WeChat Pay supports full and partial refunds within 90 days:

// Full refund
const fullRefund = await omise.charges.refund('chrg_test_...', {
amount: 10000
});

// Partial refund
const partialRefund = await omise.charges.refund('chrg_test_...', {
amount: 5000 // Half refund
});
Refund Timeline
  • Refunds are processed within 1-3 business days
  • Customers receive refunds in their WeChat wallet
  • Supported within 90 days of original transaction
Refund Policy

Refund windows and policies are subject to change. Always verify current refund capabilities via the Omise API documentation or your merchant dashboard.

Common Issues & Troubleshooting​

Issue: QR code not scanning​

Cause: Customer using incompatible QR scanner or expired code

Solution:

// Ensure QR code is fresh and valid
function displayQRCode(qrCodeUrl, expiresAt) {
const qrImage = document.getElementById('qr-code');
qrImage.src = qrCodeUrl;

// Show expiration warning
const timeUntilExpiry = new Date(expiresAt) - Date.now();
if (timeUntilExpiry < 60000) { // Less than 1 minute
showWarning('QR code expiring soon! Please scan now.');
}
}

// Provide refresh option
function refreshQRCode() {
// Generate new QR code
createNewPayment();
}

Issue: Payment timeout​

Cause: Customer didn't complete payment within 5 minutes

Solution:

// Set appropriate timeout
const QR_EXPIRY_TIME = 5 * 60 * 1000; // 5 minutes

setTimeout(() => {
if (!paymentConfirmed) {
showMessage('Payment session expired. Please create a new payment.');
enableRetry();
}
}, QR_EXPIRY_TIME);

Issue: WeChat app not opening on mobile​

Cause: Deep link issue on mobile browsers

Solution:

function isMobileDevice() {
return /Android|iPhone|iPad|iPod/i.test(navigator.userAgent);
}

if (isMobileDevice()) {
// Try to open WeChat app directly
window.location = `weixin://dl/business/?t=${encodeURIComponent(qrCodeData)}`;

// Fallback to QR code display
setTimeout(() => {
if (document.hidden === false) {
displayQRCodeFallback();
}
}, 2000);
} else {
// Desktop: Show QR code
displayQRCode();
}

Issue: Currency conversion confusion​

Cause: Customer sees CNY, merchant sees THB

Solution:

// Display both currencies clearly
function displayPaymentAmount(amountTHB, exchangeRate) {
const amountCNY = amountTHB * exchangeRate;

return `
<div class="payment-amount">
<p>You pay: ¥${amountCNY.toFixed(2)} CNY</p>
<p class="small">Merchant receives: ฿${(amountTHB / 100).toFixed(2)} THB</p>
<p class="small">Exchange rate: 1 THB = ${exchangeRate.toFixed(4)} CNY</p>
</div>
`;
}

Issue: Payment stuck in pending​

Cause: Network issues or delayed notification from WeChat

Solution:

// Implement robust status checking
async function checkPaymentStatus(chargeId, maxAttempts = 10) {
let attempts = 0;

const checkInterval = setInterval(async () => {
attempts++;

try {
const charge = await omise.charges.retrieve(chargeId);

if (charge.status === 'successful') {
clearInterval(checkInterval);
handleSuccess(charge);
} else if (charge.status === 'failed') {
clearInterval(checkInterval);
handleFailure(charge);
} else if (attempts >= maxAttempts) {
clearInterval(checkInterval);
handleTimeout(charge);
}
} catch (error) {
console.error('Status check error:', error);
}
}, 3000); // Check every 3 seconds
}

Best Practices​

1. Display Clear QR Code​

// Generate high-quality QR code display
function displayQRCode(qrUrl) {
return `
<div class="qr-container">
<img src="${qrUrl}"
alt="WeChat Pay QR Code"
style="width: 300px; height: 300px; padding: 20px; background: white; border: 2px solid #09BB07;">
<p style="color: #09BB07; margin-top: 15px;">
<strong>Scan with WeChat to Pay</strong>
</p>
</div>
`;
}

2. Implement Countdown Timer​

function startCountdown(duration, onExpire) {
let timeLeft = duration;
const display = document.getElementById('timer');

const timer = setInterval(() => {
const minutes = Math.floor(timeLeft / 60);
const seconds = timeLeft % 60;

display.textContent = `${minutes}:${seconds.toString().padStart(2, '0')}`;

// Warning at 1 minute
if (timeLeft === 60) {
display.style.color = 'red';
showWarning('Only 1 minute remaining!');
}

if (--timeLeft < 0) {
clearInterval(timer);
onExpire();
}
}, 1000);

return timer;
}

3. Use Webhooks for Reliability​

// Webhook is more reliable than polling
app.post('/webhooks/omise', handleWebhook);

// Polling is backup for UI updates
const statusCheckInterval = setInterval(checkStatus, 3000);

4. Validate Amount Limits​

function validateWeChatPayAmount(amount, currency) {
const limits = {
'THB': { min: 100, max: 10000000 },
'SGD': { min: 10, max: 500000 },
'MYR': { min: 100, max: 2000000 },
'JPY': { min: 100, max: 100000000 }
};

const limit = limits[currency];
if (!limit) {
return 'Currency not supported';
}

if (amount < limit.min) {
return `Minimum amount is ${formatCurrency(limit.min, currency)}`;
}

if (amount > limit.max) {
return `Maximum amount is ${formatCurrency(limit.max, currency)}`;
}

return null; // Valid
}

5. Handle Cross-Border Scenarios​

// Provide clear information about currency conversion
function displayCrossBorderInfo(amountLocal, currency) {
return `
<div class="cross-border-info">
<h4>Cross-Border Payment</h4>
<ul>
<li>You will be charged in Chinese Yuan (CNY)</li>
<li>Your bank/WeChat will handle currency conversion</li>
<li>Merchant receives: ${formatCurrency(amountLocal, currency)}</li>
<li>Final amount depends on exchange rate at payment time</li>
</ul>
</div>
`;
}

6. Optimize for Mobile​

// Detect mobile and provide better UX
if (isMobileDevice()) {
// Show larger QR code on mobile
qrCodeElement.style.width = '90vw';
qrCodeElement.style.maxWidth = '400px';

// Add instructions for mobile
showInstructions('Take a screenshot or tap to save QR code, then open WeChat to scan');
}

FAQ​

What is WeChat Pay?

WeChat Pay is Tencent's mobile payment solution integrated into the WeChat messaging app. With over 1.3 billion users, it's one of China's two dominant payment methods alongside Alipay. Customers can pay instantly using QR codes or in-app payments.

Do I need a Chinese bank account to accept WeChat Pay?

No, you don't need a Chinese bank account. Omise handles the complexity of cross-border payments. Your customers pay in CNY from their WeChat wallets, and you receive settlement in your local currency (THB, SGD, MYR, JPY, etc.).

What are the transaction limits?

Limits vary by region:

  • Thailand: ฿1 - ฿100,000
  • Singapore: S$0.10 - S$5,000
  • Malaysia: RM1 - RM20,000
  • Japan: Â¥100 - Â¥1,000,000

Individual customer limits may vary based on their WeChat Pay verification level and bank settings.

How long does settlement take?

WeChat Pay settlements typically occur within 2-3 business days after the transaction. Exact timing depends on your merchant agreement and settlement schedule. Check your Omise dashboard for specific dates.

Can I refund WeChat Pay payments?

Yes, WeChat Pay supports both full and partial refunds within 90 days of the original transaction. Refunds are processed within 1-3 business days and returned to the customer's WeChat wallet.

How long is the QR code valid?

WeChat Pay QR codes typically expire after 5 minutes. After expiration, you need to generate a new QR code for the customer. Always display a countdown timer to inform customers of the remaining time.

Can customers pay from outside China?

Yes, WeChat Pay supports cross-border payments. Chinese tourists and overseas Chinese users can pay using their China-based WeChat Pay accounts. However, users with non-Chinese WeChat accounts may have limited payment capabilities depending on their region and verification status.

What's the difference between WeChat Pay and Alipay?

Both are major Chinese digital wallets with similar functionality:

  • WeChat Pay: 1.3B users, integrated with WeChat messaging, strong in social payments
  • Alipay: 1B+ users, from Alibaba, strong in e-commerce

For maximum reach in the Chinese market, implement both payment methods.

Do I need to display Chinese language?

While not required, displaying payment instructions in Chinese can improve conversion rates for Chinese customers. The WeChat Pay interface itself is always in the customer's preferred language (usually Chinese).

Testing​

Test Mode​

WeChat Pay can be tested using your test API keys. In test mode:

Test Credentials:

  • Use test API keys (skey_test_xxx)
  • Test supported currencies: THB, SGD, MYR, JPY
  • No actual WeChat account required for testing

Test Flow:

  1. Create source and charge with test API keys
  2. QR code is generated in test mode
  3. Test QR code displays but won't connect to real WeChat
  4. Use Omise Dashboard Actions to mark charge as successful/failed
  5. Verify webhook handling

Testing Implementation:

// Test WeChat Pay for different currencies
const testConfigs = [
{ currency: 'THB', amount: 10000 },
{ currency: 'SGD', amount: 1000 },
{ currency: 'MYR', amount: 1000 },
{ currency: 'JPY', amount: 10000 }
];

for (const config of testConfigs) {
const source = await omise.sources.create({
type: 'wechat_pay',
amount: config.amount,
currency: config.currency
});

const charge = await omise.charges.create({
amount: config.amount,
currency: config.currency,
source: source.id
});

console.log(`Test ${config.currency} QR:`, charge.source.scannable_code.image.download_uri);
}

Test Scenarios:

  • Successful payment: Verify order completion workflow
  • Failed payment: Test error handling
  • QR code display: Test QR rendering on desktop/mobile
  • Multiple currencies: Test all supported currencies
  • Amount limits: Verify min/max per currency
  • QR expiration: Test timeout handling
  • Mobile vs Desktop: Test both flows
  • Webhook delivery: Verify all webhook notifications

Important Notes:

  • Test mode QR codes won't scan with real WeChat app
  • Use dashboard to simulate payment completion
  • Test all supported currencies before going live
  • Verify webhook handling for all statuses
  • Test both mobile QR scan and desktop QR display
  • Test Chinese language support if targeting Chinese customers

For comprehensive testing guidelines, see the Testing Documentation.

Next Steps​

  1. Create WeChat Pay source
  2. Implement QR code display
  3. Set up webhook handling
  4. Test payment flow
  5. Go live