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.
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โ
| Region | Currency | Min Amount | Max Amount | API Version |
|---|---|---|---|---|
| Thailand | THB | 20.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.
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:
- Merchant creates a charge via Omise API with source type
wechat_pay_mpm - QR code is displayed on POS terminal, tablet, or printed
- Customer opens WeChat app and navigates to "Scan"
- Customer scans the QR code displayed by merchant
- Customer reviews transaction details and confirms payment
- Merchant receives webhook notification confirming payment
- 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.
- cURL
- Node.js
- PHP
- Python
# 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"
const omise = require('omise')({
secretKey: 'skey_test_YOUR_SECRET_KEY'
});
// Create source
const source = await omise.sources.create({
type: 'wechat_pay_mpm',
amount: 150000, // THB 1,500.00
currency: 'THB'
});
// Create charge
const charge = await omise.charges.create({
amount: 150000,
currency: 'THB',
source: source.id,
metadata: {
order_id: 'POS-2024-001',
terminal_id: 'TERMINAL-01'
}
});
console.log('QR Code URL:', charge.source.scannable_code.image.download_uri);
<?php
// Create source
$source = OmiseSource::create([
'type' => 'wechat_pay_mpm',
'amount' => 150000,
'currency' => 'THB'
]);
// Create charge
$charge = OmiseCharge::create([
'amount' => 150000,
'currency' => 'THB',
'source' => $source['id'],
'metadata' => [
'order_id' => 'POS-2024-001',
'terminal_id' => 'TERMINAL-01'
]
]);
$qrCodeUrl = $charge['source']['scannable_code']['image']['download_uri'];
?>
import omise
omise.api_secret = 'skey_test_YOUR_SECRET_KEY'
# Create source
source = omise.Source.create(
type='wechat_pay_mpm',
amount=150000,
currency='THB'
)
# Create charge
charge = omise.Charge.create(
amount=150000,
currency='THB',
source=source.id,
metadata={
'order_id': 'POS-2024-001',
'terminal_id': 'TERMINAL-01'
}
)
qr_code_url = charge.source.scannable_code.image.download_uri
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:
| Requirement | Recommendation |
|---|---|
| Minimum size | 3cm x 3cm (1.2" x 1.2") |
| Recommended size | 5cm x 5cm (2" x 2") or larger |
| Resolution | 300 DPI for printing |
| Contrast | Dark QR on white background |
| Quiet zone | Minimum 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โ
| Setting | Value |
|---|---|
| Minimum | 1 minute |
| Maximum | 2 hours |
| Default | 2 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()
});
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โ
| Status | Description |
|---|---|
pending | QR code displayed, awaiting customer scan and payment |
successful | Payment completed successfully |
failed | Payment failed or declined |
expired | QR code expired before payment completed |
reversed | Payment was reversed (voided) |
Failure Codesโ
| Code | Description | Recommended Action |
|---|---|---|
payment_expired | QR code expired before customer completed payment | Generate new QR code |
payment_rejected | Payment rejected by WeChat/issuing bank | Ask customer to check WeChat wallet |
insufficient_fund | Customer has insufficient balance | Ask customer to top up or use another method |
failed_processing | General processing failure | Retry or contact support |
invalid_account | Customer's WeChat Pay account issue | Ask 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โ
- cURL
- Node.js
- PHP
# 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"
// Full refund
const fullRefund = await omise.charges.createRefund('chrg_test_xxx', {
amount: 150000 // Full amount
});
// Partial refund
const partialRefund = await omise.charges.createRefund('chrg_test_xxx', {
amount: 50000 // Partial amount (THB 500.00)
});
<?php
// Full refund
$charge = OmiseCharge::retrieve('chrg_test_xxx');
$refund = $charge->refunds()->create([
'amount' => 150000
]);
// Partial refund
$partialRefund = $charge->refunds()->create([
'amount' => 50000
]);
?>
Refund Policyโ
| Feature | Details |
|---|---|
| Refund window | 90 days from transaction date |
| Full refunds | Supported |
| Partial refunds | Supported |
| Multiple refunds | Supported (up to original amount) |
| Processing time | 1-3 business days |
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.
| Feature | WeChat Pay (Online) | WeChat Pay MPM |
|---|---|---|
| Use case | E-commerce | In-store POS |
| Flow | Redirect | Offline QR |
| Source type | wechat_pay | wechat_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.
Related Resourcesโ
- WeChat Pay (Online) - For e-commerce and web checkout
- WeChat Pay UPM - User-Presented Mode (merchant scans customer)
- QR Payments Overview - All QR payment methods
- Webhooks - Handle payment event notifications
- Testing - Test WeChat Pay MPM integration
- Refunds - Refund policies and procedures