WeChat Pay UPM
Accept in-store payments from WeChat Pay users by scanning the customer's payment barcode through your point-of-sale system (POS).
For online payments, see WeChat Pay. For merchant-displayed QR codes (C scan B), see WeChat Pay MPM.
Overviewโ
WeChat Pay User-Presented Mode (UPM) is an offline payment method where the merchant scans a barcode displayed by the customer in their WeChat app. Also known as "B scan C" (Business scans Customer), this method is ideal for high-volume retail environments where the merchant has a barcode scanner at their point-of-sale terminal.
Key Features:
- Fast checkout - Scan and process in seconds
- Offline flow - No redirect required, barcode-based payment
- Large user base - Access to WeChat's 1.3B+ users
- Familiar experience - Similar to scanning product barcodes
- Cross-border - Accept payments from Chinese tourists
- POS integration - Works with existing barcode scanners
Supported Regionsโ
| Region | Currency | Min Amount | Max Amount | API Version |
|---|---|---|---|---|
| Thailand | THB | 20.00 | 150,000.00 | 2017-11-02 |
WeChat Pay UPM 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:
- Customer opens WeChat app on their phone
- Customer navigates to "Pay" and displays their payment barcode
- Merchant scans the barcode using their POS terminal
- Charge is created with the scanned barcode value
- Customer confirms payment in the WeChat app (may require PIN/biometric)
- Payment is processed and both parties receive confirmation
Typical completion time: 10-30 seconds
Implementationโ
Create Charge with Barcodeโ
For UPM, you create the charge directly with the scanned barcode value. Unlike other payment methods, there is no separate source creation step - the source is created inline with the charge.
- cURL
- Node.js
- PHP
- Python
- Ruby
- Go
- Java
- C#
curl https://api.omise.co/charges \
-u $OMISE_SECRET_KEY: \
-d "amount=150000" \
-d "currency=THB" \
-d "source[type]=wechat_pay_upm" \
-d "source[barcode]=130991292552725093"
const omise = require('omise')({
secretKey: 'skey_test_YOUR_SECRET_KEY'
});
const charge = await omise.charges.create({
amount: 150000, // THB 1,500.00
currency: 'THB',
source: {
type: 'wechat_pay_upm',
barcode: '130991292552725093' // Scanned from customer's WeChat
}
});
console.log('Charge status:', charge.status);
console.log('Charge ID:', charge.id);
<?php
$charge = OmiseCharge::create([
'amount' => 150000,
'currency' => 'THB',
'source' => [
'type' => 'wechat_pay_upm',
'barcode' => '130991292552725093'
]
]);
echo "Status: " . $charge['status'];
echo "Charge ID: " . $charge['id'];
?>
import omise
omise.api_secret = 'skey_test_YOUR_SECRET_KEY'
charge = omise.Charge.create(
amount=150000,
currency='THB',
source={
'type': 'wechat_pay_upm',
'barcode': '130991292552725093'
}
)
print(f"Status: {charge.status}")
print(f"Charge ID: {charge.id}")
require 'omise'
Omise.api_key = 'skey_test_YOUR_SECRET_KEY'
charge = Omise::Charge.create({
amount: 150000,
currency: 'THB',
source: {
type: 'wechat_pay_upm',
barcode: '130991292552725093'
}
})
puts "Status: #{charge.status}"
puts "Charge ID: #{charge.id}"
charge, err := client.Charges().Create(&operations.CreateCharge{
Amount: 150000,
Currency: "THB",
Source: &operations.Source{
Type: "wechat_pay_upm",
Barcode: "130991292552725093",
},
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Status: %s\n", charge.Status)
Map<String, Object> sourceParams = new HashMap<>();
sourceParams.put("type", "wechat_pay_upm");
sourceParams.put("barcode", "130991292552725093");
Charge charge = client.charges().create(new Charge.CreateParams()
.amount(150000L)
.currency("THB")
.source(sourceParams));
System.out.println("Status: " + charge.getStatus());
System.out.println("Charge ID: " + charge.getId());
var charge = await client.Charges.Create(new CreateChargeRequest
{
Amount = 150000,
Currency = "THB",
Source = new PaymentSource
{
Type = "wechat_pay_upm",
Barcode = "130991292552725093"
}
});
Console.WriteLine($"Status: {charge.Status}");
Console.WriteLine($"Charge ID: {charge.Id}");
Response:
{
"object": "charge",
"id": "chrg_test_5rt6s9vah5lkvi1rh9d",
"amount": 150000,
"currency": "THB",
"status": "pending",
"source": {
"object": "source",
"id": "src_test_5rt6s9vah5lkvi1rh9c",
"type": "wechat_pay_upm",
"flow": "offline",
"barcode": "130991292552725093"
},
"created_at": "2024-01-15T10:30:00Z"
}
Handle Charge Responseโ
app.post('/pos/wechat-payment', async (req, res) => {
const { amount, barcode, order_id } = req.body;
// Validate amount
if (amount < 2000 || amount > 15000000) {
return res.status(400).json({
success: false,
error: 'Amount must be between 20 and 150,000'
});
}
// Validate barcode format
if (!barcode || barcode.length < 10) {
return res.status(400).json({
success: false,
error: 'Invalid barcode'
});
}
try {
const charge = await omise.charges.create({
amount: amount,
currency: 'THB',
source: {
type: 'wechat_pay_upm',
barcode: barcode
},
metadata: {
order_id: order_id,
pos_terminal: 'POS-001'
}
});
if (charge.status === 'successful') {
// Payment successful immediately
printReceipt(charge);
res.json({
success: true,
charge_id: charge.id,
status: 'successful'
});
} else if (charge.status === 'pending') {
// Customer needs to confirm in WeChat app
// Wait for webhook notification
res.json({
success: true,
charge_id: charge.id,
status: 'pending',
message: 'Please ask customer to confirm payment in WeChat'
});
} else {
res.json({
success: false,
status: charge.status,
message: charge.failure_message || 'Payment failed'
});
}
} catch (error) {
console.error('WeChat Pay UPM error:', error);
res.status(500).json({
success: false,
error: error.message
});
}
});
Handle Webhook Notificationโ
app.post('/webhooks/omise', (req, res) => {
const event = req.body;
if (event.key === 'charge.complete') {
const charge = event.data;
if (charge.source && charge.source.type === 'wechat_pay_upm') {
if (charge.status === 'successful') {
// Payment confirmed
notifyPOS(charge.id, 'success');
updateOrderStatus(charge.metadata.order_id, 'paid');
console.log(`WeChat Pay UPM successful: ${charge.id}`);
} else if (charge.status === 'failed') {
// Payment failed
notifyPOS(charge.id, 'failed', charge.failure_message);
updateOrderStatus(charge.metadata.order_id, 'failed');
console.log(`WeChat Pay UPM failed: ${charge.id}, reason: ${charge.failure_message}`);
}
}
}
res.status(200).send('OK');
});
Barcode Scanner Integrationโ
Scanner Requirementsโ
Any standard barcode scanner compatible with your POS system can be used for WeChat Pay UPM. The scanner needs to read the numeric barcode from the customer's WeChat app.
Compatible scanners:
- USB barcode scanners (1D/2D)
- Bluetooth barcode scanners
- Built-in POS scanners
- Mobile device cameras with scanning software
Barcode Formatโ
WeChat payment barcodes are typically:
- 18-digit numeric codes
- Start with specific prefixes (10, 11, 12, 13, 14, 15)
- Valid for a limited time (usually 1 minute)
// Validate WeChat barcode format
function isValidWeChatBarcode(barcode) {
// WeChat barcodes are typically 18 digits
// and start with specific prefixes
const validPrefixes = ['10', '11', '12', '13', '14', '15'];
if (!/^\d{18}$/.test(barcode)) {
return false;
}
const prefix = barcode.substring(0, 2);
return validPrefixes.includes(prefix);
}
POS Integration Exampleโ
// POS terminal integration
class WeChatPayUPM {
constructor(omise) {
this.omise = omise;
}
async processPayment(barcode, amount, orderId) {
// Validate inputs
if (!this.validateBarcode(barcode)) {
return { success: false, error: 'Invalid barcode format' };
}
if (!this.validateAmount(amount)) {
return { success: false, error: 'Invalid amount' };
}
try {
const charge = await this.omise.charges.create({
amount: amount,
currency: 'THB',
source: {
type: 'wechat_pay_upm',
barcode: barcode
},
metadata: {
order_id: orderId,
timestamp: new Date().toISOString()
}
});
return {
success: true,
chargeId: charge.id,
status: charge.status,
requiresConfirmation: charge.status === 'pending'
};
} catch (error) {
return {
success: false,
error: error.message
};
}
}
validateBarcode(barcode) {
return /^\d{18}$/.test(barcode);
}
validateAmount(amount) {
return amount >= 2000 && amount <= 15000000;
}
}
// Usage
const pos = new WeChatPayUPM(omise);
const result = await pos.processPayment('130991292552725093', 150000, 'ORD-12345');
if (result.success) {
if (result.requiresConfirmation) {
displayMessage('Please ask customer to confirm in WeChat app');
waitForWebhook(result.chargeId);
} else {
printReceipt(result.chargeId);
}
} else {
displayError(result.error);
}
Charge Status Valuesโ
| Status | Description |
|---|---|
pending | Charge created, awaiting customer confirmation in WeChat app |
successful | Payment completed successfully |
failed | Payment declined or failed |
expired | Charge expired (24 hours without authorization) |
WeChat Pay UPM charges expire after 24 hours if not authorized by the customer. For in-store payments, this typically indicates the customer abandoned the transaction.
Failure Codesโ
| Code | Description | Recommended Action |
|---|---|---|
payment_expired | Payment authorization expired | Create a new charge |
payment_rejected | Payment rejected by WeChat/bank | Ask customer to try again or use different payment method |
insufficient_fund | Insufficient funds in wallet | Ask customer to top up or use different payment method |
failed_processing | General processing error | Retry the transaction |
invalid_barcode | Barcode is invalid or expired | Ask customer to refresh their payment code |
// Handle failure codes
function handlePaymentFailure(charge) {
const failureCode = charge.failure_code;
const failureMessage = charge.failure_message;
const errorMessages = {
'payment_expired': 'Payment expired. Please try again.',
'payment_rejected': 'Payment was declined. Please try a different payment method.',
'insufficient_fund': 'Insufficient funds. Please top up your WeChat wallet.',
'failed_processing': 'Processing error. Please try again.',
'invalid_barcode': 'Invalid barcode. Please refresh your payment code in WeChat.'
};
return errorMessages[failureCode] || failureMessage || 'Payment failed. Please try again.';
}
Refund Supportโ
WeChat Pay UPM supports full and partial refunds within 90 days of the original transaction.
Full Refundโ
// Full refund
const refund = await omise.charges.createRefund('chrg_test_...', {
amount: 150000 // Full amount
});
console.log('Refund ID:', refund.id);
console.log('Refund status:', refund.status);
Partial Refundโ
// Partial refund
const partialRefund = await omise.charges.createRefund('chrg_test_...', {
amount: 50000 // Partial amount
});
Refund in cURLโ
# 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"
- Refunds are processed within 1-3 business days
- Customers receive refunds in their WeChat wallet
- Multiple partial refunds are allowed up to the original charge amount
Complete Implementation Exampleโ
// Express.js server with WeChat Pay UPM
const express = require('express');
const omise = require('omise')({
secretKey: process.env.OMISE_SECRET_KEY
});
const app = express();
app.use(express.json());
// Store pending transactions
const pendingTransactions = new Map();
// Process WeChat Pay UPM payment
app.post('/api/pos/wechat-upm', async (req, res) => {
const { barcode, amount, order_id, terminal_id } = req.body;
// Validate barcode
if (!barcode || !/^\d{18}$/.test(barcode)) {
return res.status(400).json({
success: false,
error: 'Invalid WeChat payment barcode'
});
}
// Validate amount (THB 20 - THB 150,000)
if (amount < 2000 || amount > 15000000) {
return res.status(400).json({
success: false,
error: 'Amount must be between 20 and 150,000'
});
}
try {
const charge = await omise.charges.create({
amount: amount,
currency: 'THB',
source: {
type: 'wechat_pay_upm',
barcode: barcode
},
metadata: {
order_id: order_id,
terminal_id: terminal_id,
payment_method: 'wechat_pay_upm'
}
});
// Handle immediate success
if (charge.status === 'successful') {
await processSuccessfulPayment(charge);
return res.json({
success: true,
charge_id: charge.id,
status: 'successful',
message: 'Payment successful'
});
}
// Handle pending (customer needs to confirm)
if (charge.status === 'pending') {
pendingTransactions.set(charge.id, {
order_id: order_id,
terminal_id: terminal_id,
created_at: new Date()
});
return res.json({
success: true,
charge_id: charge.id,
status: 'pending',
message: 'Ask customer to confirm payment in WeChat app'
});
}
// Handle failure
return res.json({
success: false,
charge_id: charge.id,
status: charge.status,
error: charge.failure_message || 'Payment failed'
});
} catch (error) {
console.error('WeChat Pay UPM error:', error);
return res.status(500).json({
success: false,
error: error.message
});
}
});
// Check payment status (for polling)
app.get('/api/pos/check-status/:chargeId', async (req, res) => {
try {
const charge = await omise.charges.retrieve(req.params.chargeId);
res.json({
charge_id: charge.id,
status: charge.status,
paid: charge.paid,
failure_message: charge.failure_message
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Webhook handler
app.post('/webhooks/omise', async (req, res) => {
const event = req.body;
if (event.key === 'charge.complete') {
const charge = event.data;
if (charge.source && charge.source.type === 'wechat_pay_upm') {
const pendingTx = pendingTransactions.get(charge.id);
if (charge.status === 'successful') {
await processSuccessfulPayment(charge);
// Notify POS terminal
if (pendingTx) {
notifyTerminal(pendingTx.terminal_id, {
type: 'payment_success',
charge_id: charge.id,
order_id: pendingTx.order_id
});
}
} else if (charge.status === 'failed') {
// Notify POS terminal of failure
if (pendingTx) {
notifyTerminal(pendingTx.terminal_id, {
type: 'payment_failed',
charge_id: charge.id,
order_id: pendingTx.order_id,
error: charge.failure_message
});
}
}
pendingTransactions.delete(charge.id);
}
}
res.status(200).send('OK');
});
// Process refund
app.post('/api/pos/refund', async (req, res) => {
const { charge_id, amount } = req.body;
try {
const refund = await omise.charges.createRefund(charge_id, {
amount: amount
});
res.json({
success: true,
refund_id: refund.id,
status: refund.status
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
});
// Helper functions
async function processSuccessfulPayment(charge) {
await updateOrderStatus(charge.metadata.order_id, 'paid');
await logTransaction(charge);
}
function notifyTerminal(terminalId, message) {
// Implementation depends on your POS system
// Could use WebSocket, polling, or push notifications
}
async function updateOrderStatus(orderId, status) {
// Update order in your database
}
async function logTransaction(charge) {
// Log transaction for reporting
}
app.listen(3000, () => {
console.log('WeChat Pay UPM server running on port 3000');
});
Best Practicesโ
1. Train Staff on the Payment Flowโ
Ensure staff understand the UPM payment flow:
- Ask customer to open WeChat and display payment code
- Scan the barcode quickly (codes expire after ~1 minute)
- Wait for confirmation or ask customer to approve in app
2. Handle Barcode Expirationโ
WeChat payment barcodes refresh automatically but expire quickly:
// If barcode scan fails, ask customer to refresh
if (charge.failure_code === 'invalid_barcode') {
displayMessage('Please ask customer to refresh their WeChat payment code');
}
3. Implement Timeout Handlingโ
// Set timeout for pending payments
const PAYMENT_TIMEOUT = 60000; // 60 seconds
async function waitForPayment(chargeId) {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error('Payment confirmation timeout'));
}, PAYMENT_TIMEOUT);
// Listen for webhook or poll
onPaymentComplete(chargeId, (result) => {
clearTimeout(timeout);
resolve(result);
});
});
}
4. Display Clear Status Messagesโ
function getStatusMessage(status, failureCode) {
const messages = {
'pending': 'Waiting for customer confirmation...',
'successful': 'Payment successful!',
'failed': getFailureMessage(failureCode),
'expired': 'Payment expired. Please try again.'
};
return messages[status] || 'Unknown status';
}
5. Use Both Webhook and Pollingโ
For reliable payment confirmation, use webhooks as primary and polling as backup:
class PaymentMonitor {
constructor(chargeId) {
this.chargeId = chargeId;
this.resolved = false;
}
async monitor() {
// Start polling as backup
const pollInterval = setInterval(async () => {
if (this.resolved) {
clearInterval(pollInterval);
return;
}
const charge = await checkChargeStatus(this.chargeId);
if (charge.status !== 'pending') {
this.resolved = true;
clearInterval(pollInterval);
this.onComplete(charge);
}
}, 3000);
// Timeout after 2 minutes
setTimeout(() => {
if (!this.resolved) {
clearInterval(pollInterval);
this.onTimeout();
}
}, 120000);
}
}
6. Validate Amount Limitsโ
function validateWeChatUPMAmount(amount) {
const MIN_AMOUNT = 2000; // THB 20.00
const MAX_AMOUNT = 15000000; // THB 150,000.00
if (amount < MIN_AMOUNT) {
return { valid: false, error: `Minimum amount is 20.00` };
}
if (amount > MAX_AMOUNT) {
return { valid: false, error: `Maximum amount is 150,000.00` };
}
return { valid: true };
}
FAQโ
What is WeChat Pay UPM?
WeChat Pay UPM (User-Presented Mode), also known as "B scan C" (Business scans Customer), is an in-store payment method where the customer displays a payment barcode in their WeChat app and the merchant scans it using a barcode scanner. This is the reverse of MPM (Merchant-Presented Mode) where the customer scans the merchant's QR code.
What's the difference between WeChat Pay UPM and MPM?
| Feature | UPM (User-Presented Mode) | MPM (Merchant-Presented Mode) |
|---|---|---|
| Who displays code | Customer shows barcode | Merchant displays QR code |
| Who scans | Merchant scans customer's barcode | Customer scans merchant's QR |
| Speed | Faster (5-15 seconds) | Slower (30-60 seconds) |
| Hardware needed | Barcode scanner required | No hardware needed |
| Best for | High-volume retail, quick service | Casual retail, restaurants |
UPM is typically faster because it requires fewer steps from the customer.
What type of barcode scanner do I need?
Any standard 1D/2D barcode scanner that can read numeric barcodes will work. Most modern POS systems include compatible scanners. The scanner needs to read the 18-digit numeric barcode displayed in the customer's WeChat app. USB, Bluetooth, and integrated POS scanners are all compatible.
How long is the customer's payment barcode valid?
WeChat payment barcodes automatically refresh and are typically valid for about 1 minute. If scanning fails with an "invalid_barcode" error, ask the customer to refresh their payment code by pulling down on the screen in WeChat's payment section.
Why is my charge stuck in "pending" status?
A pending status means the customer needs to confirm the payment in their WeChat app. This may happen for larger transactions or based on the customer's security settings. Ask the customer to check their WeChat app for a payment confirmation prompt. The charge will expire after 24 hours if not confirmed.
Can I refund WeChat Pay UPM transactions?
Yes, WeChat Pay UPM 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.
Testingโ
Test Modeโ
WeChat Pay UPM can be tested using your test API keys:
Test Flow:
- Create charge with test API keys and a test barcode
- In test mode, use the Omise Dashboard to simulate payment completion
- Verify webhook handling
Test Barcode:
# Use a valid format test barcode
curl https://api.omise.co/charges \
-u skey_test_YOUR_SECRET_KEY: \
-d "amount=150000" \
-d "currency=THB" \
-d "source[type]=wechat_pay_upm" \
-d "source[barcode]=130000000000000001"
Testing Scenarios:
- Successful payment: Verify POS receives confirmation
- Failed payment: Test error handling and retry logic
- Pending status: Test customer confirmation flow
- Timeout: Test expired charge handling
- Refunds: Test full and partial refund flows
Important Notes:
- Test mode does not connect to real WeChat servers
- Use Omise Dashboard "Actions" to mark charges as successful/failed
- Always test webhook handling before going live
- Test with various amount values (min/max limits)
For comprehensive testing guidelines, see the Testing Documentation.
Related Resourcesโ
- WeChat Pay - Online WeChat Pay payments
- WeChat Pay MPM - Merchant-Presented Mode (customer scans)
- QR Payments Overview - All QR payment methods
- Alipay+ UPM - Similar UPM flow for Alipay+
- Webhooks - Handle payment events
- Refunds - Refund policies and procedures
- Testing - Test WeChat Pay UPM integration