概要
Webhook は、注文、決済、サブスクリプション、返金などのイベントが発生したとき、サーバーにリアルタイム通知を配信します。http チャネルは本ページで説明する JSON エンベロープと署名検証を使用します — 本ガイドの大部分はこのチャネルが対象です。IM チャネルでは各プラットフォーム固有のフォーマットが使われ、認証は URL のトークンによって行われます。署名の検証は不要です。
セットアップ
1
チャネルを選択し URL を準備
- HTTP — POST リクエストを受け付け
200を返すサーバーエンドポイントを構築します。 - Feishu / Discord / Telegram / Slack — 対象プラットフォームで Bot または受信 Webhook を作成し、その URL をコピーします。Telegram の場合はメッセージを受信する chat ID も控えておきます。
2
(HTTP のみ)検証用公開鍵をコピー
Waffo は環境ごと(Test / Production)に 1 組の固定鍵ペアを使用しており、すべてのストアで共通です。Webhook 登録時に公開鍵が返されるわけではなく、ダッシュボードから自分でコピーする方式です。ダッシュボード を開き、任意のストア → 設定 → Webhooks に移動して、連携する環境(Test または Production)の Webhook Public Key をコピーし、サーバーの設定に保存してください。以降すべての HTTP webhook はこの鍵で検証します。
どのストアのダッシュボードでも Test 公開鍵は同じ、Production 公開鍵も同じです —— プラットフォーム共通の鍵です。Webhook URL を追加・編集・削除しても変わりません。
3
Webhook を登録
ダッシュボード → 設定 → Webhooks から追加するか、
POST /v1/actions/store/add-webhook を呼び出します。各 Webhook レコードは 1 つのチャネル、1 つの URL、購読イベント、対象環境(Test の場合は testMode: true、Production の場合は false)を指定します。1 つのストアに複数の Webhook を登録できます。4
テストイベントを送信
ダッシュボードの「テストイベントを送信」ボタンを使用して、登録した 1 つまたはすべての Webhook にサンプルイベントを配信します。
5
署名を検証してイベントを処理
HTTP チャネルでは、以下のコード例を使ってイベント処理前に署名を検証します。IM チャネルは事前レンダリングされたメッセージを配信するため、サーバー側の処理は不要です。
環境の隔離
各 Webhook はtestMode フラグによって 1 つの環境に登録されます。Test と Production は完全に独立しています:
各 HTTP ペイロードの
mode フィールドはソース環境を示します: "test" または "prod"。
ペイロードフォーマット
ヘッダー
ボディ
トップレベルフィールド
data フィールド
下表は各 data フィールドの型、出現条件、意味を定義します。特定のイベントの正確なフィールド構成は、下記「イベント詳細」にある各イベントの完全な body を参照してください。
条件列の「決済イベント」は order.completed と subscription.payment_succeeded、「サブスクリプションイベント」はすべての subscription.* イベントを指します。
金額は表示フォーマット文字列であり、最小通貨単位からすでに変換されています。例えば、USD
"29.00" = 2900 セント、JPY "4500" = 4500 円です。利用可能な場合は subtotal と total を使用して明細表示できます。Webhook ペイロードの
taxRate はパーセント値です —— 10 は 10% を意味します。この単位の表記は API 全体で統一を進めており、他の箇所(preview-tax のレスポンス例、TypeScript SDK の型定義)では現在も小数値として記述されています。税の徴収は未開始のため、現時点ではすべての注文で taxRate は 0 です。単位を決め打ちせず、ペイロードの値を読み取ってください。eventId マッピング
eventId はイベントをトリガーしたビジネスエンティティを識別します。
subscription.past_due は eventId に -YYYY-MM を付加します。同一サブスクリプションにつき、カレンダー月ごとに最大 1 回の past_due イベントがトリガーされます。翌月もまだ滞納中の場合、新しいイベントが発行されます。イベントタイプ
一覧
subscription.updated のイベントテンプレートは準備済みで、サブスクリプション商品変更機能のリリース後に有効化されます。イベント詳細
order.completed
order.completed
トリガー: 単発注文の決済が初めて成功した時。Body:このイベントでは
amount は total と同値です。サブスクリプションおよび返金フィールドは含まれません。値がない場合に省略されるフィールド: productDescription、merchantProvidedBuyerIdentity、billingDetail、orderMerchantExternalId、taxRate、taxName、subtotal、total、paymentMethod、paymentLast4。推奨アクション:- デジタル商品の配信(ライセンスキー、ダウンロードリンク、アクティベーションコード)
- 注文管理システムの更新
- customer への確認送信(Waffo の組み込みメールを使用しない場合)
order.completed がトリガーされるのは 1 回のみです。返金は refund.succeeded / refund.failed で通知されます。subscription.activated
subscription.activated
トリガー: 新規サブスクリプションの初回決済が成功した時(このイベントは個別の課金ではなく注文に紐づくため、
pending → active)。Body:paymentId を含む決済フィールドは一切含まれません。値がない場合に省略されるフィールド: productDescription、merchantProvidedBuyerIdentity、billingDetail、orderMerchantExternalId、taxRate、taxName、subtotal、total、currentPeriodStart、currentPeriodEnd。推奨アクション:- 加入者のアカウントをプロビジョニングしてアクセスを付与
- サブスクリプション開始日を記録
pending から active に遷移した時にのみ発行されます。同じ初回決済は課金自体に対して subscription.payment_succeeded も発行します — サブスクリプションライフサイクル を参照してください。subscription.payment_succeeded
subscription.payment_succeeded
トリガー: サブスクリプション課金の成功 — 初回決済、各更新、滞納期間を解消する課金を含みます。Body:他の
subscription.* イベントと異なり、このイベントは課金に紐づくため決済フィールドを含みます。amount は今期の実際の課金額で、total と同値です。サブスクリプションがキャンセル予定であっても、このイベントに canceledAt は現れません。値がない場合に省略されるフィールド: productDescription、merchantProvidedBuyerIdentity、billingDetail、orderMerchantExternalId、taxRate、taxName、subtotal、total、paymentMethod、paymentLast4、currentPeriodStart、currentPeriodEnd。推奨アクション:- サービス期間の延長
- この請求サイクルの請求書を生成
- サブスクリプションが以前
past_dueだった場合、フルアクセスを復元
subscription.canceling
subscription.canceling
トリガー: customer またはマーチャントがキャンセルをリクエスト。サブスクリプションは現在の支払い済み期間の終了まで有効です。Body:このイベントに決済フィールドは含まれません。
amount はサブスクリプションの期間あたりの価格であり、個別の課金額ではありません。値がない場合に省略されるフィールド: productDescription、merchantProvidedBuyerIdentity、billingDetail、orderMerchantExternalId、taxRate、taxName、subtotal、total、currentPeriodStart、currentPeriodEnd。推奨アクション:- 「サブスクリプションは [日付] に終了します」通知を表示
- リテンションフロー(例:割引更新)を提供
- アクセスを取り消さないでください — customer は現在の期間分を支払い済みです
subscription.uncanceled がトリガーされます)。subscription.uncanceled
subscription.uncanceled
トリガー: 現在の期間が終了する前にキャンセルが取り消された時。Body:このイベントに決済フィールドは含まれません。キャンセルが取り消されると
canceledAt はクリアされるため、現れません。値がない場合に省略されるフィールド: productDescription、merchantProvidedBuyerIdentity、billingDetail、orderMerchantExternalId、taxRate、taxName、subtotal、total、currentPeriodStart、currentPeriodEnd。推奨アクション:- 「まもなく終了」通知を削除
- 自動更新ステータスを復元
subscription.updated
subscription.updated
トリガー: サブスクリプション商品が変更された時(アップグレードまたはダウングレード)。Body:すべてのフィールドは変更後の状態を表します — ペイロードに変更前後の対比は含まれないため、変更前のプランを知ることはできません。アップグレードとダウングレードを区別する必要がある場合は、変更前のプランを自身で記録してください。このイベントに決済フィールドは含まれません。値がない場合に省略されるフィールド:
productDescription、merchantProvidedBuyerIdentity、billingDetail、orderMerchantExternalId、taxRate、taxName、subtotal、total、currentPeriodStart、currentPeriodEnd。推奨アクション:- customer のアクセスレベルを更新(機能の追加/削除)
- 請求記録を更新
subscription.canceled
subscription.canceled
トリガー: サブスクリプションが終了 — 支払い済み期間が終了し、これ以上の更新は行われません。Body:このイベントに決済フィールドは含まれません。
canceledAt はキャンセルがリクエストされた時刻で、本イベントより前になります — サブスクリプションはそれ以降も currentPeriodEnd まで有効でした。amount は期間あたりの価格であり、最後の課金額ではありません。値がない場合に省略されるフィールド: productDescription、merchantProvidedBuyerIdentity、billingDetail、orderMerchantExternalId、taxRate、taxName、subtotal、total、currentPeriodStart、currentPeriodEnd、canceledAt。推奨アクション:- アクセスを取り消す(または無料プランにダウングレード)
- 猶予期間中データを保持(customer が再登録する場合に備えて)
- 「サブスクリプション終了」確認を送信
subscription.past_due
subscription.past_due
トリガー: 更新決済が失敗し、サブスクリプションが滞納状態に入った時。Body:
eventId には -YYYY-MM サフィックスが付きます — eventId が id と異なるのはこのイベントだけで、月次の重複排除はこれによって実現されています。決済フィールドを含まないため、課金が失敗した理由はペイロードからは分かりません。amount は請求金額です。値がない場合に省略されるフィールド: productDescription、merchantProvidedBuyerIdentity、billingDetail、orderMerchantExternalId、taxRate、taxName、subtotal、total、currentPeriodStart、currentPeriodEnd。推奨アクション:- customer に支払い方法の更新を通知
- オプションでサービスを制限(完全に取り消すのではなく機能を制限)
- すぐにアクセスを取り消さないでください — PSP が自動的に課金をリトライする場合があります
past_due イベント。翌月もまだ滞納中の場合、新しいイベントが発行されます。refund.succeeded
refund.succeeded
トリガー: 返金が完了し、資金が返還された時。Body(部分返金の例: 元の決済 29.00 に対して 10.00 の返金):
subtotal / total / taxRate / taxName は元の決済基準で、比較用に保持されています。返金には税の内訳がないため taxAmount は常に "0" です。税の詳細は元の注文を確認してください。paymentId は返金対象の課金を指します — サブスクリプションの場合は特定の期の課金であり、どの期が返金されたかはこれで判断できます。ペイロードにはその決済の累計返金額は含まれないため、1 つの決済が複数回返金される可能性がある場合は自身で記録してください。値がない場合に省略されるフィールド: productDescription、merchantProvidedBuyerIdentity、billingDetail、orderMerchantExternalId、refundTicketMerchantExternalId、taxRate、taxName、subtotal、total、paymentMethod、paymentLast4、refundReason。推奨アクション:- 配信済みデジタル商品を取り消す(ライセンスの失効、ダウンロードの無効化)
- 注文ステータスを「返金済み」に更新
refund.failed
refund.failed
トリガー: 返金処理が失敗した時。Body:その他の構造は
refund.succeeded と同じです。2 つのイベントを区別できるのは refundStatus のみなので、フィールドの有無ではなくこれで分岐してください。ペイロードに失敗理由は含まれないため、ダッシュボードで返金チケットを確認してください。値がない場合に省略されるフィールド: refund.succeeded と同じ。推奨アクション:- 手動確認のために障害をログに記録
- 商品を取り消さないでください(返金は完了していません)
ライフサイクルとトリガータイミング
イベントは購入者のクリック時ではなく、背後のビジネス事実が確定した時点で発行されます。決済系イベントは決済プロバイダーが課金を確認した後、サブスクリプション状態系イベントはプロバイダーが状態変更を確認した後に発行されます。発行後、エンドポイントへの配信は通常数秒以内に完了します。timestamp フィールドはイベントが生成された時刻を保持します。リトライでも同じ値が使われ、配信試行ごとに変化することはありません。
配信タイムライン
決済が確定した地点は、チェックアウトのリダイレクトではなくイベントです。購入者がリダイレクト前にブラウザを閉じても、イベントは届きます。一回限りの注文のライフサイクル
決済が拒否された注文は
pending のまま残り、購入者は同じ注文で再度支払えます。order.completed は注文につき 1 回だけ — 最終的に成功した決済が発行します。
サブスクリプションライフサイクル
キャンセルは 2 つの時点に分かれます。
subscription.canceling はキャンセルがリクエストされた瞬間に発行され、終了自体は支払い済み期間の終わりに予約されます。subscription.canceled は決済プロバイダーが終了を確認した後 — リクエスト時点ではなく currentPeriodEnd の前後 — に発行されます。その間もサブスクリプションは有効で、customer はキャンセルを取り消せます。
滞納中のサブスクリプションは次の更新失敗で終了します。1 回目の拒否で active から past_due に移って subscription.past_due を発行し、past_due 状態でさらに拒否されるとサブスクリプションが終了して subscription.canceled を発行します。
返金のライフサイクル
返金イベントは最後のステップ — 決済プロバイダーが結果を確定した時点 — でのみ発行されます。返金チケットの作成・承認・却下・再提出では webhook は発生しないため、審査中のチケットは確定するまでエンドポイントからは見えません。API 経由の返金は自動承認され、そのままprocessing に入ります。チケットの状態と照会方法は 返金エンドポイント を参照してください。
イベントが発行されないケース
署名検証
本番環境では必ず署名を検証してください。 検証なしでは、誰でもエンドポイントに偽造リクエストを送信できます。アルゴリズム
SDK の使用(推奨)
SDK は公開鍵を埋め込み、環境を自動検出し、フォーマットの正規化を処理します。手動検証
TypeScript SDK を使用しない場合は、署名検証を手動で実装してください。レスポンス要件
- 2xx ステータスコードを返してください(推奨:
200) - 10 秒以内に応答してください
- レスポンスボディは問いません
リトライポリシー
配信失敗時は指数バックオフで自動的にリトライされます。配信ステータス
ダッシュボードの Webhook ログで配信履歴を確認できます。ステータス、HTTP レスポンスコード、レスポンスボディ(1000 文字に切り詰め)が含まれます。
重複の処理
ネットワークの問題により、同じイベントが複数回配信される場合があります。イベント処理が冪等であることを確認してください。 重複排除にはeventType + eventId の組み合わせ(システム内で一意制約あり)を使用してください。
同一ビジネスイベント(同一の
eventType + eventId)は 1 つの配信レコードのみを作成します — 重複することはありません。ただし、リトライにより単一の配信がエンドポイントに複数回到達する場合があります。ベストプラクティス
必ず署名を検証する
必ず署名を検証する
常に
X-Waffo-Signature を検証してください。検証なしでは、誰でもエンドポイントに偽造リクエストを送信できます。HTTPS を使用する
HTTPS を使用する
本番環境の Webhook URL は、転送中のデータを保護するために HTTPS を使用する必要があります。
即座にレスポンスし、非同期で処理する
即座にレスポンスし、非同期で処理する
すぐに
200 を返し、ビジネスロジックはバックグラウンドで処理してください。レスポンスが遅いと不要なリトライが発生します。eventType + eventId で重複排除する
eventType + eventId で重複排除する
eventType + eventId の組み合わせで重複排除してください。同じ配信が複数回処理されても副作用がないようにしてください。タイムスタンプを確認する
タイムスタンプを確認する
リプレイ攻撃を防ぐために、
t タイムスタンプが現在時刻から 5 分以内であることを確認してください。正しい環境の鍵を使用する
正しい環境の鍵を使用する
Test と Production は異なる鍵ペアを使用します。ペイロードの
mode フィールドに公開鍵を一致させてください。受信ペイロードをログに記録する
受信ペイロードをログに記録する
デバッグのために受信したペイロードを保存してください。ダッシュボードでも配信ログクエリを提供しています。
購読したすべてのイベントを処理する
購読したすべてのイベントを処理する
まだ必要でなくても、購読しているすべてのイベントに分岐を追加してください。未処理のイベントには
200 を返してください — エラーを返すと不要なリトライがトリガーされます。テスト
テストイベントの送信(推奨)
ダッシュボードの「テストイベントを送信」ボタンを使用して、実際のトランザクションをトリガーせずにテストイベントを送信します。テストイベントは固定のサンプルデータ(amount 0、taxAmount 0、product “[TEST] Webhook Verification”)を使用し、常に Test 鍵で署名されます。 10 種類すべてのイベントタイプがサポートされています — 各イベントをテストしてハンドラーを検証してください。Test モードを使用する
- ダッシュボードで Test 環境の Webhook URL とイベントを設定します
- Test モードで実際の操作を行います(注文の作成、決済の処理)
- イベントは Test 署名鍵を使用して Test Webhook URL に送信されます
ローカル開発
トンネルを使用してローカルサーバーを公開します。配信ログ
ダッシュボードで Webhook 配信履歴を確認できます。- ステータス: pending / success / failed
- HTTP ステータスコード: サーバーのレスポンスコード
- レスポンスボディ: サーバーのレスポンス(1000 文字に切り詰め)
- タイムスタンプ: 最終配信試行
FAQ
Webhook が受信できない
- ダッシュボードで Webhook URL が設定され、公開アクセス可能であることを確認してください
- 正しいイベントタイプを購読していることを確認してください
- 正しい環境(Test / Production)を使用していることを確認してください
- ファイアウォールが Waffo からのリクエストを許可していることを確認してください
- ダッシュボードの「テストイベントを送信」で問題を切り分けてください
署名検証が失敗する
- 正しい環境の公開鍵(Test vs Production)を使用していることを確認してください
- 生のリクエストボディを使用していることを確認してください — パースされた JSON オブジェクトではありません
- ミドルウェアやプロキシがリクエストボディを変更していないか確認してください
- 署名入力の形式が
${t}.${rawBody}(タイムスタンプ + ドット + 生のボディ)であることを確認してください - TypeScript を使用している場合は、@waffo/pancake-ts SDK に切り替えてください — 鍵の選択とフォーマットの正規化を自動的に処理します
重複イベントが受信される
これは通常のリトライ動作です。エンドポイントが非 2xx を返したかタイムアウトした場合、システムがリトライします。ハンドラーが冪等であることを確認してください — 重複排除にはeventType + eventId の組み合わせを使用してください。
サブスクリプションの初回決済で 2 つのイベントが届くのはなぜですか?
初回の課金は 2 つの事実を同時に確定させます。サブスクリプションがアクティブになったこと(subscription.activated、注文に紐づく)と、支払いを受領したこと(subscription.payment_succeeded、課金に紐づく)です。両者は独立して配信されるため、順序も間隔も保証されません。activated でアクセスをプロビジョニングし、payment_succeeded で受領を記録すれば、初回決済の処理はちょうど 1 回だけ実行されます。
subscription.canceled は正確にはいつ届きますか?
currentPeriodEnd の前後です — キャンセルがリクエストされた時点ではありません。キャンセルのリクエストは即座に subscription.canceling を発行し、終了を支払い済み期間の終わりに予約します。決済プロバイダーが終了を確認した後に subscription.canceled が続きます。アクセスをいつ終了すべきかは、canceling ペイロードの currentPeriodEnd で判断してください。
subscription.canceling と subscription.canceled の違い
canceling: キャンセルがリクエストされましたが、現在の支払い済み期間がまだ終了していません。サブスクリプションはまだ有効で、customer はキャンセルを取り消すことができます(uncanceledがトリガーされます)。アクセスを取り消さないでください。canceled: サブスクリプションは終了しています。これは不可逆です — アクセスを取り消すか権限をダウングレードしてください。
各イベントの data.amount の意味
すべてのイベント: data.amount はそのイベントに対する取引金額(税込み)です。
order.completed/subscription.activated— 決済金額subscription.payment_succeeded— 今期の更新金額subscription.past_due— 今期の請求金額refund.succeeded/refund.failed— 返金金額subscription.canceling/subscription.canceled/subscription.uncanceled— サブスクリプションの期間あたりの金額