メインコンテンツへスキップ

WeChat Pay UPM

POSシステムを通じて顧客の決済バーコードをスキャンし、WeChat Payユーザーからの店舗内決済を受け付けます。

他のWeChat Payオプション

オンライン決済については、WeChat Payをご覧ください。加盟店提示型QRコード(C scan B)については、WeChat Pay MPMをご覧ください。

概要

WeChat Pay User-Presented Mode(UPM)は、加盟店が顧客のWeChatアプリに表示されたバーコードをスキャンするオフライン決済方式です。「B scan C」(Business scans Customer)とも呼ばれ、POSターミナルにバーコードスキャナーを備えた大量取引の小売環境に最適です。

主な特徴:

  • 高速な精算 - 数秒でスキャンと処理が完了
  • オフラインフロー - リダイレクト不要、バーコードベースの決済
  • 大規模なユーザーベース - WeChatの13億人以上のユーザーにアクセス
  • 馴染みのある体験 - 商品バーコードのスキャンと同様
  • クロスボーダー - 中国人観光客からの決済を受け付け
  • POS連携 - 既存のバーコードスキャナーで動作

対応地域

地域通貨最小金額最大金額APIバージョン
タイTHB20.00150,000.002017-11-02
クロスボーダー決済

WeChat Pay UPMは、中国人観光客をターゲットとする加盟店にとって特に価値があります。顧客はWeChatウォレットからCNYで支払い、加盟店はTHBで決済を受け取ります。

仕組み

決済フロー:

  1. 顧客がスマートフォンでWeChatアプリを開く
  2. 顧客が「Pay」に移動し、決済バーコードを表示
  3. 加盟店がPOSターミナルでバーコードをスキャン
  4. スキャンしたバーコード値で課金を作成
  5. 顧客がWeChatアプリで決済を確認(PIN/生体認証が必要な場合あり)
  6. 決済が処理され、双方が確認を受け取る

一般的な完了時間: 10〜30秒

実装

バーコードで課金を作成

UPMでは、スキャンしたバーコード値で直接課金を作成します。他の決済方法とは異なり、別途ソース作成ステップはなく、課金と同時にインラインでソースが作成されます。

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"

レスポンス:

{
"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"
}

課金レスポンスの処理

app.post('/pos/wechat-payment', async (req, res) => {
const { amount, barcode, order_id } = req.body;

// 金額を検証
if (amount < 2000 || amount > 15000000) {
return res.status(400).json({
success: false,
error: 'Amount must be between 20 and 150,000'
});
}

// バーコード形式を検証
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') {
// 決済が即座に成功
printReceipt(charge);
res.json({
success: true,
charge_id: charge.id,
status: 'successful'
});
} else if (charge.status === 'pending') {
// 顧客がWeChatアプリで確認する必要あり
// Webhook通知を待つ
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
});
}
});

Webhook通知の処理

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') {
// 決済確認
notifyPOS(charge.id, 'success');
updateOrderStatus(charge.metadata.order_id, 'paid');
console.log(`WeChat Pay UPM successful: ${charge.id}`);
} else if (charge.status === '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');
});

バーコードスキャナー連携

スキャナー要件

POSシステムと互換性のある標準的なバーコードスキャナーであれば、WeChat Pay UPMに使用できます。スキャナーは顧客のWeChatアプリから数値バーコードを読み取る必要があります。

対応スキャナー:

  • USBバーコードスキャナー(1D/2D)
  • Bluetoothバーコードスキャナー
  • 内蔵POSスキャナー
  • スキャンソフトウェア搭載のモバイルデバイスカメラ

バーコード形式

WeChat決済バーコードの特徴:

  • 18桁の数値コード
  • 特定のプレフィックス(10、11、12、13、14、15)で始まる
  • 有効期限が限られている(通常1分)
// WeChatバーコード形式を検証
function isValidWeChatBarcode(barcode) {
// WeChatバーコードは通常18桁
// 特定のプレフィックスで始まる
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連携例

// POSターミナル連携
class WeChatPayUPM {
constructor(omise) {
this.omise = omise;
}

async processPayment(barcode, amount, orderId) {
// 入力を検証
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;
}
}

// 使用例
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);
}

課金ステータス値

ステータス説明
pending課金作成済み、WeChatアプリでの顧客確認待ち
successful決済が正常に完了
failed決済が拒否または失敗
expired課金が期限切れ(24時間以内に承認なし)
課金の有効期限

WeChat Pay UPM課金は、顧客が承認しない場合、24時間後に期限切れになります。店舗内決済の場合、これは通常、顧客が取引を中止したことを示します。

失敗コード

コード説明推奨アクション
payment_expired決済承認が期限切れ新しい課金を作成
payment_rejectedWeChat/銀行により決済が拒否された再試行または別の決済方法を依頼
insufficient_fundウォレットの残高不足チャージまたは別の決済方法を依頼
failed_processing一般的な処理エラー取引を再試行
invalid_barcodeバーコードが無効または期限切れ顧客に決済コードの更新を依頼
// 失敗コードの処理
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.';
}

返金サポート

WeChat Pay UPMは、元の取引から90日以内全額および部分返金をサポートしています。

全額返金

// 全額返金
const refund = await omise.charges.createRefund('chrg_test_...', {
amount: 150000 // 全額
});

console.log('Refund ID:', refund.id);
console.log('Refund status:', refund.status);

部分返金

// 部分返金
const partialRefund = await omise.charges.createRefund('chrg_test_...', {
amount: 50000 // 部分金額
});

cURLでの返金

# 全額返金
curl https://api.omise.co/charges/chrg_test_xxx/refunds \
-u $OMISE_SECRET_KEY: \
-d "amount=150000"

# 部分返金
curl https://api.omise.co/charges/chrg_test_xxx/refunds \
-u $OMISE_SECRET_KEY: \
-d "amount=50000"
返金タイムライン
  • 返金は1〜3営業日以内に処理されます
  • 顧客はWeChatウォレットで返金を受け取ります
  • 元の課金金額まで複数の部分返金が可能です

完全な実装例

// Express.jsサーバーとWeChat Pay UPM
const express = require('express');
const omise = require('omise')({
secretKey: process.env.OMISE_SECRET_KEY
});

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

// 保留中の取引を保存
const pendingTransactions = new Map();

// WeChat Pay UPM決済を処理
app.post('/api/pos/wechat-upm', async (req, res) => {
const { barcode, amount, order_id, terminal_id } = req.body;

// バーコードを検証
if (!barcode || !/^\d{18}$/.test(barcode)) {
return res.status(400).json({
success: false,
error: 'Invalid WeChat payment barcode'
});
}

// 金額を検証(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'
}
});

// 即座に成功を処理
if (charge.status === 'successful') {
await processSuccessfulPayment(charge);
return res.json({
success: true,
charge_id: charge.id,
status: 'successful',
message: 'Payment successful'
});
}

// 保留中を処理(顧客が確認する必要あり)
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'
});
}

// 失敗を処理
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
});
}
});

// 決済ステータスを確認(ポーリング用)
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ハンドラー
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);

// POSターミナルに通知
if (pendingTx) {
notifyTerminal(pendingTx.terminal_id, {
type: 'payment_success',
charge_id: charge.id,
order_id: pendingTx.order_id
});
}
} else if (charge.status === 'failed') {
// POSターミナルに失敗を通知
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');
});

// 返金を処理
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
});
}
});

// ヘルパー関数
async function processSuccessfulPayment(charge) {
await updateOrderStatus(charge.metadata.order_id, 'paid');
await logTransaction(charge);
}

function notifyTerminal(terminalId, message) {
// 実装はPOSシステムに依存
// WebSocket、ポーリング、またはプッシュ通知を使用可能
}

async function updateOrderStatus(orderId, status) {
// データベースの注文を更新
}

async function logTransaction(charge) {
// レポート用に取引を記録
}

app.listen(3000, () => {
console.log('WeChat Pay UPM server running on port 3000');
});

ベストプラクティス

1. スタッフに決済フローをトレーニング

スタッフがUPM決済フローを理解していることを確認:

  • 顧客にWeChatを開いて決済コードを表示するよう依頼
  • バーコードを素早くスキャン(コードは約1分で期限切れ)
  • 確認を待つか、顧客にアプリで承認するよう依頼

2. バーコードの有効期限切れを処理

WeChat決済バーコードは自動的に更新されますが、すぐに期限切れになります:

// バーコードスキャンが失敗した場合、顧客に更新を依頼
if (charge.failure_code === 'invalid_barcode') {
displayMessage('Please ask customer to refresh their WeChat payment code');
}

3. タイムアウト処理を実装

// 保留中の決済にタイムアウトを設定
const PAYMENT_TIMEOUT = 60000; // 60秒

async function waitForPayment(chargeId) {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error('Payment confirmation timeout'));
}, PAYMENT_TIMEOUT);

// Webhookをリッスンまたはポーリング
onPaymentComplete(chargeId, (result) => {
clearTimeout(timeout);
resolve(result);
});
});
}

4. 明確なステータスメッセージを表示

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. Webhookとポーリングの両方を使用

信頼性の高い決済確認のために、Webhookをプライマリ、ポーリングをバックアップとして使用:

class PaymentMonitor {
constructor(chargeId) {
this.chargeId = chargeId;
this.resolved = false;
}

async monitor() {
// バックアップとしてポーリングを開始
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);

// 2分後にタイムアウト
setTimeout(() => {
if (!this.resolved) {
clearInterval(pollInterval);
this.onTimeout();
}
}, 120000);
}
}

6. 金額制限を検証

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

WeChat Pay UPMとは何ですか?

WeChat Pay UPM(User-Presented Mode)は、「B scan C」(Business scans Customer)とも呼ばれ、顧客がWeChatアプリで決済バーコードを表示し、加盟店がバーコードスキャナーでそれをスキャンする店舗内決済方式です。これは、顧客が加盟店のQRコードをスキャンするMPM(Merchant-Presented Mode)の逆です。

WeChat Pay UPMとMPMの違いは何ですか?
機能UPM(User-Presented Mode)MPM(Merchant-Presented Mode)
コードを表示するのは顧客がバーコードを表示加盟店がQRコードを表示
スキャンするのは加盟店が顧客のバーコードをスキャン顧客が加盟店のQRをスキャン
速度より速い(5〜15秒)より遅い(30〜60秒)
必要なハードウェアバーコードスキャナーが必要ハードウェア不要
最適な用途大量取引の小売、クイックサービスカジュアルな小売、レストラン

UPMは顧客のステップが少ないため、通常より高速です。

どのタイプのバーコードスキャナーが必要ですか?

数値バーコードを読み取れる標準的な1D/2Dバーコードスキャナーであれば動作します。ほとんどの最新のPOSシステムには対応スキャナーが含まれています。スキャナーは顧客のWeChatアプリに表示される18桁の数値バーコードを読み取る必要があります。USB、Bluetooth、および統合POSスキャナーはすべて互換性があります。

顧客の決済バーコードはどのくらい有効ですか?

WeChat決済バーコードは自動的に更新され、通常約1分間有効です。「invalid_barcode」エラーでスキャンが失敗した場合、顧客にWeChatの決済セクションで画面を下にプルして決済コードを更新するよう依頼してください。

課金が「pending」ステータスのままなのはなぜですか?

保留中のステータスは、顧客がWeChatアプリで決済を確認する必要があることを意味します。これは、高額取引や顧客のセキュリティ設定に基づいて発生する可能性があります。顧客にWeChatアプリで決済確認プロンプトを確認するよう依頼してください。課金は確認されない場合、24時間後に期限切れになります。

WeChat Pay UPM取引を返金できますか?

はい、WeChat Pay UPMは元の取引から90日以内の全額および部分返金をサポートしています。返金は1〜3営業日以内に処理され、顧客のWeChatウォレットに返金されます。

テスト

テストモード

WeChat Pay UPMはテストAPIキーを使用してテストできます:

テストフロー:

  1. テストAPIキーとテストバーコードで課金を作成
  2. テストモードでは、Omiseダッシュボードを使用して決済完了をシミュレート
  3. Webhook処理を検証

テストバーコード:

# 有効な形式のテストバーコードを使用
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"

テストシナリオ:

  • 成功した決済:POSが確認を受信することを検証
  • 失敗した決済:エラー処理と再試行ロジックをテスト
  • 保留中ステータス:顧客確認フローをテスト
  • タイムアウト:期限切れの課金処理をテスト
  • 返金:全額および部分返金フローをテスト

重要な注意事項:

  • テストモードは実際のWeChatサーバーに接続しません
  • Omiseダッシュボードの「アクション」を使用して課金を成功/失敗としてマーク
  • 本番稼働前に必ずWebhook処理をテスト
  • さまざまな金額値(最小/最大制限)でテスト

包括的なテストガイドラインについては、テストドキュメントをご覧ください。

関連リソース

次のステップ

  1. 最初のUPM課金を作成
  2. バーコードスキャナー連携を設定
  3. Webhook処理を実装
  4. 決済フローをテスト
  5. 本番稼働