Webhook — 概要
Webhook は 信頼できる シグナルです。ブラウザのコールバック
(onSuccess) やダッシュボードの表示は補助的な情報であり、
Webhook が真実のソースです。
配信保証
- 少なくとも 1 回。 サーバーがタイムアウト内に 2xx を返さない場合、
1 つのイベントは最大 6 回 配信される可能性があります。
ハンドラは冪等にしてください —
X-Deliveryで重複排除を行います。 - HTTP リクエストごとに 1 イベント。 バッチ配信は行いません。
- エンドポイント単位の独立性。 複数のエンドポイントを登録している場合、 それぞれが独立した配信とリトライのトラックを持ちます。 あるマーチャント URL が遅くても他に影響しません — ホストごとに サーキットブレーカが付いています。
- 署名付き。 すべてのペイロードに
X-Signatureヘッダーが付きます (シークレットローテーション後の 24 時間ウィンドウ中はX-Signature-Prevも付きます)。本文を扱う前に必ず検証してください。 詳しくは 署名検証 をご覧ください。
サブスクライブ可能なイベント種別
| イベント | 発火条件 |
|---|---|
payment.settled | オンチェーン送金がチェーンの確認カウントを満たしました。注文を支払い済みにする際に使用してください。 |
payment.failed | 法定通貨決済がプロバイダにより明示的に拒否された場合 (現在は: Stripe Webhook が失敗を通知)。暗号資産のタイムアウトでは発火しません — それらは代わりに checkout.expired として表面化し、短額の暗号資産決済は payment.underpaid として表面化します。 |
payment.underpaid | 資金は届いたものの注文合計に満たない場合(典型例: ステーブルコインの送金手数料が金額から差し引かれた)。 |
payment.overpaid | 注文合計を超える資金が届いた場合。余剰分は記録されますが自動返金はされません。 |
order.created | 新しい注文が作成されました — B2B API 呼び出しまたはチェックアウトセッションからの変換。 |
order.canceled | 注文がキャンセルされました。ペイロードの data.reason で手動キャンセルと payment_timeout (ワーカーがスイープした未払い古注文) を区別します。 |
order.resolved | PARTIAL_PAID の注文が PAID へ解決されました — マーチャントが不足分を許容しました。 |
order.reopened | 以前に自動キャンセルされた注文 (canceled_reason=payment_timeout) がマーチャントにより再オープンされました。 |
checkout.created | バイヤーが注文のチェックアウトを開きました。 |
checkout.completed | バイヤー側の操作が完了 (オンチェーン決済の確定を意味しません — それは payment.settled で判定)。 |
checkout.expired | バイヤーが離脱し、セッション TTL が切れました。 |
payment.refund.requested | 返金レコードが作成されました — マーチャント開始の API 呼び出し、または顧客提出の返金リクエストフォーム経由。 |
payment.refund.approved | 保留中の返金が承認ワークフローを通過しました。 |
payment.refund.rejected | 保留中の返金が却下されました。 |
payment.refund.executed | 返金のオンチェーン送金が確定し、レコードが最終状態 executed に移行しました。 |
refund_request.created | 返金リクエストトークンが発行されました。data.source は b2b / dashboard / renewal。任意のサブスクライブ — 注文ごとにどのトークンが現在有効かを追跡する監査パイプラインに便利。 |
refund_request.renewal_requested | トークン期限切れ後にバイヤーが「新しいリンクを要求」をクリック。サブスクライブを強く推奨 — 再発行ウィジェットに新しい対応項目が来たというマーチャントへの合図です。 |
refund_request.renewed | 再発行が承認され、新しいトークンが古いものを置き換えました。data.old_token / data.new_token が監査チェーンを形成します。 |
refund_request.canceled | マーチャントがダッシュボードからトークンを CANCELED に遷移させました (例: 再発行リクエスト拒否、live リンクの停止)。冪等 — 最初の遷移のみ発火。data.reason は任意のマーチャントノート。 |
ダッシュボードはこのリストを GET /v1/webhooks/event-types から取得するため、
エンドポイントの作成・編集フォームはプラットフォームが実際に発行する
イベントと常に一致します。発行されないイベントへのサブスクライブは
作成時に明確なエラーで拒否されます。
テストイベントはサブスクライブできません。 ダッシュボードの
エンドポイント単位の Send Test ボタンは、webhook.test.ping
エンベロープをそのエンドポイント 1 つだけに同期的に POST します
(リトライパイプラインをバイパス)。一方、レガシーのマーチャント単位
「テストイベント送信」パスは、フィルタに関わらず全アクティブエンドポイント
へ webhook.test エンベロープをファンアウトします。どちらも上のカタログ
には現れません — サブスクライブによってではなく、エンドポイントを登録して
いることによって受信します。
ハンドルするイベントだけにサブスクライブしてください。各エンドポイントは
独自のイベントフィルタを持ちます; ワイルドカード "*" は
「将来追加されるものを含む全イベント」を意味します。
サブスクライブするイベントを絞ることでハンドラがシンプルになり、
かつ 我々がリトライしなければならない領域も小さくなります。
ペイロード + ヘッダー
HTTP ボディはイベント固有のデータオブジェクトそのものです。 Stripe スタイル
の外側エンベロープはありません — イベント種別、配信 ID、発行タイムスタンプ
などのフィールドは代わりに ヘッダー に入ります。payment.settled の
ボディは次のような形です:
{
"receipt_id": "rcp_…",
"order_id": "ord_…",
"payment_intent_id": "pin_…",
"checkout_session_id": "cst_…",
"merchant_id": "mer_…",
"customer_id": "cus_…",
"total": "49.00",
"currency": "USD",
"payment_method": "crypto",
"token": "USDC",
"network": "polygon",
"tx_hash": "0x…",
"deposit_address": "0x…",
"treasury_address": "0x…",
"amount_received": "49.00",
"confirmations": 5,
"metadata": { /* per-event */ }
}他のイベントは独自のフィールドセットを持ちます — イベント単位のドキュメントが
揃うまでは、正式な形は payment-service/internal/domain/events.go のパブリッシャ
構造体を参照してください。フィールド名は安定 (lower snake_case); オンチェーン
tx ハッシュは常に tx_hash (not transaction_hash) です。
受信リクエストのヘッダー
Content-Type: application/json
X-Event: payment.settled
X-Delivery: 7c9e6679-7425-40de-944b-e07fc1f90ae7
Idempotency-Key: 7c9e6679-7425-40de-944b-e07fc1f90ae7
X-Timestamp: 1729536000
X-Signature: sha256=9a8b7c…
X-Signature-Prev: sha256=fa31b2… (ローテーション猶予ウィンドウ中のみ)| ヘッダー | 内容 |
|---|---|
X-Event | イベント種別 (例: payment.settled)。JSON をパースせずに済ませたい場合、プロキシ層でこれにルーティングしてください。 |
X-Delivery | 配信行を識別する UUID。同じ (event, endpoint) ペアの すべてのリトライをまたいで安定 — 冪等キーとして使ってください。 |
Idempotency-Key | X-Delivery のミラー (同じ値)。すべての配信で設定 — Stripe / GitHub の慣習を踏襲しています。 |
X-Timestamp | 試行が送信された Unix 秒。ペイロードに含めて署名するので、捕捉された (body, X-Signature) ペアを永続的にリプレイすることはできません — 許容範囲外のタイムスタンプの配信は拒否してください。 |
X-Signature | HMAC-SHA256(secret, X-Timestamp + "." + raw_body) の sha256=<hex>。詳しくは 署名検証。 |
X-Signature-Prev | 同じアルゴリズムを 前の シークレットで適用したもの。ローテーション後 24 時間ウィンドウの間のみ存在 — 切り替え中、新旧どちらの鍵を使う検証側でも配信を受け入れ続けられるようにします。ウィンドウが閉じるとヘッダーは送信されません。 |
リトライスケジュール
エンドポイントがタイムアウト内に 2xx を返さない場合、
次のスケジュールでリトライします (初回試行からの相対時刻):
| 試行 | 遅延 | 累計 |
|---|---|---|
| 1 | 0s | 0s |
| 2 | +1 分 | 1m |
| 3 | +5 分 | 6m |
| 4 | +15 分 | 21m |
| 5 | +1 時間 | 1h 21m |
| 6 | +6 時間 | 7h 21m |
試行 6 が失敗すると配信は デッドレター に移され、マーチャント
アカウントのメール宛に通知されます。デッドレター入りしたイベントは
ダッシュボードの Developers → Webhooks → Delivery history パネル、
または直接 POST /v1/webhooks/deliveries/:id/replay でリプレイできます。
各リプレイは独自の X-Delivery を持つ新しい配信行を作成します — 監査
チェーンは parent_delivery_id で元に紐付くため、リプレイのリトライが
元のイベントを隠すことはありません。
エンドポイントの登録
マーチャントダッシュボード から:
- Developers → Webhooks → + Add endpoint
- URL を貼り付け —
https://…のみ (プレーン HTTP は拒否されます; 作成フォームはlocalhost、プライベート IP レンジ、ユーザー情報を 含む URL もブロックします) - サブスクライブするイベントを選択 (全イベントは
*) - 環境を選択 — test または live (それぞれ独自のシークレットを 持ち、互いに混在することはありません)
- 保存 → ダッシュボードが署名シークレット (
whsec_…) を 1 度だけ 表示します。 サーバー側に保存してください; 次の 2 つの機能で使います。
マーチャントごと、環境ごとに最大 10 個 のエンドポイントを登録できます (例: 本番フルフィルメント用、ステージングミラー用、Slack 通知用)。 それぞれが独立したリトライ状態、シークレット、ホスト単位のサーキット ブレーカを維持します。
各エンドポイントのライフサイクル操作
各エンドポイントカードの ⋮ メニューには以下が並びます:
- Edit — URL、説明、サブスクリプションリストを変更します。
新しい URL は作成時と同じ
https:///SSRF ルールで再検証されます。 - Send Test —
webhook.test.pingエンベロープを現在のシークレットで 署名して同期 POST します。ダッシュボードに HTTP ステータス、レイテンシ、 応答の先頭 512 バイトが表示されます。RMQ パイプラインをバイパスするので 結果は即座に返ります。 - Rotate Secret — 新しいシークレットを生成します。前のシークレットは
24 時間 有効のままです (この間、配信は
X-SignatureとX-Signature-Prevの両方を含むので、検証側がどちらの鍵を使っていても 再デプロイ中もイベントを受け入れ続けられます)。 - Reveal Secret — 既存のシークレットを再表示します。新規の 2FA 検証で ゲートされ、監査ログに記録されます; コピーを紛失し、かつローテーションが 許容できない場合のみ使用してください。
- Enable / Disable — 配信履歴を失わずに
is_activeをトグルします。 無効化されたエンドポイントはダッシュボードに残りますが、新しい配信は 受け取りません。 - Delete — 永続的に削除します。後で再有効化する可能性があるなら Disable を使ってください。
ハンドラのコツ
- 2xx をすばやく返す。 重い処理の前に
200 OKで確認応答 — フルフィル メントはバックグラウンドキューにキックしてください。試行ごとの タイムアウトは 10 秒 で、それより長く応答を保持するとリトライが トリガーされます。タイムアウトはプラットフォーム側で固定で、マーチャント 設定では変更できません — ハンドラがどうしてもより長い時間を必要とする 場合はサポートに連絡してください。 X-Deliveryで重複排除する (またはIdempotency-Key— 同じ値)。 2xx を返しても上流プロキシが接続を切ってリトライがトリガーされる可能性が あります; 配信 ID は同じ配信行のすべてのリトライにわたって安定なので、 これが正しいキーです。- 未知のイベント種別を許容する。 新しいイベントが現れる可能性があります; 4xx ではなく 200 + no-op を返してください。さもないとリトライキューが 膨らみます。
- ビジネスロジックの横に
X-Deliveryをログする。 何か起きたとき、 それが我々の側と御社の側を結合するキーです。
次に
- 署名検証 — 正確なアルゴリズム + リプレイ対策パターン。
- コンセプト → セッション — 各イベント発火時に セッションがどの状態にあるか。