Skip to main content

WeChat Pay MPM

Accept offline in-store payments via WeChat Pay using Merchant-Presented Mode (MPM), where customers scan a QR code displayed by the merchant at the point-of-sale terminal.

Other WeChat Pay Options

For online payments, see WeChat Pay. For customer-presented barcode scanning, see WeChat Pay UPM.

Overviewโ€‹

WeChat Pay Merchant-Presented Mode (MPM), also known as "C scan B" (Customer scans Business), is an offline payment method where the merchant generates and displays a unique QR code for each transaction. The customer then scans this QR code using their WeChat app to complete the payment.

This flow is ideal for in-store retail environments, restaurants, and any physical point-of-sale where the merchant controls the checkout process and displays payment QR codes on terminals, tablets, or printed receipts.

Key Features:

  • โœ… In-store payments - Perfect for retail, restaurants, and physical stores
  • โœ… No customer app install - Works with existing WeChat app (1.3B+ users)
  • โœ… Quick checkout - Customers scan and pay in seconds
  • โœ… No hardware required - Display QR on any screen or print
  • โœ… Cross-border - Accept payments from Chinese tourists
  • โœ… Offline capable - Works in environments with limited internet for customers

Supported Regionsโ€‹

RegionCurrencyMin AmountMax AmountAPI Version
ThailandTHB20.00 (2,000 satang)150,000.00 (15,000,000 satang)2017-11-02

To enable WeChat Pay MPM, email support@omise.co to request this feature.

Cross-Border Payments

WeChat Pay MPM is particularly valuable for merchants targeting Chinese tourists. Customers pay in CNY from their WeChat wallet, and you receive settlement in THB.

How It Worksโ€‹

Payment Flow:

  1. Merchant creates a charge via Omise API with source type wechat_pay_mpm
  2. QR code is displayed on POS terminal, tablet, or printed
  3. Customer opens WeChat app and navigates to "Scan"
  4. Customer scans the QR code displayed by merchant
  5. Customer reviews transaction details and confirms payment
  6. Merchant receives webhook notification confirming payment
  7. Merchant provides receipt or confirmation to customer

Typical completion time: 30 seconds - 2 minutes

Implementationโ€‹

Step 1: Create Charge with Sourceโ€‹

For WeChat Pay MPM, you create the source and charge in a single API call or separately.

# Create source
curl https://api.omise.co/sources \
-u $OMISE_SECRET_KEY: \
-d "type=wechat_pay_mpm" \
-d "amount=150000" \
-d "currency=THB"

# Create charge with source
curl https://api.omise.co/charges \
-u $OMISE_SECRET_KEY: \
-d "amount=150000" \
-d "currency=THB" \
-d "source=src_test_xxx"

Response:

{
"object": "charge",
"id": "chrg_test_5rt6s9vah5lkvi1rh9c",
"amount": 150000,
"currency": "THB",
"status": "pending",
"source": {
"object": "source",
"id": "src_test_5rt6s9vah5lkvi1rh9c",
"type": "wechat_pay_mpm",
"flow": "offline",
"amount": 150000,
"currency": "THB",
"scannable_code": {
"type": "qr",
"image": {
"download_uri": "https://api.omise.co/charges/chrg_test_.../documents/qr_code.png"
}
}
},
"expires_at": "2024-01-15T14:30:00Z"
}

Step 2: Display QR Code on POSโ€‹

Display the QR code from charge.source.scannable_code.image.download_uri on your POS terminal.

// POS Terminal Display Logic
async function displayPaymentQR(orderId, amount) {
try {
// Create charge
const response = await fetch('/api/create-wechat-mpm-charge', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
order_id: orderId,
amount: amount
})
});

const data = await response.json();

// Display QR code on POS terminal
const qrDisplay = document.getElementById('pos-qr-display');
qrDisplay.innerHTML = `
<div class="payment-screen">
<h2>Scan to Pay with WeChat</h2>
<img src="${data.qr_code_url}" alt="WeChat Pay QR" />
<p class="amount">Amount: เธฟ${(amount / 100).toFixed(2)}</p>
<p class="timer">Expires in: <span id="countdown">2:00:00</span></p>
</div>
`;

// Start expiration countdown
startCountdown(data.expires_at, 'countdown');

// Start polling for payment status
pollPaymentStatus(data.charge_id);

} catch (error) {
showError('Failed to create payment. Please try again.');
}
}

Step 3: Handle Webhook Notificationโ€‹

const express = require('express');
const app = express();

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

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

if (charge.source.type === 'wechat_pay_mpm') {
if (charge.status === 'successful') {
// Payment successful
notifyPOSTerminal(charge.metadata.terminal_id, {
status: 'success',
charge_id: charge.id,
amount: charge.amount
});

// Print receipt
printReceipt(charge);

// Update order status
updateOrderStatus(charge.metadata.order_id, 'paid');

} else if (charge.status === 'failed') {
// Payment failed
notifyPOSTerminal(charge.metadata.terminal_id, {
status: 'failed',
failure_code: charge.failure_code,
failure_message: charge.failure_message
});
}
}
}

res.sendStatus(200);
});

QR Code Displayโ€‹

Display Requirementsโ€‹

For optimal scanning success, follow these guidelines:

RequirementRecommendation
Minimum size3cm x 3cm (1.2" x 1.2")
Recommended size5cm x 5cm (2" x 2") or larger
Resolution300 DPI for printing
ContrastDark QR on white background
Quiet zoneMinimum 4-module white border

POS Terminal Exampleโ€‹

<style>
.wechat-mpm-display {
background: #fff;
padding: 30px;
text-align: center;
border-radius: 10px;
max-width: 400px;
margin: 0 auto;
}
.wechat-mpm-display .qr-container {
background: #fff;
padding: 20px;
border: 3px solid #09BB07;
border-radius: 8px;
display: inline-block;
margin: 20px 0;
}
.wechat-mpm-display .qr-code {
width: 200px;
height: 200px;
}
.wechat-mpm-display .amount {
font-size: 28px;
font-weight: bold;
color: #333;
}
.wechat-mpm-display .timer {
color: #666;
font-size: 16px;
}
.wechat-mpm-display .timer.warning {
color: #ff6600;
}
.wechat-mpm-display .timer.expired {
color: #ff0000;
}
.wechat-logo {
width: 50px;
height: 50px;
}
</style>

<div class="wechat-mpm-display">
<img src="/images/wechat-pay-logo.svg" class="wechat-logo" alt="WeChat Pay" />
<h2>Scan to Pay</h2>

<div class="qr-container">
<img id="qr-code" class="qr-code" src="" alt="WeChat Pay QR Code" />
</div>

<p class="amount">เธฟ<span id="display-amount">1,500.00</span></p>
<p class="timer">Expires in: <span id="countdown">2:00:00</span></p>

<div class="instructions">
<p>1. Open WeChat</p>
<p>2. Tap "Discover" then "Scan"</p>
<p>3. Scan this QR code</p>
</div>
</div>

Charge Expiration Configurationโ€‹

By default, WeChat Pay MPM charges expire after 2 hours. You can customize the expiration time when creating the source.

Custom Expirationโ€‹

curl https://api.omise.co/sources \
-u $OMISE_SECRET_KEY: \
-d "type=wechat_pay_mpm" \
-d "amount=150000" \
-d "currency=THB" \
-d "expires_at=2024-01-15T15:00:00Z"

Expiration Limitsโ€‹

SettingValue
Minimum1 minute
Maximum2 hours
Default2 hours

Implementation with Custom Expirationโ€‹

const source = await omise.sources.create({
type: 'wechat_pay_mpm',
amount: 150000,
currency: 'THB',
// Expire in 30 minutes
expires_at: new Date(Date.now() + 30 * 60 * 1000).toISOString()
});
Expiration Best Practices

For busy retail environments, consider shorter expiration times (5-15 minutes) to avoid customers attempting to scan expired QR codes. Always display a visible countdown timer.

Charge Status Valuesโ€‹

StatusDescription
pendingQR code displayed, awaiting customer scan and payment
successfulPayment completed successfully
failedPayment failed or declined
expiredQR code expired before payment completed
reversedPayment was reversed (voided)

Failure Codesโ€‹

CodeDescriptionRecommended Action
payment_expiredQR code expired before customer completed paymentGenerate new QR code
payment_rejectedPayment rejected by WeChat/issuing bankAsk customer to check WeChat wallet
insufficient_fundCustomer has insufficient balanceAsk customer to top up or use another method
failed_processingGeneral processing failureRetry or contact support
invalid_accountCustomer's WeChat Pay account issueAsk customer to verify account status

Handling Failuresโ€‹

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

if (event.key === 'charge.complete' && event.data.status === 'failed') {
const charge = event.data;
const failureCode = charge.failure_code;

switch (failureCode) {
case 'payment_expired':
// Offer to generate new QR code
notifyPOS('QR expired. Generate new payment?');
break;

case 'insufficient_fund':
// Suggest alternative payment
notifyPOS('Insufficient funds. Suggest another payment method.');
break;

case 'payment_rejected':
// Customer may need to verify account
notifyPOS('Payment rejected. Ask customer to check WeChat wallet.');
break;

default:
notifyPOS(`Payment failed: ${charge.failure_message}`);
}
}

res.sendStatus(200);
});

Refundsโ€‹

WeChat Pay MPM supports full and partial refunds within 90 days of the original transaction.

Create Refundโ€‹

# Full refund
curl https://api.omise.co/charges/chrg_test_xxx/refunds \
-u $OMISE_SECRET_KEY: \
-d "amount=150000"

# Partial refund
curl https://api.omise.co/charges/chrg_test_xxx/refunds \
-u $OMISE_SECRET_KEY: \
-d "amount=50000"

Refund Policyโ€‹

FeatureDetails
Refund window90 days from transaction date
Full refundsSupported
Partial refundsSupported
Multiple refundsSupported (up to original amount)
Processing time1-3 business days
Refund Destination

Refunds are returned to the customer's WeChat Pay wallet. The customer will receive a notification in their WeChat app when the refund is processed.

Best Practices for POS Integrationโ€‹

1. Display Clear QR Codeโ€‹

function displayQRCode(qrUrl, amount) {
// Ensure QR code is prominently displayed
return `
<div class="pos-payment-screen">
<div class="qr-wrapper" style="
background: white;
padding: 20px;
border: 4px solid #09BB07;
display: inline-block;
">
<img src="${qrUrl}"
alt="Scan to Pay"
style="width: 250px; height: 250px;" />
</div>
<h2 style="color: #09BB07;">Scan with WeChat</h2>
<p class="amount">เธฟ${(amount / 100).toFixed(2)}</p>
</div>
`;
}

2. Implement Countdown Timerโ€‹

function startCountdown(expiresAt, elementId) {
const expiryTime = new Date(expiresAt).getTime();

const timer = setInterval(() => {
const now = Date.now();
const timeLeft = expiryTime - now;

if (timeLeft <= 0) {
clearInterval(timer);
document.getElementById(elementId).textContent = 'EXPIRED';
document.getElementById(elementId).classList.add('expired');
handleExpiredQR();
return;
}

const hours = Math.floor(timeLeft / (1000 * 60 * 60));
const minutes = Math.floor((timeLeft % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((timeLeft % (1000 * 60)) / 1000);

document.getElementById(elementId).textContent =
`${hours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;

// Warning when less than 5 minutes remain
if (timeLeft < 5 * 60 * 1000) {
document.getElementById(elementId).classList.add('warning');
}
}, 1000);

return timer;
}

3. Poll for Payment Status (Backup)โ€‹

While webhooks are the primary notification method, implement polling as a backup:

async function pollPaymentStatus(chargeId, interval = 3000, maxAttempts = 40) {
let attempts = 0;

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

try {
const response = await fetch(`/api/charge-status/${chargeId}`);
const data = await response.json();

if (data.status === 'successful') {
clearInterval(poll);
handlePaymentSuccess(data);
} else if (data.status === 'failed') {
clearInterval(poll);
handlePaymentFailure(data);
} else if (data.status === 'expired') {
clearInterval(poll);
handleExpiredQR();
} else if (attempts >= maxAttempts) {
clearInterval(poll);
// Max attempts reached, rely on webhook
}
} catch (error) {
console.error('Status check error:', error);
}
}, interval);

return poll;
}

4. Handle Network Issuesโ€‹

async function createChargeWithRetry(orderData, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch('/api/create-wechat-mpm-charge', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(orderData)
});

if (response.ok) {
return await response.json();
}
} catch (error) {
if (i === maxRetries - 1) {
throw new Error('Failed to create charge after multiple attempts');
}
// Wait before retry
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
}
}
}

5. Provide Cancel/Regenerate Optionsโ€‹

function showPaymentControls(chargeId) {
return `
<div class="payment-controls">
<button onclick="cancelPayment('${chargeId}')" class="btn-cancel">
Cancel Payment
</button>
<button onclick="regenerateQR()" class="btn-regenerate">
New QR Code
</button>
</div>
`;
}

async function cancelPayment(chargeId) {
// Note: Cannot cancel pending charge, but can expire/void
if (confirm('Cancel this payment and return to checkout?')) {
// Clear display and return to order entry
clearPaymentScreen();
returnToOrderEntry();
}
}

6. Log All Transactionsโ€‹

function logTransaction(charge, status) {
const logEntry = {
timestamp: new Date().toISOString(),
charge_id: charge.id,
order_id: charge.metadata?.order_id,
terminal_id: charge.metadata?.terminal_id,
amount: charge.amount,
currency: charge.currency,
status: status,
failure_code: charge.failure_code || null
};

// Store in local DB for reconciliation
saveToLocalLog(logEntry);

// Send to central logging system
sendToLoggingService(logEntry);
}

FAQโ€‹

What is Merchant-Presented Mode (MPM)?

MPM, also known as "C scan B" (Customer scans Business), is an offline payment method where the merchant displays a QR code and the customer scans it with their wallet app. This is the standard flow for in-store WeChat Pay transactions where the merchant controls the checkout experience.

What's the difference between WeChat Pay MPM and WeChat Pay (online)?

WeChat Pay (online) is designed for e-commerce and web checkout, where customers are redirected to WeChat to complete payment. WeChat Pay MPM is designed for physical point-of-sale environments where a QR code is displayed on a terminal and the customer scans it in-store.

FeatureWeChat Pay (Online)WeChat Pay MPM
Use caseE-commerceIn-store POS
FlowRedirectOffline QR
Source typewechat_paywechat_pay_mpm
What's the difference between MPM and UPM?

In MPM (Merchant-Presented Mode), the merchant displays a QR code and the customer scans it. In UPM (User-Presented Mode), the customer displays a barcode from their WeChat app and the merchant scans it with a barcode scanner. UPM is typically faster but requires barcode scanning hardware.

How long is the QR code valid?

QR codes expire after 2 hours by default. You can customize the expiration time between 1 minute and 2 hours when creating the source. Always display a countdown timer to inform staff and customers of the remaining time.

Can I void or refund MPM transactions?

Yes. WeChat Pay MPM 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 Pay wallet.

Do I need special hardware for MPM?

No special hardware is required for MPM. You can display the QR code on any screen (POS terminal, tablet, monitor) or even print it. The customer uses their own phone with the WeChat app to scan and pay.