WeChat Pay MPM
加盟店がPOS端末で表示するQRコードを顧客がスキャンするMerchant-Presented Mode(MPM)を使用して、WeChat Payによるオフライン店舗決済を受け付けます。
オンライン決済についてはWeChat Payをご覧ください。顧客が提示するバーコードのスキャンについてはWeChat Pay UPMをご覧ください。
概要
WeChat Pay Merchant-Presented Mode(MPM)は、「C scan B」(顧客が加盟店をスキャン)とも呼ばれ、加盟店が各取引に対して固有のQRコードを生成・表示するオフライン決済方法です。顧客はWeChatアプリを使用してこのQRコードをスキャンし、決済を完了します。
このフローは、店舗小売環境、レストラン、加盟店がチェックアウトプロセスを管理し、端末、タブレット、または印刷されたレシートに決済用QRコードを表示するあらゆる実店舗のPOSに最適です。
主な特徴:
- ✅ 店舗決済 - 小売店、レストラン、実店舗に最適
- ✅ 顧客アプリのインストール不要 - 既存のWeChatアプリで動作(13億人以上のユーザー)
- ✅ 迅速なチェックアウト - 顧客は数秒でスキャンして支払い
- ✅ ハードウェア不要 - 任意の画面でQRを表示または印刷
- ✅ クロスボーダー - 中国人観光客からの決済を受け付け
- ✅ オフライン対応 - 顧客のインターネット接続が限られた環境でも動作
対応地域
| 地域 | 通貨 | 最小金額 | 最大金額 | APIバージョン |
|---|---|---|---|---|
| タイ | THB | 20.00(2,000サタン) | 150,000.00(15,000,000サタン) | 2017-11-02 |
WeChat Pay MPMを有効にするには、support@omise.coにメールでこの機能をリクエストしてください。
WeChat Pay MPMは、中国人観光客をターゲットとする加盟店にとって特に価値があります。顧客はWeChatウォレットからCNYで支払い、加盟店はTHBで決済を受け取ります。
仕組み
決済フロー:
- 加盟店がOmise APIを通じてソースタイプ
wechat_pay_mpmで課金を作成 - QRコードがPOS端末、タブレット、または印刷物に表示される
- 顧客がWeChatアプリを開き、「スキャン」に移動
- 顧客が加盟店が表示するQRコードをスキャン
- 顧客が取引詳細を確認し、支払いを承認
- 加盟店が支払い確認のWebhook通知を受信
- 加盟店が顧客にレシートまたは確認を提供
一般的な完了時間: 30秒〜2分
実装
ステップ1:ソース付き課金の作成
WeChat Pay MPMでは、ソースと課金を単一のAPI呼び出しで作成するか、個別に作成できます。
- 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
レスポンス:
{
"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"
}
ステップ2:POSにQRコードを表示
charge.source.scannable_code.image.download_uriからQRコードをPOS端末に表示します。
// 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.');
}
}
ステップ3:Webhook通知の処理
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コード表示
表示要件
スキャン成功率を最適化するため、以下のガイドラインに従ってください:
| 要件 | 推奨事項 |
|---|---|
| 最小サイズ | 3cm x 3cm(1.2" x 1.2") |
| 推奨サイズ | 5cm x 5cm(2" x 2")以上 |
| 解像度 | 印刷用300 DPI |
| コントラスト | 白背景に暗いQR |
| 余白 | 最小4モジュールの白 い境界線 |
POS端末の例
<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>
課金有効期限の設定
デフォルトでは、WeChat Pay MPM課金は2時間後に期限切れになります。ソース作成時に有効期限をカスタマイズできます。
カスタム有効期限
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"
有効期限の制限
| 設定 | 値 |
|---|---|
| 最小 | 1分 |
| 最大 | 2時間 |
| デフォルト | 2時間 |
カスタム有効期限の実装
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()
});
混雑する小売環境では、顧客が期限切れのQRコードをスキャンしようとするのを防ぐため、より短い有効期限(5〜15分)を検討してください。常に残り時間を表示するカウントダウンタイマーを表示してください。
課金ステータス値
| ステータス | 説明 |
|---|---|
pending | QRコードが表示され、顧客のスキャンと支払いを待機中 |
successful | 支払いが正常に完了 |
failed | 支払いが失敗または拒否 |
expired | 支払い完了前にQRコードが期限切れ |
reversed | 支払いが取り消された(無効化) |
失敗コード
| コード | 説明 | 推奨アクション |
|---|---|---|
payment_expired | 顧客が支払いを完了する前にQRコードが期限切れ | 新しいQRコードを生成 |
payment_rejected | WeChat/発行銀行により支払いが拒否 | 顧客にWeChatウォレットの確認を依頼 |
insufficient_fund | 顧客の残高が不足 | 顧客にチャージまたは別の支払い方法の使用を依頼 |
failed_processing | 一般的な処理エラー | 再試行またはサポートに連絡 |
invalid_account | 顧客のWeChat Payアカウントの問題 | 顧客にアカウントステータスの確認を依頼 |
失敗の処理
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);
});
返金
WeChat Pay MPMは、元の取引から90日以内の全額返金および一部返金をサポートしています。
返金の作成
- 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
]);
?>
返金ポリシー
| 項目 | 詳細 |
|---|---|
| 返金期間 | 取引日から90日 |
| 全額返金 | 対応 |
| 一部返金 | 対応 |
| 複数回返金 | 対応(元の金額まで) |
| 処理時間 | 1〜3営業日 |
返金は顧客のWeChat Payウォレットに戻されます。返金が処理されると、顧客はWeChatアプリで通知を受け取ります。
POS統合のベストプラクティス
1. 明確なQRコードを表示
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. カウントダウンタイマーの実装
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. 支払いステータスのポーリング(バックアップ)
Webhookが主要な通知方法ですが、バックアップとしてポーリングを実装してください:
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. ネットワーク問題の処理
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. キャンセル/再生成オプションの提供
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. すべての取引をログ記録
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);
}
よくある質問
Merchant-Presented Mode(MPM)とは何ですか?
MPMは「C scan B」(顧客が加盟店をスキャン)とも呼ばれ、加盟店がQRコードを表示し、顧客がウォレットアプリでそれをスキャンするオフライン決済方法です。これは、加盟店がチェックアウト体験を管理する店舗内WeChat Pay取引の標準的なフローです。
WeChat Pay MPMとWeChat Pay(オンライン)の違いは何ですか?
WeChat Pay(オンライン)は、顧客が支払いを完了するためにWeChatにリダイレクトされるeコマースとWebチェックアウト用に設計されています。WeChat Pay MPMは、端末にQRコードが表示され、顧客が店内でスキャンする実店舗のPOS環境用に設計されています。
| 機能 | WeChat Pay(オンライン) | WeChat Pay MPM |
|---|---|---|
| ユースケース | eコマース | 店舗POS |
| フロー | リダイレクト | オフラインQR |
| ソースタイプ | wechat_pay | wechat_pay_mpm |
MPMとUPMの違いは何ですか?
MPM(Merchant-Presented Mode)では、加盟店がQRコードを表示し、顧客がそれをスキャンします。UPM(User-Presented Mode)では、顧客がWeChatアプリからバーコードを表示し、加盟店がバーコードスキャナーでそれをスキャンします。UPMは通常より高速です が、バーコードスキャンハードウェアが必要です。
QRコードの有効期限はどのくらいですか?
QRコードはデフォルトで2時間後に期限切れになります。ソース作成時に1分から2時間の間で有効期限をカスタマイズできます。残り時間をスタッフと顧客に知らせるため、常にカウントダウンタイマーを表示してください。
MPM取引を無効化または返金できますか?
はい。WeChat Pay MPMは元の取引から90日以内の全額返金と一部返金の両方をサポートしています。返金は1〜3営業日以内に処理され、顧客のWeChat Payウォレットに戻されます。
MPMには特別なハードウェアが必要ですか?
MPMには特別なハードウェアは必要ありません。任意の画面(POS端末、タブレット、モニター)でQRコードを表示するか、印刷することもできます。顧客は自分のスマートフォンとWeChatアプリを使用してスキャンし、支払います。
関連リソース
- WeChat Pay(オンライン) - eコマースとWebチェックアウト 用
- WeChat Pay UPM - User-Presented Mode(加盟店が顧客をスキャン)
- QR決済概要 - すべてのQR決済方法
- Webhook - 決済イベント通知の処理
- テスト - WeChat Pay MPM統合のテスト
- 返金 - 返金ポリシーと手順