Nomba Webhooks は何をしますか?
Webhookは、Nombaがあなたのサーバーに実際に資金が移動したことを伝える仕組みです。インターネット上の誰でもあなたのWebhook URLにPOSTできるため、ハンドラーは注文の支払い済みマークや返金を行う前に、各リクエストがNombaから送信されたことを証明する必要があります。また、Nombaが同じイベントを再試行する可能性にも対応しなければなりません。
公式SDKの検証機能を使用してください。HMACを自前で実装しないでください。Nombaの署名は特定のフィールドを:で結合したものをカバーしており、SDKは単純な実装では見逃してしまうリプレイ攻撃もブロックします。
| 言語 | 検証 |
|---|---|
Node.js / TS (@nombaoss/nomba-node) | verifyWebhook(rawBody, headers, signingKey) |
Python (nomba) | from nomba.webhooks import verify_signature → verify_signature(raw_body, headers, signing_key) |
PHP (nombaoss/nomba-php) | Nomba\Webhook::verify($rawBody, $headers, $signingKey) |
各言語の正確なインポート方法と引数の型については、references/sdk-setup.md を参照してください。各関数は解析されたイベントを返すか、例外(WebhookVerificationError / WebhookVerificationException)をスローします。これらの関数にはAPI認証情報は不要で、Nombaダッシュボード(Developer → Webhook Setup)のWebhook 署名鍵のみが必要です。署名鍵はNOMBA_WEBHOOK_SIGNING_KEYのような環境変数から読み取ってください。これはAPIクライアントシークレットとは異なるシークレットです。
ハンドラーの手順
- 生のボディを取得する。 Nombaが送信した正確なバイトを検証します。JSONボディパーサーが先に実行され、その後にオブジェクトを再シリアライズすると、署名の一致が止まるか、さらに悪いことに、実際に処理するものとは別のものを検証してしまう可能性があります。フレームワークごとに生のボディを取得する方法が異なります。正しい方法については references/frameworks.md を参照し、該当するフレームワークのCSRF保護からこのルートを除外してください。
- 検証し、それから解析する。 検証機能が返すイベントを使用します。失敗した場合は
401(または400)を応答し、処理を停止します。拒否されたリクエストの完全なボディやヘッダーをinfoレベルでログに記録しないでください。 requestIdで重複排除する。 Nombaは配信を再試行するため、同じイベントが複数回到着する可能性があります。処理済みの各requestIdを一意制約付きで記録し、既に見たものはスキップします。重複の場合も200を返してください。そうしないとNombaが再試行を続けます。- 迅速に応答する。 迅速に
200を返し、時間のかかる処理(メール送信、フルフィルメント、元帳更新)はrequestIdをキーにしたバックグラウンドジョブで行います。処理の遅いハンドラーはタイムアウトと再配信を引き起こします。 - 以下の表を使用してイベントに対応する。 イベントを参照情報によって自身のレコードと照合し、金額と通貨が期待通りであることを確認してからフルフィルメントを実行します。
各イベントの意味
event_type | 照合対象 | 処理 |
|---|---|---|
payment_success | チェックアウト: data.order.orderReference; バーチャル口座 / 振込: data.transaction.aliasAccountReference または transactionId | data.transaction.transactionAmount(および通貨)を注文と照合し、支払い済みとマークします。トークン化されたチェックアウトの場合は、data.tokenizedCardData.tokenKey(存在し、"N/A"でない場合)を顧客に関連付けてサーバーサイドのみに保存します。 |
payment_failed | 上記と同じ | 試行を失敗としてマークし、顧客に再試行させます。課金は発生していません。 |
payment_reversal | 上記と同じ | 受領した資金が取り消されました。フルフィルメントを元に戻すか、レビュー用にフラグを立てます。 |
payout_success | data.transaction.merchantTxRef | 振込または請求書の支払いが配信されたことをマークします。 |
payout_failed | data.transaction.merchantTxRef | まだ返金しないでください。 Nombaは別の経路で再試行する可能性があります。失敗としてマークし、待機します。 |
payout_refund | data.transaction.merchantTxRef | 資金がNombaウォレットに戻ってきました。これで、顧客に返金またはクレジットし、 payoutを失敗としてマークしても安全です。 |
payout_refundではなくpayout_failed(またはタイムアウト)で返金することは、加盟店が顧客に二重支払いをしてしまう典型的な方法です。認識できないイベントについては、ログに記録して200を返してください。
APIで資金に関する決定を確定する。 Nombaの署名がカバーするのは、event_type、requestId、data.merchant.userId、data.merchant.walletId、data.transaction.transactionId、type、time、responseCode、およびタイムスタンプのみです。金額、orderReference、merchantTxRef、カードのtokenKeyは署名されていません。したがって、フルフィルメント、返金、カードトークンの保存を行う前に、SDKを使用して再クエリを実行し、APIの応答に基づいて行動してください。チェックアウト支払いの場合はtransactions.get({ orderReference })(本番環境ではcheckout.getTransactionも)、 payoutの場合はtransactions.get({ merchantTxRef })を使用します。署名済みのrequestIdで重複排除することで、既に処理済みのイベントの改変コピーを防ぎ、再クエリが残りをカバーします。
リプレイ保護
requestIdで重複排除することが主な防御策です。既に処理済みのイベントのリプレイまたは再試行配信は何も変更しません。
検証機能は古いタイムスタンプを拒否することもできますが(toleranceSeconds / tolerance_seconds / PHPの第4引数)、デフォルトではオフにしておいてください。Nombaはnomba-timestampをペイロードが送信された時刻として文書化していますが、自動再試行が新しいタイムスタンプを取得するかどうかは明記していません。もし元のタイムスタンプを保持する場合、Nombaの再試行ウィンドウよりも短いトレランスを設定すると、障害後の正当な再試行が拒否され、支払い済みの注文が確認されないままになる可能性があります。サンドボックスで再配信が新しいタイムスタンプを運ぶことを確認した後にのみ有効にし、その場合も余裕を持った値(例:1時間)に設定し、サーバークロックを同期してください。
例(Node.js + Express)
import express from "express";
import { verifyWebhook, WebhookEventType } from "@nombaoss/nomba-node";
const app = express();
// このルートのみ生のボディを使用。アプリ全体の express.json() より先に登録する。
app.post("/webhooks/nomba", express.raw({ type: "*/*" }), async (req, res) => {
let event;
try {
event = verifyWebhook(req.body, req.headers, process.env.NOMBA_WEBHOOK_SIGNING_KEY!);
} catch {
return res.sendStatus(401);
}
// request_id に対する一意制約。既に処理済みの場合は false を返す。
const isNew = await webhookEvents.recordOnce(event.requestId, event.event_type);
if (isNew) await jobs.enqueue("nomba-webhook", event); // バックグラウンドで処理
res.sendStatus(200);
});
Python(Flask/FastAPI/Django)、PHP(Laravel/プレーン)、およびその他のNodeフレームワークについては、references/frameworks.md を参照してください。
テスト
- SDKのドキュメントに記載されたテストベクトルを使用してハンドラーを単体テストします。ボディはSDKの
webhook-fixture.json、鍵はSampleSignatureKey123、ヘッダーはnomba-timestamp: 2025-09-29T10:51:44Z、nomba-signature: q30yFZDyykoymBf9+mt1wz5UAuLk57raPGPHlGzIv00=です。間違った鍵、改ざんされたボディ、欠落したヘッダーが拒否されること、および重複したrequestIdが一度だけ処理されることをアサートします。 - エンドツーエンドテスト:ダッシュボードでテスト用のWebhook URLと署名鍵を設定し、トンネルを使用してローカルサーバーを公開し、サンドボックスで支払いを行います。ダッシュボードから配信の再プッシュやデバッグが可能です(https://developer.nomba.com/docs/api-basics/troubleshoot-webhooks)。
チェックリスト
- 生のボディは、解析やビジネスロジックの前に、SDKで検証される。
- 署名鍵はコードではなく環境変数から取得される。
- 不正な署名は401/400を返し、それ以外の処理は行われない。
-
requestIdは一意制約で重複排除される。重複の場合も200を返す。 - 負荷の高い処理はバックグラウンドで実行され、ハンドラーはすぐに200を返す。
- フルフィルメント前に、金額と通貨が注文と照合される。
- 返金は
payout_refund(または再クエリでREFUNDが返された場合)のみで行う。 - ルートはCSRF保護から除外され、HTTPSを使用する。