第17巻 API契約定義(3) ― Bank向けAPIと横断仕様

本巻は docs/specs/32_api_contracts.md のZC→Bank Ingress API以降(Bank顧客/行員/着金フィルタAPI、内部API、冪等性・エラーカタログ等の横断仕様)を収める。前巻→第16巻の続き。


ZC→Bank Ingress API(13本・Bank Mockが実装)

SettlementProofRef(bank_proof_ref の一般化)

bank_proof_ref の型は BankProofRef/SettlementProofRef(同一型の別名)。
proof_type の値域(実装の正: src/types/primitives.ts#ProofType:
PAYER_EXEC_PROOF | PAYER_HV_ISOLATION_PROOF | PAYEE_EXEC_PROOF |
NO_DEBIT_RECORDED_PROOF | ONCHAIN_ESCROW_LOCK_PROOF | ONCHAIN_RELEASE_PROOF |
CREDIT_FAILED_PROOF | EXT_REFUND_PROOF)と
既存フィールド(issuer_bank_id, proof_id, recorded_at, custody_detail)は不変。
値の意味の全体像は 30_internal_design.md §12.3.3 を正とする。
proof_type と venue は直交する別軸であり、SettlementProofRef は型の別名であって
proof_type の値ではない(同§の規範)。以下の4フィールドを加法的
(すべて optional/nullable)に追加し、銀行勘定確認以外の証跡型
(日銀ネット/オンチェーン/第三者アテステーション)を同列に扱う。

{
  "issuer_bank_id": "001",
  "proof_type": "PAYEE_EXEC_PROOF",
  "proof_id": "PROOF-...",
  "recorded_at": "RFC3339",
  "custody_detail": null,
  "venue": "BANK_LEDGER",
  "external_ref": null,
  "signer_key_id": null,
  "verified_at": null
}
  • venue: "BANK_LEDGER"(既定・参加行勘定確認)| "IGS_BOJ"(日銀ネット相当)
    | "ONCHAIN"(オンチェーン移転証明)| "ATTESTATION"(第三者アテステーション)
    | "CB_TOKEN"(トークン化中央銀行当座預金での確定)。
    既存の createProof()(参加行勘定確認、src/shared/proof.ts)が生成する
    proof は省略可(実質 BANK_LEDGER)。
    CB_TOKEN は非JPY通貨のファイナリティ・レール:各通貨の中央銀行(ECB / FedNY…)
    がトークン化した当座預金をチェーン上で確定させる。ZC は外国中銀へ直接接続できない
    ため、発行体の署名付き観測(source='CB_TOKEN:{中銀}:{チェーン}')を
    KeyRegistry で検証した証跡のみ受理する。トークン化JPYは BOJ クラシック当座に
    追加するレール(src/shared/central_bank.ts)。
  • external_ref: 当該 venue 内での参照(チェーン上の tx ハッシュ/IGS-ID/
    アテステーションID)。venue=BANK_LEDGER では未使用。
  • signer_key_id: 証跡に署名した外部主体の KeyRegistry.key_id。
  • verified_at: ZC が src/shared/external_signature.ts で署名検証に成功
    した時刻(RFC3339)。

venue != "BANK_LEDGER" の証跡は src/shared/proof.ts の
createSettlementProof() で生成する。このパスは内部で
verifyExternalSignature() を呼ぶため、KeyRegistry 未登録・失効・署名
不正の場合は §エラーカタログの KEY_* / EXTERNAL_SIGNATURE_INVALID /
SIGNATURE_REPLAYED を返す。

全エンドポイントは POST /bank/{bank_id}/zc-ingress/...

共通リクエストヘッダー(ZC の egress 署名は二重受け。X-ZC-Key-Id の有無で判別):

# 非対称署名パス(新方式・優先): X-ZC-Key-Id が在るときこちらが選択される
X-ZC-Key-Id:    KEY-...          # KeyRegistry の署名鍵 key_id(owner_type='ZC')
X-ZC-Sig-Nonce: string           # 鍵ごとに一意な nonce(リプレイ防止)
X-ZC-Sig-Time:  RFC3339          # 署名時刻
X-ZC-Signature: base64           # buildSignedMessage(...) への署名

# HMAC パス(旧方式・後方互換): X-ZC-Key-Id が無いときのフォールバック
X-ZC-Signature: HMAC-SHA256-hex

X-Idempotency-Key: string        # 参考。ingress の冪等判定はボディの request_id(ZcRequests)で行う
Content-Type: application/json

検証は handleBankIngressHttp(src/bank/ingress.ts)が行い、X-ZC-Key-Id
ヘッダーの有無で非対称署名(verifyZcSignature、src/shared/zc_signature.ts)と
HMAC(verifySignature、src/shared/hmac.ts)を判別する。いずれも失敗時は
401 INVALID_SIGNATURE、HMAC パスで署名欠落時は 401 MISSING_SIGNATURE。この
段階的二重受けは §メッセージ・スキーマ進化と互換性政策 で述べる移行パターンの実例である。

POST /bank/:bankId/zc-ingress/reserve-funds

H_RESERVED確保要求

Request:

{
  "request_id": "uuid",
  "txid": "TX-...",
  "amount": { "value": 1200, "currency": "JPY" },
  "account_hash": "h:..."
}

Response OK: { "result": "RESERVED", "reservation_ref": "uuid" }
Response NG: { "result": "ERROR", "reason_code": "INSUFFICIENT_FUNDS" }

POST /bank/:bankId/zc-ingress/execute-debit

a実行指示(PayerExecRequested準拠、30_internal_design.md §13.2)

Request:

{
  "request_id": "uuid",
  "txid": "TX-...",
  "amount": { "value": 1200, "currency": "JPY" },
  "decision_proof_ref": "DP-...",
  "h_reservation": { "reservation_id": "H-...", "mode": "RESERVED" },
  "execution_deadline": "RFC3339",
  "lane": "EXPRESS|...",
  "payer_account_hash": "h:..."
}

Response: { "result": "OK", "bank_proof_ref": { "issuer_bank_id": "001", "proof_type": "PAYER_EXEC_PROOF", "proof_id": "...", "recorded_at": "RFC3339" } }

※ HIGH_VALUEレーンは reserve-funds を経由しないため payer_account_hash を直接渡す

POST /bank/:bankId/zc-ingress/execute-credit

b実行指示(PayeeExecRequested)

Request:

{
  "request_id": "uuid",
  "txid": "TX-...",
  "amount": { "value": 1200, "currency": "JPY" },
  "decision_proof_ref": "DP-...",
  "payee_account_hash": "h:..."
}

Response: { "result": "OK", "bank_proof_ref": { "issuer_bank_id": "002", "proof_type": "PAYEE_EXEC_PROOF", "proof_id": "...", "recorded_at": "RFC3339", "custody_detail": null } }
※ Custody発生時: "custody_detail": { "is_custody": true, "reason_code": "ACCOUNT_CLOSED", "custody_account_ref": "..." }

POST /bank/:bankId/zc-ingress/release-reserve

H_RESERVED解放

Request: { "request_id": "uuid", "txid": "TX-...", "reservation_ref": "uuid" }

Response: { "result": "RELEASED", "reservation_ref": "uuid" }

POST /bank/:bankId/zc-ingress/leg-ready-check

GTID事前レディネス確認

Request:

{
  "request_id": "uuid",
  "gtid": "GT-...",
  "leg_id": "L1",
  "role": "PAYER|PAYEE",
  "amount": { "value": 3000, "currency": "JPY" },
  "account_hash": "h:..."
}

Response: { "result": "OK" } または { "result": "NG", "reason_code": "INSUFFICIENT_FUNDS" }

POST /bank/:bankId/zc-ingress/authority-check

AML/制裁スクリーニング

Request:

{
  "request_id": "uuid",
  "txid": "TX-...",
  "check_type": "INITIAL|RECHECK",
  "vault_ref": "optional"
}

Response: { "result": "OK" } または { "result": "NG", "reason_code": "SANCTIONS_MATCH" }

POST /bank/:bankId/zc-ingress/name-check

名義確認

Request:

{
  "request_id": "uuid",
  "txid": "TX-...",
  "pspr_ref": "optional",
  "account_hash": "h:..."
}

Response: { "result": "MATCH" } または { "result": "MISMATCH", "reason_code": "NAME_MISMATCH" }

POST /bank/:bankId/zc-ingress/account-verify

口座確認(ZCからBankへ照会)

Request:

{
  "request_id": "uuid",
  "verification_id": "V-...",
  "target_account_hash": "h:...",
  "target_account_name": "佐藤 花子"
}

Response: { "result": "MATCHED|MISMATCHED|NOT_FOUND", "match_score": 1.0, "name_provided": "...|null", "fraud_warning": false }

  • match_score は完全一致 1.0 / 編集距離 1 以内 0.8(表記ゆれ吸収)/ それ以外 0.0。
  • 照合対象の名義(name_provided)は要求側が渡した文字列をそのまま返す。行内に保管された
    名義そのもの(かつて本節が actual_name として記載していたもの)は返さない
    ——
    返せば、口座番号を総当たりして名義を収集する経路になる。したがって ZC 側の
    AccountVerifications.target_account_name は本経路では埋まらない。
  • target_account_name を省略した場合は口座存在確認のみとなり MATCHED / match_score: 1.0。
  • 本節の項目名は ZC 側 POST /api/account-verify の項目名と一致しない(ZC が
    target_account_id/name_to_verify を受け、境界で target_account_hash/target_account_name
    へ写す)。両端の型は src/types/api/bank-ingress.ts#BankAccountVerifyIngressRequest /
    #BankAccountVerifyIngressResponse を唯一の宣言とし、呼び手(src/zc/directory/account_verify.ts)と
    受け手(src/bank/ingress/verify.ts)の双方がこれを import する——両端が別々に同名の型を
    宣言していた間、項目名がずれていても型検査が通り、当コマンドは口座を解決できないまま
    出荷されていた。

POST /bank/:bankId/zc-ingress/credit-notify

入金結果通知

Request:

{
  "request_id": "uuid",
  "txid": "TX-...",
  "payee_account_hash": "...",
  "amount": { "value": 1200, "currency": "JPY" },
  "payer_bank_id": "001",
  "payer_name_masked": "タ●●",
  "purpose": "P2P"
}

Response: { "result": "NOTIFIED" }

POST /bank/:bankId/zc-ingress/rtp-notify

RTP請求通知(payee → payer bank)

Request:

{
  "request_id": "uuid",
  "rtp_id": "RTP-...",
  "payee_bank_id": "001",
  "payer_bank_id": "002",
  "amount": { "value": 2000, "currency": "JPY" },
  "expires_at": "RFC3339",
  "payee_name": "田中商店",
  "description": "optional"
}

Response: { "result": "NOTIFIED" }

POST /bank/:bankId/zc-ingress/debit-settled

決済完了通知(払出行=発起行向け)。ZC が TX を SETTLED まで確定させた後に送り、
着金が相手方へ届き end-to-end で最終確定したことを知らせる(全銀将来ビジョン
入金結果通知の払出側)。行内では監査証跡(BankAuditLog)を残すのみ。冪等。

Request:

{
  "request_id": "uuid",
  "txid": "TX-...",
  "amount": { "value": 1200, "currency": "JPY" },
  "payee_bank_id": "002",
  "settled_at": "RFC3339"
}

Response: { "result": "ACKNOWLEDGED", "txid": "TX-..." }

POST /bank/:bankId/zc-ingress/initialize-bank

参加行側の勘定・仕訳の初期化。ZC は参加行登録(Participants)のみを行い、行内勘定
(別段預金 SUSPENSE/ZC清算勘定 SETTLEMENT/現金 ASSET/利益剰余金 EQUITY/日銀預け金
BOJ)と初期仕訳は各行が用意する(設計原則: 金融機関は既存の責務を保持)。冪等
(別段預金が既存なら ALREADY_INITIALIZED)。

Request: { "request_id": "optional uuid", "boj_prefund": 100000000000 }(boj_prefund 省略時は1000億円のBOJプレファンド)

Response: { "result": "INITIALIZED"|"ALREADY_INITIALIZED", "bank_id": "001" }

POST /bank/:bankId/zc-ingress/cleanup-bank

参加行離脱時の行内データ(勘定・仕訳・金利設定・日次残高)削除。ZC側データ
(Participants・ZcRequests・SuspenseDetails 等)は別途ZCが削除する。

Request: {}(ボディ不要)

Response: { "result": "CLEANED_UP", "bank_id": "001" }


Bank 顧客API(顧客向け)

共通ヘッダー: X-Bank-Id: 001, X-Customer-Id: customer_uuid(モック用・認証なし)

GET /bank/:bankId/v1/me/accounts

口座一覧

GET /bank/:bankId/v1/me/accounts/:accountId/balance

残高照会 → { "account_id": "...", "balance": 980000, "currency": "JPY", "as_of": "RFC3339" }

GET /bank/:bankId/v1/me/accounts/:accountId/transactions

取引履歴

POST /bank/:bankId/v1/me/transfers

振込実行(全ZCレーン対応)

Request:

{
  "amount": { "value": 5000, "currency": "JPY" },
  "payee_bank_id": "002",
  "payee_account_hash": "h:...",
  "payee_account_id": "0020000001",
  "lane": "STANDARD",
  "purpose": "P2P",
  "idempotency_key": "uuid",
  "payer_account_id": "optional"
}

payee_account_id 指定時は payee_bank_id を口座番号先頭3桁から自動導出可能。

GET /bank/:bankId/v1/me/transfers/:txid

振込状態照会

GET /bank/:bankId/v1/me/approvals

着金承認リクエスト一覧

Query: ?account_id=...&status=PENDING

POST /bank/:bankId/v1/me/approvals/:approvalId/respond

着金承認への応答

Request: { "approved": true }

承認時はZCにresume_creditを通知(Queue経由)。


Bank 行員API(行員向け)

共通ヘッダー: X-Bank-Id: 001, X-Teller-Id: teller_id(モック用・認証なし)

POST /bank/:bankId/v1/teller/cash/deposit

現金入金

POST /bank/:bankId/v1/teller/cash/withdrawal

現金払い戻し

GET /bank/:bankId/v1/teller/accounts

口座一覧(行員用)

POST /bank/:bankId/v1/teller/accounts

口座作成

POST /bank/:bankId/v1/teller/accounts/batch

口座一括作成

PATCH /bank/:bankId/v1/teller/accounts/:accountId/status

口座ステータス更新(NORMAL/FROZEN/CLOSING_HOLD/CLOSED)

GET /bank/:bankId/v1/teller/accounts/:accountId/journals

口座の仕訳照会

GET /bank/:bankId/v1/teller/journals

全仕訳照会(行全体)

GET /bank/:bankId/v1/teller/suspense

別段預金一覧

POST /bank/:bankId/v1/teller/suspense/:suspenseId/resolve

別段預金収束処理(Custody解消等)

GET /bank/:bankId/v1/teller/batch/status

バッチ処理状態照会

GET /bank/:bankId/v1/teller/audit-log

監査ログ照会

Query: ?txid=TX-...&limit=100


Bank 着金フィルタAPI

GET /bank/:bankId/v1/filters

フィルタ一覧

Query: ?account_id=...

POST /bank/:bankId/v1/filters

フィルタ作成

Request:

{
  "scope": "ACCOUNT",
  "account_id": "0010000001",
  "filter_type": "AMOUNT_LIMIT|SENDER_BLOCK|SENDER_BANK_BLOCK|EDI_PATTERN|REQUIRE_APPROVAL",
  "condition": { "max_amount": 50000 },
  "action": "REJECT|HOLD_CONFIRM|HOLD_MANUAL",
  "description": "5万円超の着金を承認制に"
}

値域の正(実装): src/types/states.ts#FilterType: AMOUNT_LIMIT | SENDER_BLOCK |
SENDER_BANK_BLOCK | EDI_PATTERN | REQUIRE_APPROVAL /
src/types/states.ts#FilterAction: REJECT | HOLD_CONFIRM | HOLD_MANUAL。

DELETE /bank/:bankId/v1/filters/:filterId

フィルタ削除

PATCH /bank/:bankId/v1/filters/:filterId

フィルタ有効/無効切替

Request: { "is_active": false }


内部API(Cron用・外部公開しない)

X-Cron-Secret ヘッダー検証必須。

規範(管理シークレットの取扱い):CRON_SECRET の照合は 定数時間比較

(timingSafeEqualStr)で行い、応答タイミングからの逐次的な秘密復元を防ぐ。

ヘッダ欠落・環境側未設定はいずれも fail-closed(403)で拒否し、空文字どうしの

一致を通過扱いにしてはならない。また CRON_SECRET を クライアントへ配信される成果物
(ダッシュボード HTML/JS 等)へ埋め込んではならない
。埋め込むと view-source で全

/internal/* 操作の認可を回避できてしまう。運用ダッシュボードは秘密を実行時に運用者から取得し

sessionStorage 等に保持する(実装 src/router/internal.ts、src/dashboard/*.html、

回帰 test/invariants/dashboard_secret_guard.test.ts)。

GET /internal/metrics

運用メトリクス(権威状態の読み取り専用射影)。既定は Prometheus テキスト展開形式
(Content-Type: text/plain; version=0.0.4)、?format=json で JSON を返す。書き込みを
行わないため read-only 縮退中でもスクレイプ可能。実装 src/zc/platform/metrics.ts。

POST /internal/cron/eod

EODバッチ手動トリガー

POST /internal/cron/timeout-sweep

タイムアウト巡回手動トリガー

POST /internal/cron/finality-audit

FinalityLog ハッシュチェーン全鎖監査の手動トリガー(通常は EOD バッチに内包)。断絶検知時は CASE へ収束。

Response: { "chains_checked": N, "entries_checked": M, "broken_chains": [...], "cases_opened": K }

POST /internal/seed

初期データ投入(開発用)

POST /internal/dns/kick

DNS手動キック

Request: { "business_date": "YYYY-MM-DD" }(省略時は当日)

POST /internal/dns/settle

DNS手動清算

Request: { "cycle_id": "..." }

POST /internal/dns/intraday-cutoff

日中カットオフ(24/365)。現行の OPEN ウィンドウを締め(kick+settle)、次の日中サイクルを
開く。1日複数回の清算=ローリング清算を可能にする(runIntradayDnsCutoff)。

Request: { "business_date": "YYYY-MM-DD", "currency": "JPY" }(いずれも省略可。既定は当日・JPY。サイクルIDは DNS-{CCY}-YYYYMMDD-NN)

POST /internal/dns/resume

DNS_HOLD(ネット債務者の残高不足による中断)からの再開。ブリッジ流動性の補填後に呼び、resumeDns が当座残高を再チェックして清算を再試行する(10_requirements.md §3.2.5.2 の制度プロトコル)。

Request: { "cycle_id": "..." }

GET /internal/boj-positions

各銀行の日銀預け金勘定(BOJ)残高照会

POST /internal/sim/setup

シミュレーター大規模初期化(20行×200口座)

POST /internal/sim/setup-bank

シミュレーター単行セットアップ

POST /internal/transfers/:txid/resume-credit

着金承認後のクレジット処理再開通知


ダッシュボード・静的ページ

パス 内容
/ /dashboard メインダッシュボード(取引一覧・状態可視化)
/console オペレーションコンソール(Alpine.js + ECharts)
/bank-app 顧客向け銀行アプリ(Alpine.js)
/theater /theatre Settlement Theater — 状態遷移のアニメーション再生
/sky Sky モード(システム俯瞰ビュー)

リクエストトレーシング(横断仕様)

全 HTTP レスポンスは X-Request-Id ヘッダーを必ず返す。エラー時は
レスポンス本文の request_id フィールドにも同値が入り、構造化ログ
(後述)と突合可能になる。

  • リクエスト側が X-Request-Id を付与した場合、その値をそのまま採用する。
  • 付与が無い場合、サーバが req-<uuid> を生成して返す。
  • Cloudflare Queues コンシューマでも同等の logger を初期化し、request_id,
    message_type, txid, gtid, attempt を 1 行 JSON として console
    に出力する(Logpush でそのままパース可能)。

実装: src/shared/logger.ts を参照。詳細は
docs/specs/30_internal_design.md § Observability に集約。


冪等性(横断仕様)

idempotency_key を受け取る全エンドポイント(/api/transfers、
/api/gtid/register、/api/rtp/request、/api/htlc/*、
/api/fx/transfers など)は、同一キーの再送に対して保存済みの応答を
そのまま返す
。ただし HTTP ステータスコードは初回と異なる場合がある
(例: HTLC create は初回 201、再送では 200)。

同一キーが異なるリクエストボディで再使用された場合は、保存済み応答
を返さず 409 IDEMPOTENCY_KEY_CONFLICT を返す。ボディの同一性は
リクエスト全体の SHA-256 ハッシュで判定する(src/shared/idempotency.ts
の resolveIdempotency())。これにより、クライアントが同じキーを使い回し
つつ金額や payee だけ変えてしまうといった実装ミスをしても、古い応答が
意図せずそのまま返ってしまったり、二重実行と誤認されたりすることを防げる。

署名のリプレイ防止など、ボディ比較を伴わない単発キー(例:
sig:{key_id}:{nonce})はこの判定の対象外(ハッシュ未保存、常に
非衝突)。

実装: src/shared/idempotency.ts。


メッセージ・スキーマ進化と互換性政策(横断仕様)

参加者は N 行いても同時にはアップグレードしない。したがって電文・API 契約には
明示的な進化政策が要る。これが無いまま本番へ行くと、最初のスキーマ改訂が最初の
全網障害
になりかねない。実在の決済レールの電文改訂が「年次サイクル・数年前告知」
という重さで動くのは、この「参加者全員が同時にはアップグレードできない」という
問題への答えである。ZC も同じ規律を採る。

方針(3点)

  1. n / n-1 併存受理: サーバは常に現行スキーマ n と直前 n-1 の 2 世代を同時に
    受理する。リクエストは schema_version(PaymentInitiated の必須フィールド。永続化列は
    Transactions.schema_version、31_schema.md § Transactions、既定 '1.0')で自世代を自己申告する。
    新フィールドは加法的・任意(省略時は従来動作)として入れ、破壊的変更は世代境界で
    のみ行う。
  2. 公表された非推奨期限: n-1 を受理し続ける期限を事前に公表する。告知から期限までは
    全参加行が移行できるだけの猶予(実在レール同様、複数年規模)を置く。
  3. 強制移行手続: 期限到来後、n-1 のリクエストは受理を停止し明示拒否する
    (サイレントに古い解釈へ倒さない)。移行が滞る参加行には期限前に個別告知し、接続
    認定試験で n 適合を確認したうえで切替える。

すでに一度やっている実例(署名方式の段階的二重受理)

この「段階的二重受理」を ZC は署名方式の移行で一度通している。ZC の egress 署名は
歴史的に共有 HMAC だったが、非対称署名(KeyRegistry の公開鍵、owner_type='ZC')へ
移行した。検証側(bank ingress、src/bank/ingress/)はX-ZC-Key-Id ヘッダーの有無
だけで新旧を判別する。ヘッダーが在れば非対称パス(verifyZcSignature、
src/shared/zc_signature.ts)、無ければ従来の HMAC パスへフォールバックする(§ZC→Bank
Ingress API の共通ヘッダー参照)。送信側は世代をまたいで切り替えられ、受信側は両方を
無停止で受ける。これは上記「n / n-1 併存受理」を署名という一断面で具体化したものに
ほかならない。

電文スキーマ全体にはこの発想をそのまま一般化する:「新方式の存在を示す自己申告
(署名では X-ZC-Key-Id の有無、電文では schema_version)で世代を判別し、告知した
期限まで両受け、期限で強制移行」。署名で正しく機能しているこのパターンを、契約
バージョニングの既定手続として明文化しておく。


エラーカタログ(横断仕様)

全エンドポイントは失敗時に以下の JSON 形を返す。reason_code は機械可読、
category は HTTP ステータスとリトライ可否を一元化する。

{
  "error":       "human-readable message",
  "reason_code": "H_LIMIT_EXCEEDED",
  "category":    "CONFLICT",
  "details":     { "txid": "TX-001", "requested": 1000, "available": 500 },
  "request_id":  "req-..."
}

カテゴリと HTTP ステータス対応

src/shared/errors.ts の httpStatusOf() がただ一つの真。

Category HTTP Retryable 用途
VALIDATION 400 ✗ 入力不正、フォーマット違反、FATF R.16 違反
AUTH 401 ✗ API キー欠落・HMAC 不一致・Whitelist 拒否
NOT_FOUND 404 ✗ TX/HTLC/GTID/RTP/Account/Proxy/Participant 不在
CONFLICT 409 ✗ 状態ガード・楽観ロック衝突・H 上限超過・名義不一致・サーキット開放
RATE_LIMIT 429 ✓ (backoff) レート制限
INVARIANT 500 ✗ 不変条件違反(バグ)。FinalityLog 改ざん検出、二重記帳など
INTERNAL 500 ✗ 未分類例外。実装漏れ
DOWNSTREAM 502 ✓ 銀行 / IGS / 外部呼び出しの一時的失敗
TIMEOUT 504 ✓ 銀行 / IGS のタイムアウト

Queue コンシューマは Retryable=✓ のみ msg.retry() し、それ以外は
msg.ack() してケース化する(無限ループ防止)。

422 (Unprocessable Entity) について: 上表の category → HTTP 写像は

httpStatusOf()(DomainError 経由)の対応であり 422 を含まない。一方、

一部のエンドポイントは業務ルール違反を httpStatusOf() を介さず 422 で

直接返す(FOUR_EYES_REQUIRED / EVIDENCE_REQUIRED / A_OR_B_CONFIRMED

(POST /api/.../h-unlock-authorize)、FOUR_EYES_REQUIRED / EVIDENCE_REQUIRED /

B_CONFIRMED / WINDOW_EXPIRED / NOT_CORRECTABLE

(POST /api/transfers/:txid/misrecord-correct)、OVER_REVERSAL(POST /api/reversals)、

AMOUNT_BALANCE_MISMATCH(GTID advance)など)。これらは「構文は妥当だが

現在の業務状態では処理不能」を表す意図的な 422 であり、category 写像の

対象外である点に注意(category フィールドには VALIDATION を付す)。

金額の上限について: 金額系フィールド(amount.value / QR amount /

銀行テラー amount / initial_deposit 等)は、各 ingress の入力境界で

MAX_AMOUNT_VALUE(src/shared/constants.ts、1兆=1_000_000_000_000)を

超えると INVALID_AMOUNT で拒否される。これは業務上の上限(tx_amount_limit

/ daily_amount_limit / hv_threshold は別途 Participants 等で管理)では

なく、FXレート乗算など下流の数値演算が Number.MAX_SAFE_INTEGER に近づいて

精度劣化することを防ぐための、不正・異常入力に対するセーフティネットである。

適用対象:POST /api/transfers、POST /api/htlc/create、

POST /api/gtid/register(leg単位)、POST /api/rtp/request、

POST /api/fx/quote・POST /api/fx/transfers、QR generate、

銀行テラー cash/deposit・cash/withdrawal・口座開設 initial_deposit。

主要 reason_code 一覧

reason_code category 発生箇所例
INVALID_REQUEST VALIDATION バリデーション全般
MISSING_FIELD VALIDATION 必須フィールド欠落
INVALID_AMOUNT VALIDATION 金額が負・非整数・上限超過(amount.value は 1兆 (MAX_AMOUNT_VALUE、src/shared/constants.ts) を超えると拒否される)
INVALID_CURRENCY VALIDATION amount.currency が非対応(POST /api/transfersはJPY固定、POST /api/gtid/registerはJPY/USD/EUR/GBP/CHF。上記「GTID register」本文の許可通貨が正)
INVALID_LANE VALIDATION 未知の lane 値
INVALID_STATE VALIDATION クライアント側からの不正な状態指定
INVALID_PROXY_TYPE VALIDATION Proxy 解決時
FATF_R16_VIOLATION VALIDATION クロスボーダーの FATF データ不備
PREIMAGE_MISMATCH VALIDATION HTLC claim 時に hashlock 不一致
EXPIRED VALIDATION RTP・HTLC・QR の有効期限超過
UNAUTHORIZED AUTH API キー欠落
INVALID_HMAC AUTH ZC↔Bank HMAC 検証失敗
WHITELIST_REJECTED AUTH HTLC Auth Whitelist で拒否
ACCOUNT_FROZEN AUTH 口座凍結中の操作
TX_NOT_FOUND NOT_FOUND GET /api/transactions/:txid などで該当無し
HTLC_NOT_FOUND NOT_FOUND HTLC 操作対象不在
GTID_NOT_FOUND NOT_FOUND GTID 操作対象不在
RTP_NOT_FOUND NOT_FOUND RTP 操作対象不在
ACCOUNT_NOT_FOUND NOT_FOUND 口座照会失敗
PROXY_NOT_FOUND NOT_FOUND Proxy 解決失敗
PARTICIPANT_NOT_FOUND NOT_FOUND 参加行未登録
CONCURRENCY_CONFLICT CONFLICT 楽観ロック競合(transitionWithLog strict モード)
STATE_GUARD CONFLICT 状態ガードで遷移不可
IDEMPOTENCY_REPLAY CONFLICT 同一 idempotency_key の再送(既存応答返却)
IDEMPOTENCY_KEY_CONFLICT CONFLICT 同一 idempotency_key を異なるリクエストボディで再使用(409、既存応答は返さない)
ALREADY_PROCESSED CONFLICT 既処理イベントの再投入
H_LIMIT_EXCEEDED CONFLICT H 予約が h_limit を超過(JPYはParticipants、非JPYはParticipantCurrencyLimits)
RESERVE_FAILED CONFLICT 銀行側 reserve-funds が NG
AUTHORITY_CHECK_NG CONFLICT 銀行側 authority-check が NG
NAME_MISMATCH CONFLICT 名義不一致
CIRCUIT_OPEN CONFLICT CircuitBreaker が OPEN
BANK_ERROR DOWNSTREAM 銀行 ingress が 5xx
BANK_TIMEOUT TIMEOUT 銀行 ingress が応答遅延
IGS_ERROR DOWNSTREAM IGS コールバック失敗
ALS_LOOKUP_FAILED DOWNSTREAM ALS(Account Lookup Service)失敗
RATE_LIMITED RATE_LIMIT レート上限
LEGACY_ADAPTER_REQUEST_IN_FLIGHT DOWNSTREAM Legacy adapter: 同一 request_id が別の呼出しで処理中(src/bank/legacy/adapter.ts)
MANDATE_NOT_FOUND NOT_FOUND 委任が存在しない(失効操作・スコープ照合)
DD_MANDATE_NOT_FOUND NOT_FOUND 継続収納契約が存在しない
COLLECTION_NOT_FOUND NOT_FOUND 収納予告が存在しない
CHARGE_REF_ALREADY_COLLECTED CONFLICT 当該費目は既に CONFIRMED_OK(二重収納の防止)
CHARGE_REF_INVALID VALIDATION PERIODIC の構造検証違反、または PR-DD-PERIOD-AHEAD-MAX 超過
BUDGET_RATE_EXCEEDED CONFLICT リセット型の累計枠を超過(時が経てば再び使える)
BUDGET_EXHAUSTED CONFLICT 消尽型の累計枠を使い切った(契約の総量に達した=契約範囲外)
CAP_EXCEEDS_POLICY VALIDATION 契約が宣言した上限が制度上限(PR-DD-*)を超える
SIGNATURE_REQUIRED_FOR_RAISE AUTH 上限の引き上げに顧客署名がない(引き下げには不要)
FROZEN_UNFAVOURABLE_CHANGE CONFLICT 凍結後の不利益変更(増額・前倒し)。減額・取下げは可
NOTICE_PERIOD_TOO_SHORT VALIDATION 予告期間が契約の notice_days_min に満たない
LADDER_MAX_EXCEEDED VALIDATION ラダーの段数が上限を超える
LADDER_RUNG_CANNOT_BE_REALTIME VALIDATION 第 2 段以降を REALTIME にはできない
LATEFEE_EXCEEDS_POLICY VALIDATION 遅延損害金が率または絶対額の上限を超える
REALTIME_NOT_PERMITTED AUTH 即時収納は既定で不許可(開放は制度判断による)
MODE_UNSUPPORTED_BY_PAYER_BANK CONFLICT 払出行のプロファイルが要求モードを提供できない
CHAIN_TAMPERED INVARIANT FinalityLog のハッシュチェーン検証失敗
LEDGER_IMBALANCE INVARIANT Bank 仕訳の借方=貸方が崩れた
IMPOSSIBLE_TRANSITION INVARIANT ALLOWED_TRANSITIONS 不在の遷移
OWNERSHIP_VIOLATION INVARIANT 単一所有者則違反: 行を所有しない当事者が状態遷移を発行(strict:falseでも降格されず無条件throw、src/shared/errors.ts / 30_internal_design.md#single-owner)
KEY_NOT_FOUND NOT_FOUND KeyRegistry に key_id が存在しない(外部署名検証)
KEY_REVOKED AUTH occurred_at が鍵の revoked_at 以降(外部署名検証)
KEY_EXPIRED AUTH 鍵が status!=ACTIVE、または有効期間外(外部署名検証)
EXTERNAL_SIGNATURE_INVALID AUTH KeyRegistry の公開鍵での署名検証失敗
SIGNATURE_REPLAYED CONFLICT 同一 (key_id,nonce) の再使用(外部署名検証)
TIMESTAMP_SKEW VALIDATION occurred_at が許容スキューを超過(外部署名検証)
TEMPLATE_NOT_WHITELISTED AUTH ConditionTemplate が存在しないか ACTIVE でない
ATTESTER_UNAUTHORIZED AUTH 鍵が allowed_attester_scope の範囲外(アテステーション)
ATTESTATION_INVALID VALIDATION statement_hash が sha256 hex 形式でない
ATTESTATION_EXPIRED VALIDATION occurred_at から TTL(既定60分)を超過したアテステーションを利用しようとした
MANDATE_NOT_FOUND NOT_FOUND Mandate(または委任チェーンの祖先)が存在しない
MANDATE_BREACH AUTH amount/purpose/lane がチェーン中いずれかのリンクの許可範囲外
MANDATE_EXPIRED AUTH now がいずれかのリンクの [valid_from, valid_to) 範囲外
MANDATE_REVOKED AUTH いずれかのリンクが revoked_at 以降(失効は遡及しない)
WATCHER_UNAUTHORIZED AUTH 署名検証済み鍵の owner_type が EXTERNAL_RAIL/ATTESTER でない
ANCHOR_NOT_FOUND NOT_FOUND FinalityAnchor に anchor_id が存在しない
CHAIN_NOT_ANCHORED NOT_FOUND アンカー時点で当該チェーンが存在しなかった
COSIGN_ENTRY_NOT_FOUND NOT_FOUND 副署対象チェーンに FinalityLog エントリが無い(GENESIS)
COSIGN_BASIS_NOT_FOUND NOT_FOUND 副署の基準エントリがまだ無い(不可逆点のエントリも、当該チェーンを含むアンカーも存在しない)。tip へフォールバックすると定足数が成立しなくなるため、エラーとして返す
COSIGN_PARTICIPANT_MISMATCH AUTH 署名検証済み鍵の owner が participant_id と一致しない
COSIGN_NOT_APPLICABLE VALIDATION GLOBAL/未分類チェーン、または当該チェーン(TX/GTID/DNS)の当事者でない参加行への副署
SYSTEM_BCP_READ_ONLY DOWNSTREAM ZCが BCP_READONLY(ベンダー障害縮退モード)中で新規の資金移動を受理できない
SYSTEM_QUORUM_LOSS_READ_ONLY DOWNSTREAM ZCが QUORUM_LOSS_READONLY(設計原則10の自動縮退、合意ログ quorum 喪失)中で状態確定を受理できない。回復で自動解除されるため queue は retry で保持
ONCHAIN_TIMELOCK_INVALID VALIDATION cross_chain.onchain_timelock が timelock 以降(ZC側外側タイムロックが先に切れる)
ONCHAIN_CHAIN_CLASS_REQUIRED VALIDATION cross_chain 指定時に onchain_chain_class が無い(確定種別が無いと Watcher 定足数の既定が最弱に落ちるため)
NOT_CROSS_CHAIN VALIDATION cross-chain-lock/onchain-fulfillment を cross_chain_source が NULL のHTLCに対して呼んだ
ONCHAIN_PROOF_MISMATCH VALIDATION onchain-fulfillment の preimage が hashlock に一致しない
ONCHAIN_TIMEOUT VALIDATION クロスチェーンHTLCの onchain_timelock 超過(DECIDED_CANCELへ遷移)
CONDITION_TEMPLATE_NOT_SET VALIDATION claim-by-attestation 対象のHTLCに condition_template_id が設定されていない(プログラマビリティ)
TEMPLATE_MISMATCH VALIDATION claim-by-attestation の template_id がHTLCの condition_template_id と一致しない(プログラマビリティ)
ATTESTATION_NOT_PASS VALIDATION アテステーションの verified_result が PASS でない(HtlcClaimRejectedを記録)
COUNTERPARTY_WINDOW_CLOSED CONFLICT EXPRESS精査時に相手行(payee)の稼働ウィンドウが閉じている。PRECHECKED_SUSPENDEDへ一時停止し、ウィンドウ再開後にタイムアウトスイープが自動再開する(稼働ウィンドウ)
ELIGIBILITY_NOT_ATTESTED AUTH createAuthRequest():ホワイトリストに eligibility_template_id が設定されているが、eligibility_attestation が未提供/verified_result!=='PASS'/検証失敗(受取側起点オーソリ)
PURPOSE_VIOLATION AUTH captureHtlcAuth():HtlcAuthRequests.purpose がホワイトリストの allowed_purposes に含まれない(受取側起点オーソリ)
PROOF_SOURCE_UNTRUSTED AUTH assertTrustedSettlementProof():venue!=='BANK_LEDGER' の SettlementProofRef に signer_key_id/verified_at が無い=署名検証済みでない(決済証跡の信頼アンカー)
CONDITION_EXPR_INVALID VALIDATION condition_expr_json の式木が構造検証に失敗(POST /api/htlc/create・claim-by-conditions・POST /api/conditions/*)
CONDITION_EXPR_NOT_SET VALIDATION claim-by-conditions 対象の HTLC に condition_expr_json が設定されていない
CONDITIONS_NOT_MET VALIDATION 条件式が不成立(状態は HTLC_LOCKED のまま。HtlcConditionsEvaluated を証跡化)
ATTESTATION_EQUIVOCATION CONFLICT 同一 (template, subject) に PASS/FAIL が混在(fail-closed。CASE へ収束)
WATCHER_EQUIVOCATION CONFLICT 同一 (source, external_ref) に矛盾する Watcher 観測(WatcherEquivocationDetected を証跡化し CASE へ収束。20_method_design.md §7.7.2-4)
ONCHAIN_QUORUM_PENDING VALIDATION Watcher 定足数(onchain_min_watchers)未達。HTLC_ONCHAIN_PENDING に留まり OnchainQuorumPending を証跡化(20_method_design.md §7.7.2-2)
ONCHAIN_INSUFFICIENT_CONFIRMATIONS VALIDATION 確認深度ゲート未通過。深度は定足数を構成する相異なる運用主体の申告の最小値を採る(20_method_design.md §7.7.2-3)
INVARIANT_VIOLATION INVARIANT 状態機械・所有権・ゼロサム等の不変条件違反(バグ)。IMPOSSIBLE_TRANSITION / OWNERSHIP_VIOLATION の上位分類
ZC_SIGNING_NOT_CONFIGURED INTERNAL ZC egress 署名鍵が未設定(src/shared/zc_signature.ts)
ZC_SIGNATURE_WRONG_OWNER AUTH ZC egress 署名の検証鍵の owner_type が ZC でない
FX_NO_ROUTE CONFLICT 指定通貨ペアに ACTIVE な見積経路が無い(POST /api/fx/quote・/api/fx/transfers)
FX_QUOTE_EXPIRED CONFLICT 経路上の見積が失効(valid_to 超過・WITHDRAWN)
FX_ALREADY_REFUNDED CONFLICT 払戻済みの FX 送金への claim(POST /api/fx/transfers/:gtid/claim)。かつて FX_QUOTE_EXPIRED を流用していたが、「見積が切れた」と「もう払い戻した」は呼び出し側の次の行動が異なるため分離した
FX_CLAIM_WINDOW_EXPIRED CONFLICT claim 時点で最上流 timelock までの残余が 1 ホップ分(FX_HTLC_HOP_MARGIN_MS)を切っており、決済を開始しても間に合わない。ゲートを取らずに払戻側へ倒す(POST /api/fx/transfers/:gtid/claim)
FX_RATE_MISMATCH VALIDATION 再プライシング後の effective_rate が min_effective_rate より不利(POST /api/fx/transfers)
INVALID_FX_RATE VALIDATION PUT /api/fx/rates のレート値・通貨・有効期限が不正
FX_FXP_ACCOUNT_MISSING VALIDATION fxp_accounts に経路上の全 FXP×全通貨のキーが揃っていない
FX_ROUTE_INCONSISTENT VALIDATION 予約コード(経路の連結性違反)。buildFxEdges が構築時に連結を保証するため現在どこからも投げられない(20_method_design.md §17.5-3)
FX_LIQUIDITY_INSUFFICIENT CONFLICT 予約コード(FXP の流動性不足)。GTID レーンの H 予約失敗パスが汎用的に検出するため現在どこからも投げられない(20_method_design.md §17.5)

DomainError を経由しない reason_code(直接 HTTP 返却)

上表は REASON_CODE_CATEGORY(src/shared/errors.ts)に登録され、categoryOf() →
httpStatusOf() の写像を持つコードである。これとは別に、入力境界で jsonError(status, code, …)
により直接 HTTP を返すコード
が存在する。両者を混同しないため、代表的なものを以下に挙げる。

reason_code HTTP 発生箇所
USE_HTLC_ENDPOINT 422 POST /api/transfers に lane=HTLC(§ POST /api/transfers。副作用の前に拒否)
DAILY_LIMIT_EXCEEDED 422 Participants.daily_amount_limit 超過(アトミック UPDATE の meta.changes=0)
AMOUNT_EXCEEDS_TX_LIMIT 422 Participants.tx_amount_limit 超過
PARTICIPATION_MODE_RECEIVE_ONLY 422 RECEIVE_ONLY 参加行が送金を起票
AMOUNT_BALANCE_MISMATCH 422 GTID の通貨別金額均衡違反(advanceGtid。10_requirements.md §3.2.4-7)
MISSING_LEG_ROLE 422 GTID に PAYER / PAYEE いずれかの leg が存在しない(同上)
OVER_REVERSAL 422 Reversal 累計が元 TX 金額を超過(POST /api/reversals)
APPROVAL_REF_REQUIRED 422 事前承認必須 reason に approval_ref が無い(10_requirements.md §4.3.1)
INVALID_MANDATE_ID 400 mandate_id が MANDATE- で始まらない
PURPOSE_CODE_REQUIRED 403 照会に X-Purpose-Code が無い(§照会の認可)
REQUESTER_UNIDENTIFIED 403 照会の主体が識別できない(同上)
PARTICIPANT_SIGNATURE_REQUIRED 403 鍵登録済みの参加行が署名なしで照会した(同上)
PARTICIPANT_SIGNATURE_INVALID 403 署名不正/他行の鍵/リプレイ(同上)
CROSS_PARTICIPANT_SCOPE 403 参加者が一覧・フィード系を照会した(同上)
FATF_DATA_REQUIRED / FATF_VALIDATION_FAILED 400 クロスボーダーの FATF R.16 データ不備

規範(category の決まり方):jsonError() は、まず categoryOf(reason_code) を引く。

登録済みならその category を用いる。未登録の場合に限り、HTTP ステータスから category を
導出する
(400/422→VALIDATION、401/403→AUTH、404→NOT_FOUND、409→CONFLICT、

429→RATE_LIMIT、502→DOWNSTREAM、504→TIMEOUT)。したがって直接 HTTP 返却のコードは

未登録でも安全に分類される。

ただし DomainError は例外である:throw new DomainError(code, …) の code が未登録だと

HTTP ステータスの手がかりが無く、categoryOf() は INTERNAL(500・retry 不可)へ落ちる。

DomainError に渡す reason_code は必ず REASON_CODE_CATEGORY へ登録すること。

この不変条件は test/invariants/spec_refs.test.ts が機械検査する。

規範(追加時の手順):新規 reason_code を追加する場合は src/shared/errors.ts の
REASON_CODE_CATEGORY と本表を同じ変更で更新する。両者の一致は
test/invariants/spec_refs.test.ts が突合し、片方だけの更新は CI で落ちる。


照会の認可(横断仕様)

要件 S-5(アクセスは目的コードなしに成立しない)と S-7(参加行間の越境参照が構造的に
不可能)は、ひとつの機構として実装する(10_requirements.md §8.5、実装
src/zc/platform/access.ts、対象経路表 src/zc/platform/access_routes.ts)。両者は同じ 2 つの
事実——誰が訊いているかと何を見てよいか——を必要とするため、分けると主体解決が 2 つ生まれて
やがて食い違う。

リクエストヘッダ

X-Purpose-Code: P01|P02|P03|P04|P05|P06|P07   # 必須(`10_requirements.md` §3.3.2.2.1)
X-Bank-Id:      001                            # 参加者スコープ
X-Cron-Secret:  <CRON_SECRET>                  # 運営スコープ(両方あれば運営が優先)

# 参加者の主体認証(当該行に ACTIVE な PARTICIPANT 鍵があるときは必須)
X-Participant-Key-Id:    KEY-001                # KeyRegistry(owner_type='PARTICIPANT')
X-Participant-Sig-Nonce: string                 # 鍵ごとに一意(リプレイ防止)
X-Participant-Sig-Time:  RFC3339                # 署名時刻(スキュー検査)
X-Participant-Signature: base64                 # 下記ペイロードへの署名

参加者の主体認証(規範)

X-Bank-Id 単独は主張であって身元ではない。 誰でも他行の ID を書けるため、これだけでは
当事者判定(S-7)が飾りになる。参加者は KeyRegistry(owner_type='PARTICIPANT')の鍵で
リクエストに署名して主体を証明する——ZC egress 署名(shared/zc_signature.ts)の鏡像である。

署名対象は次の 4 項目(buildSignedMessage の payload。30_internal_design.md §12.6 と同じ正規化)。

{ "method": "GET", "path": "<資源識別子>", "bank_id": "001", "purpose_code": "P01" }

path と purpose_code を署名に含めるのは、ある照会で得た署名を別の照会へ付け替えられない
ようにするためである。含めなければ、一度許可された署名が nonce の有効な間だけ「何でも読める券」に
なる。

強制は鍵の登録状態で決まる(規範):当該参加行に ACTIVE な PARTICIPANT 鍵が 1 本でもあれば、
署名は必須
(欠落は 403 PARTICIPANT_SIGNATURE_REQUIRED)。鍵が無い参加行は従来どおり
X-Bank-Id の申告で読めるが、アクセス監査台帳には未認証として記録される
(subject_id に (unauthenticated) を付す)。

なぜ「ヘッダの有無」で切り替えないか:ZC egress の署名移行は X-ZC-Key-Id の有無で新旧を

両受けしている(§メッセージ・スキーマ進化)。あれは送信側の移行であり、世代を選ぶのは

送信者自身なので正しい。受信側の真正性検査を呼び出し側の任意で切れるようにしたら、
検査していないのと同じ
である。したがってここでは切替の権限をレジストリ側に置き、

鍵を登録した参加行から順に強制が有効になる形にした(参加行ごとの段階移行であり、

一斉切替の日を作らない)。

鍵の所有者検査:署名が通っても、その鍵が owner_type='PARTICIPANT' かつ
owner_ref == X-Bank-Id でなければ拒否する(403 PARTICIPANT_SIGNATURE_INVALID)。
これが無いと、登録済みのアテスターや Watcher の鍵で任意の参加行になりすませる。

鍵の登録・失効は制度行為であり、4 眼承認の統制に従う(10_requirements.md §3.3.4)。

判定と応答

状況 応答 なぜその番号か
目的コードが無い/未知の値 403 PURPOSE_CODE_REQUIRED 参照する前に拒否するため、応答は対象の存在を漏らさない
主体が識別できない 403 REQUESTER_UNIDENTIFIED 同上
鍵登録済みの参加行が署名を付けない 403 PARTICIPANT_SIGNATURE_REQUIRED 主張した行であることを証明していない=その行ではない
署名が不正/他行の鍵/リプレイ 403 PARTICIPANT_SIGNATURE_INVALID 同上。DataAccessViolationDetected を記録する——登録鍵に対する不正署名はなりすましの形そのもの
参加者が当事者でない 404 NOT_FOUND(存在しない場合と同一本文) 403 は「在るがあなたのものではない」と答えてしまう。それは S-7 が禁じた越境の事実そのもの(20_method_design.md §9.4.4 (B)-1 と同じ論理)
参加者が一覧・フィードを要求 403 CROSS_PARTICIPANT_SCOPE 10_requirements.md §3.3.2.2.3 が参加者の照会を「取引ID/CASE ID/当事者キー」に限定している。黙って絞り込むのではなく拒否する——絞り込みの実装漏れは静かに漏れる
運営スコープ 許可(横断可) 監督・運用の職務。すべて監査台帳に残る

当事者の定義:取引=payer/payee 行、GTID=全レッグの行、HTLC=payer/payee 行、
受取側起点オーソリ=payer/payee 行、CASE=紐づく取引・GTID の当事者、Reversal=元取引の当事者、
Circuit Breaker(個別)=当該行自身。取引の派生ビュー(/events・/explain・/story・
/verify・/reversals)は元取引と同じ判定に従う。

公開のまま残す照会:GET /api/dns/:business_date/status(全参加主体向けの公式ステータス)、
/api/banks、OpenAPI 仕様書等。公開である理由を経路表に明記しており、理由の無い除外は
test/invariants/query_access.test.ts が落とす。同テストは、新しい GET /api/… が経路表にも
公開一覧にも無い
場合にも落ちる——認可漏れは「書き忘れ」で起きるため、書き忘れ自体を検出する。

閉域照会は別規範:GET /api/dns/:business_date/hold_detail は拒否を一律 404 に統一する
(HOLD の有無自体が閉域情報であるため)。上表の 403/404 の使い分けは適用しない。

監査台帳:許可・拒否のいずれも AccessAuditLog に記録する(31_schema.md § AccessAuditLog)。
台帳の書込み失敗で照会を失敗させてはならない(可用性要件 A-2 との衝突を避ける)。

未充足:鍵を登録していない参加行は申告のみで読める(移行のための意図的な経路)。

系として「参加行が他行を騙れない」と言えるのは全参加行が鍵を登録し終えた時点であり、

それは制度側の移行工程(登録の督促と期限)に属する。機構・強制・監査は揃っている——

残っているのは運用(全行の鍵登録)であって設計ではない

(30_internal_design.md 第10章 Roadmap)。


状態 reason_code(横断仕様)

reason_code という語は本書群で 3 つの異なる値空間を指す(10_requirements.md 序章
§ reason_code の 3 つの値空間)。上の
§ エラーカタログ が定めるのは エラー reason_code——要求が拒否された理由——だけである。
本節は残る 2 つ、すなわち 状態 reason_code(Transactions.reason_code 列)と
CASE reason_code(Cases.reason_code 列)を扱う。

位置づけ

  • 説明するもの:受理された取引が いま その状態にある理由。「なぜ止まっているのか」「なぜ取り消されたのか」。
  • 載る場所:GET /api/transactions/:txid の reason_code、FinalityLog の payload、Cases.reason_code。
  • HTTP ステータスを持たない。 したがって REASON_CODE_CATEGORY への登録は要求しない
    (登録済みの値と綴りが一致することはあるが、それは共用であって規約ではない)。
  • 窓口の説明はこの空間に紐づける(20_method_design.md §10.4.1.1)。

主要な値(レーン・局面別)

reason_code 付く局面 遷移
SUSPEND_NAMECHECK_PENDING 名義確認の応答待ち PRECHECKED → PRECHECKED_SUSPENDED
SUSPEND_AUTHORITY_PENDING AML/制裁照会(Authority Check)の応答待ち。判定不能の時点で PRECHECKED のまま先置きし、T_auth 超過で遷移する(20_method_design.md §3.3.1) PRECHECKED → PRECHECKED_SUSPENDED
SUSPEND_EXEC_TIMEOUT Decision 後、a が期限内に成立しない DECIDED_TO_SETTLE → SUSPENDED
SUSPEND_PAYEE_PROOF_TIMEOUT a 成立後、b が期限内に成立しない PAYER_EXEC_CONFIRMED → SUSPENDED
FAILED_EXEC_TIMEOUT SUSPENDED の滞留が上限を超えた SUSPENDED → FAILED_EXECUTION
IGS_FAILED 中銀決済が HOLD または不成立で返った(両者を区別しない。下記の注意) PAYER_EXEC_CONFIRMED → SUSPENDED
BOJ_INSUFFICIENT_FUNDS HIGH_VALUE 受付時に中銀当座残高が不足 PRECHECKED → DECIDED_CANCEL
DNS_HOLD_IGS_STOPPED DNS_HOLD 中で igs_mode=STOP(全件停止) PRECHECKED → PRECHECKED_SUSPENDED
DNS_RINGFENCED igs_mode=RINGFENCED で原因行として隔離 同上
DNS_IGS_THROTTLED igs_mode=RINGFENCED_PLUS で公平性予算を超過(Defer) 同上
INSUFFICIENT_FUNDS 参加行側の残高不足 PRECHECKED → DECIDED_CANCEL
CANCEL_BY_PAYER 支払人による取消 → DECIDED_CANCEL
TIMELOCK_EXPIRED HTLC の timelock 到来 HTLC_LOCKED → DECIDED_CANCEL
RECHECK_AUTHORITY_NG claim 直前の AML 再照会が NG HTLC_LOCKED → DECIDED_CANCEL
RECHECK_AUTHORITY_UNAVAILABLE claim 直前の AML 再照会に答えが返らない(回路 OPEN 等)。claim を拒否するが取消はせず、再試行に委ねる(30_internal_design.md §15.4) 遷移なし(証跡のみ)
INVALID_PREIMAGE preimage 不一致(状態は維持し、証跡のみ残す) 遷移なし
MISRECORD_CORRECTED 誤記録訂正(10_requirements.md §4.4) 追記のみ
SUSPEND_ADAPTER_DOWN 参加行 Adapter へ到達できない(Circuit Breaker が OPEN)。「相手が拒否した」EXEC_*_FAILED とは別値——CASE 集約(20_method_design.md §10.9.3.6)がこの区別に依存する → SUSPENDED
EXEC_DEBIT_FAILED / EXEC_CREDIT_FAILED 参加行に届いたうえで実行が失敗した → SUSPENDED
CREDIT_FAILED_PROOF_REQUIRED Reversal 起票に物理的不能の証明が無く CASE へ収束(10_requirements.md §4.3.0) CASE 起票

注意(IGS_FAILED は HOLD と不成立を区別しない):中銀決済が HOLD(一時的な流動性

不足。再試行で解消しうる)で返った場合も、FAILED(不成立)で返った場合も、取引に付く

状態 reason_code は 同一の IGS_FAILED である。両者を分けるのは IgsRequests.status

であって取引側の reason_code ではない。したがって 窓口が「待てば済むのか、済まないのか」を
reason_code だけで判断してはならない
——判断材料は照会応答の external_settlement

({status, retriable})である(20_method_design.md §9.4.4.1 (A))。

命名規約(規範)

状態 reason_code は 2 つの family に分かれる。どちらの family かで綴りを決める。

family いつ使うか 綴り 例
待機理由 何かを待っている——時間経過または相手の応答で解ける。窓口の答えは「待てば進む」 SUSPEND_*。待ちが解けずに終わったときは CANCEL_*_TIMEOUT(=解けなかった待ちの帰結を接頭辞にする) SUSPEND_NAMECHECK_PENDING・SUSPEND_EXEC_TIMEOUT・SUSPEND_ADAPTER_DOWN・CANCEL_PRECHECK_TIMEOUT
事象理由 確定した事実が起きた——待っても変わらない。窓口の答えは「この理由で止まった/終わった」 事象そのものの名前(接頭辞を付けない) IGS_FAILED・BOJ_INSUFFICIENT_FUNDS・DNS_RINGFENCED・TIMELOCK_EXPIRED

規範

  1. 遷移先を名前に含めない(事象理由)/含める(待機理由)という上表の使い分けを守る。
    遷移先そのものは state 列が既に持っているので、事象理由にまで接頭辞を付けると重複になり、
    CANCELLED の行に SUSPEND_* が載るような矛盾する組み合わせを作れてしまう。
    • CANCEL_ で始まる値がすべて待機理由なのではない。 待機理由に属するのは
      解けなかった待ちの帰結、すなわち CANCEL_*_TIMEOUT の形だけである
      (CANCEL_PRECHECK_TIMEOUT)。一方 CANCEL_BY_PAYER は支払人が取り消したという確定した
      事実
      であり、待ちではない——待機理由の綴りを共有しているが事象理由に属する。
      接頭辞ではなく「待っていたのか、起きたのか」で family を決める、が上表の趣旨である。
  2. エラー reason_code を状態 reason_code として転用しない。 両空間は別物である
    (10_requirements.md 序章)。転用すると、窓口が見る値に「API 呼び出しが失敗した理由」が
    混入する。かつて Adapter 不通が CIRCUIT_OPEN(エラー空間の値)で表現されず
    EXEC_DEBIT_FAILED に潰れていたのは、この線引きが無かったためである
    (現在は待機理由 SUSPEND_ADAPTER_DOWN を用いる)。
  3. 「相手が拒否した」と「相手に届かなかった」を同じ値にしない。 前者は事象理由、
    後者は待機理由であり、顧客への説明も運用の打ち手も異なる。

規範

  1. 本表は網羅ではない。 状態 reason_code は局面ごとに追加され得るため、本表は窓口・運用が
    分岐に使う主要値
    を固定する。網羅的な現行値は実装(Transactions.reason_code に書き込む
    全箇所)を正とする。
  2. 本書群が規範として名指しする状態 reason_code は、実在する値でなければならない。
    規範として先に固定したが未実装の値は、【未実装】 を付して明示する(30_internal_design.md §10.0)。
    この不変条件は test/invariants/spec_refs.test.ts が機械検査する。
  3. 遷移の条件を状態 reason_code で書かない。 reason_code は人間向けの説明ラベルであり、
    状態機械のガードではない。遷移を縛るのは状態そのもの(ALLOWED_TRANSITIONS)と、
    external_settlement_status のような専用の判定列である(20_method_design.md §3.2.1)。

PDFを作成

フォントは初回だけ読み込むため、1回目は時間がかかります。

用紙
組み方向
表紙
本文