システムが午前2時に「ストライキ」を起こしたとき
時計の針がちょうど午前2時を指したとき、ようやく眠りにつこうとした瞬間にSlackの通知が鳴り止まなくなりました。ある顧客からの厳しいメッセージが届いていました。「500kの送金に成功して、お金も引き落とされているのに、なぜアカウントがProにアップグレードされないんだ?」。ログを確認して驚きました。銀行側のメンテナンスにより、古いQRコードスキャン用スクリプトが遅延し、サービスを有効化するためのデータが時間内に届いていなかったのです。
その時、私は理解しました。銀行のログをスキャンするような不安定なスクリプトや手動確認に頼り続けていれば、遅かれ早かれ顧客を失うことになると。スタートアップには、公式のAPIがあり、安定していて、そして何より取引手数料が0ドンであるソリューションが必要です。PayOSは、現在最も注目すべき名前です。この記事では、PayOSをNode.jsに実装し、注文の取りこぼしを完全に防ぐための適切なWebhook処理方法を解説します。
なぜStripeやPayPalではなくPayOSなのか?
最初から決済ゲートウェイの選択を誤ると、後でリファクタリングに何週間も費やすことになります。現実的な数字を見て、その違いを確認してみましょう。
1. 手動銀行振込 (Manual Bank Transfer)
- 問題点: 顧客はスクリーンショットを撮る必要があり、あなたは手動で照合する必要があります。
- リスク: スケールさせるのが非常に困難です。1日に50件の注文があれば、銀行アプリのチェックだけで1日が終わってしまいます。
2. 国際決済ゲートウェイ (Stripe, PayPal)
- コスト: 取引手数料は通常2.9% + $0.3です。100万VNĐの注文の場合、約40,000VNĐの手数料がかかります。
- 手続き: ベトナムの銀行への出金に3〜7日かかり、さらに為替差損も発生します。
3. PayOS (VietQR決済ゲートウェイ)
- コスト: 取引手数料は無料(0ドン)。お金は即座にあなたの銀行口座に直接入金されます。
- 体験: 顧客はQRコードをスキャンするだけで完了です。口座番号や振込内容を手動で入力する必要はありません。
結論: ベトナムのユーザー向けのプロジェクトや、低コストで運用したいマルチテナントSaaSであれば、運用コストを節約するためにPayOSが最適な選択肢です。
実装のステップ
以前、お金に関する処理コードが散在していたために、システムをゼロから作り直さなければならなかったことがあります。そこから得た教訓は、決済ロジックを独立したモジュールに分けることです。
ステップ1:プロジェクトの初期化
PayOSの公式ライブラリをインストールします。潜在的なセキュリティエラーに悩まされたくないのであれば、チェックサムのハッシュ関数を自分で書かないようにしましょう。
npm install @payos/node dotenv express
PayOSのダッシュボードから Client ID、API Key、および Checksum Key を取得し、.env ファイルに追加します:
PAYOS_CLIENT_ID=your_id
PAYOS_API_KEY=your_key
PAYOS_CHECKSUM_KEY=your_checksum_key
ステップ2:インスタンスの設定
接続を管理するために payos.js ファイルを作成します。これにより、コードが整理され、メンテナンスが容易になります。
const PayOS = require("@payos/node");
require('dotenv').config();
const payos = new PayOS(
process.env.PAYOS_CLIENT_ID,
process.env.PAYOS_API_KEY,
process.env.PAYOS_CHECKSUM_KEY
);
module.exports = payos;
ステップ3:決済リンクの作成
顧客が決済ボタンをクリックしたとき、APIを呼び出してQRリンクを取得します。非常に重要な注意点として、orderCode は **Number**(整数)型である必要があります。IDが文字列の場合は、ハッシュ関数を使用して数値に変換してください。
app.post("/create-payment-link", async (req, res) => {
const { amount, orderId } = req.body;
const body = {
orderCode: Number(orderId),
amount: amount,
description: `注文 ${orderId} の支払い`,
returnUrl: "https://your-app.com/success",
cancelUrl: "https://your-app.com/cancel",
};
try {
const paymentLinkRes = await payos.createPaymentLink(body);
return res.json({ url: paymentLinkRes.checkoutUrl });
} catch (error) {
return res.status(500).json({ message: "決済リンクを作成できませんでした" });
}
});
Webhookの処理:不正アクセスを許さない
多くの人が、顧客が「サイトに戻る」ボタンをクリックするのを待ってから注文を更新しようとしますが、これは致命的な間違いです。顧客は決済完了後にタブを閉じてしまう可能性があるからです。Webhookこそが、ロジックを正確に処理すべき場所です。
署名検証 (Verify Signature)
悪意のある者が、あなたのWebhook URLに偽のリクエストを送信してサービスを詐取しようとする可能性があります。PayOSは、データが彼らのサーバーからのみ送信されたことを保証するためのチェックサムメカニズムを提供しています。
app.post("/payos-webhook", async (req, res) => {
const webhookData = req.body;
try {
// PayOSから送信されたデータが正しいか検証する
const verifiedData = payos.verifyPaymentWebhookData(webhookData);
if (webhookData.code === "00") {
// チェック:この注文は既に処理済みか?
const order = await Order.findOne({ id: verifiedData.orderCode });
if (order && order.status !== 'PAID') {
await order.update({ status: 'PAID', paidAt: new Date() });
console.log(`注文 ${verifiedData.orderCode} の決済が正常に完了しました。`);
}
}
return res.json({ success: true });
} catch (error) {
return res.status(400).json({ message: "署名が無効です" });
}
});
べき等性(二重処理の防止)
ネットワークエラーなどにより、PayOSが同じ注文に対してWebhookを2〜3回送信することがあります。データベースで更新前に注文ステータスを確認しないと、システムが二重に入金処理を行ったり、サービス有効化メールを何度も送信したりする可能性があります。Node.js環境でべき等性(Idempotency)を確保し、ビジネスロジックを実行する前に必ず if (order.status !== 'PAID') をチェックしてください。
Ngrokを使った高速デバッグのコツ
Webhookをテストするためにわざわざ本番サーバーにコードをデプロイする代わりに、**Ngrok** を使ってローカルマシンへのトンネルを作成しましょう。APIの動作確認には、Postmanに代わるAPI管理とテストツールのBrunoを併用するのも効率的です。
- Node.jsアプリをポート3000で実行します。
- コマンドを入力:
ngrok http 3000。 - Ngrokが提供するURLをPayOSダッシュボードのWebhook設定に貼り付けます。
これで、テスト用のQRコードをスキャンするたびに、個人のマシンのターミナルに即座にログが表示されます。この方法は、デプロイを待つ時間を何時間も節約してくれます。
おわりに
決済の統合は、単にコードを動くように書くことではなく、安全なプロセスを構築することです。常に署名を検証し、重複処理を避け、各取引の詳細なログを記録してください。さらに高度な自動化を目指すなら、堅牢な(Durable)ワークフローを構築して、決済後の処理を確実に実行させることも検討に値します。システムが円滑に稼働すれば、深夜の苦情メッセージに怯える必要もなくなります。皆さんの実装が成功することを願っています!

