銀行エンジニアリング 31:イベントとWebhook(重複・順序・欠落・署名)
銀行からアプリへの通知は、遅れたり、重なったり、逆順に届いたり、届かなかったりします。それでも結果が狂わないように、正規のイベント列と、通知の受け方を設計します。
v8以降に実装した台帳・記帳エンジンなどを実際に動かし、正常時と異常時の両方から銀行システムの構造を学ぶ。
Prerequisite: BANKING SYSTEMLEARNING GOALS
このページで学ぶこと
- Webhookは『お知らせ』であり、正本ではない
- event_idで重複を捨て、sequenceで並べる
- 署名の検証と、欠落の取り戻し
PLAIN LANGUAGE
まず、ここだけつかむ
通知(Webhook)は遅れたり、重なったり、逆順になったり、届かなかったりします。正本は欠番のないイベント列です。
アプリは event_id で重複を捨て、sequence で並べ、欠番があれば events API で取り戻します。
通知は資金を動かしません。だから、どんな乱れがあっても金融の結果は変わりません。
DIFFICULT PARTS, STEP BY STEP
難しいポイントを、順番にほどく
専門用語を覚えることより、「なぜ必要か」「このシステムのどこで使うか」を理解することが目標です。
Outbox
状態の変更と同時に、通知の元になる記録を残す設計です。
お金は動いたのに通知の元がない、という食い違いを防ぐためです。
openbanking.emit_event。変更と同じトランザクション。
sequence と event_id
順番を表す番号と、出来事そのものの識別子です。
並べ替えと重複の排除に使うためです。
同意ごとに1から欠番なし。event_id は重複配信でも同じ。
署名
通知が本当に銀行から来たことを確かめる印です。
偽の通知を見分けるためです。
HMAC-SHA256。鍵は作成時に一度だけ表示。
Level 1 基礎:通知は届かないことがある
インターネットを通る通知は、相手が落ちていれば届かず、再送すれば重なり、順番も保証されません。ここで通知を正本として資金を動かすと、二重に支払ったり、取りこぼしたりします。
そこで、銀行は起きた出来事を欠番のない『イベント列』として記録し、Webhookはその一部を知らせるだけにします。アプリはいつでもイベント列を取り直せます。
Level 2 銀行業務:アプリ側の決まり
通知が遅れても、重なっても、逆順でも、欠けても、金融の結果は変わりません。通知は結果を表示するだけで、資金を動かさないからです。
| 問題 | アプリがすること |
|---|---|
| 同じ通知が2回来る(重複) | event_id が同じものは1つとして扱い、2回目は捨てる |
| 順番が入れ替わる | 同じ同意の中では sequence の順に並べ替えてから処理する |
| 通知が来ない(欠落) | GET /events?after_sequence=最後に処理した番号 で不足分を取り戻す |
| 偽の通知が来る | 署名を検証し、合わなければ捨てる |
Level 3 システム:MiniBankでの実装
- openbanking.emit_event が、状態の変更と同じトランザクションでイベントを追記する。同意ごとに sequence は1から欠番なしで増える。追記専用で、更新も削除もできない(PF001)
- openbanking.run_deliveries は外部へ通信しない。購読ごとの『受信者の振る舞い』(正常/最初の1回だけ失敗/常に失敗/重複/逆順)に従って、配信の試行を記録する
- 失敗した配信は5回まで記録し、5回失敗したあとの次の実行で DEAD にする(初回を含めて5回の失敗)。DEAD になっても、イベントは events API から取れる
- 署名は X-MiniBank-Signature: t=<unix>,v1=HMAC-SHA256(鍵, t + "." + 本文)。鍵は購読の作成時に一度だけ返し、ログにも監査にも残さない
- openbanking.reconcile が、連番の欠落、依頼と振込の不一致、無効な同意で生きているトークンを検出する
異常時:DEADの通知が増えた
| 段階 | 内容 |
|---|---|
| 症状 | 管理画面の『配信できなかった通知(DEAD)』が増える |
| 原因の候補 | アプリの受信口が長く止まっている、URLの誤り |
| 確認 | アプリは events API で追い付けるので、金融結果には影響しない |
| 対応 | 受信者の状態を確かめてから、必要ならアプリ側で events API を使って取り戻す |
Level 4 アーキテクチャ:Outbox
状態の変更とイベントの記録を同じトランザクションにする設計を、Outboxと呼びます。片方だけが成功して『お金は動いたのに通知の元がない』状態を作らないためです。配信は別の仕事として、何度でもやり直せます。
どこを確認すればよいか
- 管理画面「Open Banking」のWebhookの購読・配信・イベント
- Lab:受信者の振る舞いを切り替えて、アプリ側の処理を確かめる
CHECK YOUR UNDERSTANDING
3問で理解チェック
答えを頭の中で考えてから、各項目を開いて確認してください。
Q1同じ通知が2回来たらどうしますか?
event_id が同じなので2回目は捨てます。
Q2通知が来なかったら、どうやって取り戻しますか?
GET /events?after_sequence= で不足分を取得します。
Q3Webhookが常に失敗するとお金は動かなくなりますか?
いいえ。通知は資金を動かしません。5回失敗すると DEAD になり、イベントは pull で取れます。
完全に暗記する必要はありません。大切な考え方を説明できそうなら押してください。
SOURCE TRAIL
根拠と関連教材
CURRENT IMPLEMENTATIONは確認したrepository内の実装です。HISTORICAL IMPLEMENTATIONは導入・修正の履歴で、後続migrationによる再定義があり得ます。RELATED LESSONは教材への案内で、実装の一次Sourceではありません。ASSUMPTIONは仮定、UNKNOWNは未確認を表します。
確認基点: d02eae8914f39cbf06bfe422dc39c71886f82b3e / 2026-09-11。稼働環境への適用確認: UNKNOWN。パス実在の自動検査と、記述した技術的事実の確認は別です。
Implementation Status: IMPLEMENTED IN MINIBANK(この教材が扱うコードの範囲)
- RELATED LESSON/docs/be-openbanking-api-transfer資源APIと振込の依頼
- RELATED LESSON/docs/be-business-day-eodバッチと再実行の考え方
- CURRENT IMPLEMENTATION
supabase/migrations/20260930120000_v16_open_banking.sqlopenbanking.emit_event / openbanking.run_deliveries同意ごとの欠番なしのイベント列と、外部通信なしの模擬配信(再試行・DEAD・重複・逆順・署名) - CURRENT IMPLEMENTATION
supabase/migrations/20260930120000_v16_open_banking.sqlopenbanking.reconcileイベントの連番の欠落、依頼と振込の不一致、無効な同意で生きているトークンの検出 - CURRENT IMPLEMENTATION
scripts/banking/v16-openbanking.test.mjs登録・認可・PKCE・コード再使用・スコープ・所有者検査・同意の失効・振込の依頼と確認・イベント/Webhook・秘密の非保存のDBテスト