第15巻 API契約定義(1) ― 参加行→ZC受付とクロスカレンシーFX

本巻は docs/specs/32_api_contracts.md のZC Core API冒頭〜口座確認・EDI・Proxy・QR・RichDataまでを収める。続きは→第16巻。


30_internal_design.md 第12章(I/F契約:§12.1 cmd/event 正本、§12.4 冪等キー、§12.6 署名)・第13章(メッセージ定義)を踏まえたエンドポイント一覧。
実装の正は稼働中のコードそのもの。データ定義の正は 31_schema.md、API契約の正は本ファイルである。


ZC Core API

参加行→ZC 受付

POST /api/transfers

PaymentInitiated(30_internal_design.md §13.1 準拠)

Request:

{
  "schema_version": "1.0",
  "message_type": "EVENT",
  "name": "PaymentInitiated",
  "message_id": "uuid",
  "idempotency_key": "string",
  "occurred_at": "RFC3339",
  "txid": "TX-...",
  "lane": "EXPRESS|STANDARD|BULK|DEFERRED|RTP|HTLC|HIGH_VALUE",
  "amount": { "value": 1200, "currency": "JPY" },
  "payer": { "bank_id": "001", "account_hash": "h:...", "vault_ref": "optional" },
  "payee": { "bank_id": "002", "account_hash": "h:optional", "vault_ref": "optional" },
  "purpose": "MERCHANT|P2P|BILL|SALARY|REFUND",
  "pspr_ref": "optional",
  "expires_at": "RFC3339",
  "is_cross_border": 0,
  "fatf_data": { "...FATF R16フィールド..." },
  "proxy_type": "optional",
  "proxy_value": "optional",
  "qr_ref": "optional",
  "mandate_id": "optional, e.g. MANDATE-..."
}

Response(レーン別・同期):

指定 lane HTTP result state 語義
EXPRESS 200 DECISION_ACCEPTED DECIDED_TO_SETTLE Decision(Raftコミット)まで完了し decision_proof_ref が発行済み(20_method_design.md §2.1)
STANDARD / BULK / DEFERRED 200 INGRESS_ACCEPTED RECEIVED RECEIVED が FinalityLog へ永続化済み。Decision は未確定
RTP 200 INGRESS_ACCEPTED RECEIVED 同上(請求起点は POST /api/rtp/request。本エンドポイントは Attempt 成功後の実行 TX を受ける)
HIGH_VALUE 200 INGRESS_ACCEPTED RECEIVED 受理まで同期。以降は HIGH_VALUE フロー(H_RESERVED を経由しない)
HTLC 422 — — 本エンドポイントでは受理しない。USE_HTLC_ENDPOINT を返す(下記)

lane=HTLC を本エンドポイントで受理しない理由(規範): HTLC の受付には

hashlock / timelock(およびクロスチェーン脚では cross_chain)が不可欠だが、

PaymentInitiated のボディはこれらを運べない。したがって HTLC レーンの入口は

POST /api/htlc/create のみとし、本エンドポイントに lane=HTLC が来た場合は

422 USE_HTLC_ENDPOINT で拒否する。

拒否は副作用の前に行う(規範): この拒否は、冪等キーの確保・daily_amount_used の

加算・Transactions 行と PaymentInitiated の書き込みの いずれよりも前 に行わなければ

ならない。後から拒否すると、(a) 日次上限の枠を消費したまま取引が成立しない、(b) 誰も前進

させない RECEIVED 行が残る、という 2 つの説明できない状態が生じる(設計原則4 違反)。

実装は src/zc/ingress/transfers.ts(バリデーション直後のガード)。

GTID は本エンドポイントの lane 値ではない(POST /api/gtid/register が 201 GTID_ACCEPTED /
state: GT_RECEIVED を返す)。HTLC_AUTH も lane 値ではなく、lane=HTLC 上のフローである
(10_requirements.md 序章「レーンと lane 列の関係」)。

LaneType に HTLC が残る理由: 上表のとおり本エンドポイントは lane=HTLC を拒否するが、

HTLC は依然として API リクエスト値の値域(src/types/states.ts#LaneType)と
Transactions.lane 列の値
である——POST /api/htlc/create が作る行は lane='HTLC' を持つ。

拒否されるのは「POST /api/transfers という入口での指定」であって、レーンそのものの

存在ではない。GTID(リクエスト値ではない)とは性質が異なるので混同しないこと。

HIGH_VALUE 自動エスカレーション(規範・lane 書換え可能性): amount.value が閾値(PR-HV-THRESHOLD。参加行個別 Participants.hv_threshold → 環境変数 ZC_HV_THRESHOLD → 既定 1 億円の順で解決、30_internal_design.md §12.9)以上の場合、リクエストが lane=EXPRESS/STANDARD を指定していても ZC は受付時点で lane を HIGH_VALUE に書き換える(10_requirements.md §1.2.4・§3.2.7)。書換えは 2 通りで観測できる。(1) 同期応答:EXPRESS 指定なら本来 {result: "DECISION_ACCEPTED", state: "DECIDED_TO_SETTLE"} が返るところ、書換え後は {result: "INGRESS_ACCEPTED", state: "RECEIVED"} になる。(2) 照会 API(GET /api/transactions/:txid):lane が HIGH_VALUE となり、state 遷移経路も PRECHECKED → DECIDED_TO_SETTLE(H_RESERVED を経由しない)を辿る。FinalityLog の PaymentInitiated には書換え後の lane が記録される。

バリデーション:

  • tx_amount_limit チェック(Participantsテーブル)
  • daily_amount_limit チェック(アトミック UPDATE + meta.changes=0 パターン)
  • クロスボーダー送金時は FATF R16 バリデーション(全レーン対象)
  • mandate_id を指定する場合は MANDATE- で始まること(INVALID_MANDATE_ID)

mandate_id(エージェンティック・コマース/委任チェーン):

  • EXPRESS / STANDARD のプレチェック時に assertMandateValid(db, mandate_id, {amount, purpose, lane}, now)
    (src/shared/mandate.ts、31_schema.md § Mandate)で委任チェーンを検証する。
  • MANDATE_NOT_FOUND / MANDATE_REVOKED / MANDATE_EXPIRED / MANDATE_BREACH の
    いずれかが発生した場合、即時拒否ではなく PRECHECKED_SUSPENDED へ遷移し、
    当該 reason_code で Cases を1件 open する(人/オペレーションによる解除待ち)。
  • mandate_id を指定しないリクエストはこのチェックをスキップし、従来通り処理される。

POST /api/htlc/create

HTLC新規作成

Request:

{
  "htlc_id": "HTLC-...",
  "hashlock": "sha256hex",
  "timelock": "RFC3339",
  "amount": { "value": 5000, "currency": "JPY" },
  "payer_bank_id": "001",
  "payer_account_hash": "...",
  "payee_bank_id": "002",
  "payee_account_hash": "...",
  "idempotency_key": "string",
  "cross_chain": {
    "source": "ONCHAIN:ETH",
    "onchain_timelock": "RFC3339"
  },
  "condition_template_id": "TPL-..."
}

cross_chain(任意、クロスチェーンHTLC)を指定すると、同じ
hashlock が source 下のオンチェーンエスクローもロックする。
cross_chain.onchain_timelock は timelock より厳密に前である必要があり、
そうでなければ ONCHAIN_TIMELOCK_INVALID で拒否される(ZC側の外側
タイムロックは常にオンチェーン側より長く保持される)。

cross_chain を指定する場合、onchain_chain_class は必須である
(ONCHAIN_CHAIN_CLASS_REQUIRED)。Watcher 定足数の既定は確定種別から導かれる
ため(20_method_design.md §7.7.2)、種別を欠いた脚は最弱の既定=単一 Watcher に
落ちる。min_watchers を明示しても代替にならない——それは人数を決めるだけで、
確認深度が意味を持つ鎖かどうかを決めない。

condition_template_id(任意、プログラマビリティの汎用化):
TPL- で始まる文字列(INVALID_TEMPLATE_ID)。指定すると、このHTLCは
claim(preimage提示)に加えて claim-by-attestation(当該テンプレートに
対するPASSアテステーション提示)でもfulfillできるようになる。ZCは
ConditionTemplateが実際にACTIVE/whitelistedかをこの時点では検証しない
(claim-by-attestation時に検証)。

POST /api/htlc/:htlc_id/claim

preimage提示(30_internal_design.md §13.5)

Request: { "htlc_id": "...", "preimage": "secret_hex", "idempotency_key": "string" }

POST /api/htlc/:htlc_id/claim-by-attestation

署名付き成立証明(Attestation)によるHTLC fulfill(プログラマビリティの汎用化)。

「検品完了」「書類充足」「対象者該当」といった条件をZCがコードで
判定するのではなく、condition_template_idで指定されたwhitelist済み
ConditionTemplate(30_internal_design.md §15.5)に対する、KeyRegistry登録済みattesterの
署名付き成立証明(verified_result: "PASS")の提示に還元する。
preimage提示(claim)と同じ HTLC_LOCKED → HTLC_FULFILL_REQUESTED →
DECIDED_TO_SETTLE 状態遷移を共有し、状態名は流用する。

Request:

{
  "htlc_id": "HTLC-...",
  "template_id": "TPL-...",
  "statement_hash": "sha256hex",
  "verified_result": "PASS",
  "attester_key_id": "KEY-...",
  "nonce": "string",
  "occurred_at": "RFC3339",
  "signature": "base64",
  "idempotency_key": "string"
}

Response: { "result": "ACCEPTED"|"REJECTED", "htlc_id": "...", "state": "...", "reason_code"?: "..." }

検証順序とreason_code:

  • htlc_id未存在: HTLC_NOT_FOUND
  • 対象HTLCにcondition_template_idが設定されていない: CONDITION_TEMPLATE_NOT_SET
  • template_idがcondition_template_idと一致しない: TEMPLATE_MISMATCH
  • 状態がHTLC_LOCKEDでない: INVALID_STATE
  • ZC側timelock超過: TIMELOCK_EXPIREDでDECIDED_CANCELへ遷移
  • recordAttestation(30_internal_design.md §15.5・src/shared/attestation.ts)によるwhitelist/署名/scope検証:
    TEMPLATE_NOT_WHITELISTED / ATTESTATION_INVALID /
    ATTESTER_UNAUTHORIZED / KEY_* / EXTERNAL_SIGNATURE_INVALID /
    TIMESTAMP_SKEW / SIGNATURE_REPLAYED
  • アテステーションの鮮度切れ(30_internal_design.md §15.5.2、既定 60 分): ATTESTATION_EXPIRED
  • verified_result !== "PASS": ATTESTATION_NOT_PASS(HtlcClaimRejectedを
    FinalityLogに記録、状態はHTLC_LOCKEDのまま)

ZCは下層の条件が実際に満たされたかどうかを判断しない。attesterの署名・
whitelist・scope・鮮度のみを検証し、verified_result === "PASS"を
そのまま成立条件として受理する。

POST /api/htlc/:htlc_id/claim-by-conditions

condition_expr_json(AND/OR/THRESHOLD 式木)による HTLC fulfill。
各リーフテンプレートを満たすか判定し、式が成立したときのみ
HTLC_LOCKED → HTLC_FULFILL_REQUESTED → … へ進む(HtlcConditionsEvaluated
を必ず証跡化)。リーフの満たし方は以下の 2 系統:

  • Attestation テンプレート:提示された署名付き Attestation を
    recordAttestation で検証。ConditionTemplate.min_attester_quorum(既定1)
    に従い、distinct な attester operator(KeyRegistry.owner_ref)の鮮度内
    PASS
    が k 個揃って満たされる(k-of-n、Watcher の onchain_min_watchers
    と同型)。同一 operator の複数鍵は 1 と数える。同一 (template, subject) に
    PASS/FAIL が混在する equivocation は fail-closed(当該テンプレは不成立)
    とし、ATTESTATION_EQUIVOCATION の CASE へ収束(他の独立枝での成立は妨げない)。
  • 決定的述語テンプレート(ledger_predicate_json 非NULL):外部表明を
    取らず、ZC が決定的に解決。種別は TX_REACHED_STATE /
    GTID_REACHED_STATE(確定 FinalityLog の状態)、TIME_AFTER / TIME_BEFORE
    (システム時刻に対する時刻ゲート)。Attestation の提示は無視する。時刻は
    JST(システム時刻)で、at にオフセットが無ければ JST とみなす。

Request: { "htlc_id", "attestations": [ {template_id, statement_hash, verified_result, attester_key_id, nonce, occurred_at, signature} ], "idempotency_key" }(attestations は Attestation テンプレート分のみ。Ledger 述語のみの式では空配列可)

Response: { "result": "ACCEPTED"|"REJECTED", "htlc_id", "state", "reason_code"? }

  • 式不成立: CONDITIONS_NOT_MET(状態は HTLC_LOCKED のまま)
  • 式未設定: CONDITION_EXPR_NOT_SET / 破損: CONDITION_EXPR_INVALID

条件・マンデートのドライラン(読み取り専用)

副作用なし(書き込み・資金移動・Attestation 記録なし)。

  • POST /api/conditions/validate — { condition_expr } → { valid, error?, required_templates }
  • POST /api/conditions/simulate — { condition_expr, satisfied?: string[] } → { valid, met, required_templates, satisfied, missing }(不正式は400 CONDITION_EXPR_INVALID)
  • POST /api/mandates/check — { mandate_id, amount?, purpose?, lane?, now? } → checkMandate 結果 { ok, reason_code?, message? }

POST /api/mandates/:mandate_id/revoke

委任の失効。Mandate.revoked_at を設定し、以後 assertMandateValid が MANDATE_REVOKED を返すようにする。冪等(再送は already: true)。

Request: { "reason"?: "string" }

Response: { "result": "REVOKED", "mandate_id": "MANDATE-...", "revoked_at": "RFC3339", "already": false }

  • 失効は遡及しない。 assertMandateValid は now >= revoked_at で判定するため、失効前に成立した指図は影響を受けない(31_schema.md § Mandate 検証フロー 2)。
  • 委任チェーンの親を失効させると、子も MANDATE_REVOKED として弾かれる(子は親のスコープを継承するため)。個別に子を失効させる必要はない。
  • 継続収納契約(DebitMandate)が参照している委任を失効させた場合、当該契約も失効する。

エラー: 404 MANDATE_NOT_FOUND。

背景:revoked_at 列と失効判定は当初から実装されていたが、それを書き込む本番経路が存在しなかった(テストが直接 SQL を叩くのみ)。継続収納は「顧客がいつでも引落を止められる」ことを制度の前提に置くため、この穴を先に塞ぐ。

POST /api/htlc/:htlc_id/cross-chain-lock

クロスチェーンHTLC: オンチェーンエスクローのロックをWatcherが観測した
ことを記録する(クロスチェーンHTLC、Watcher専用)。HTLC_LOCKED →
HTLC_ONCHAIN_PENDING(CrossChainLockedイベント)。ZCはチェーンを
直接検査せず、KeyRegistry(31_schema.md § KeyRegistry)に登録されたWatcherの署名付き
観測のみを受理する。

Request:

{
  "htlc_id": "HTLC-...",
  "external_ref": "0x...",
  "watcher_key_id": "KEY-WATCHER-...",
  "nonce": "string",
  "occurred_at": "RFC3339",
  "signature": "base64",
  "idempotency_key": "string"
}

Response: { "result": "ACCEPTED"|"REJECTED", "htlc_id": "...", "state": "...", "reason_code"?: "..." }

対象でない/存在しない/状態不正の場合は NOT_CROSS_CHAIN /
HTLC_NOT_FOUND / INVALID_STATE を返す。

POST /api/htlc/:htlc_id/onchain-fulfillment

クロスチェーンHTLC: オンチェーンエスクローのプリイメージ公開を
Watcherが観測したことを記録する(クロスチェーンHTLC、Watcher専用)。
HTLC_ONCHAIN_PENDING → HTLC_FULFILL_REQUESTED → DECIDED_TO_SETTLE
→ ...(OnchainProofObservedイベント)。同じhashlockがZC側・
オンチェーン側の両レッグをアンロックするため、claimHtlcと同一の
決済シーケンスが実行される。

Request:

{
  "htlc_id": "HTLC-...",
  "external_ref": "0x...",
  "preimage": "secret_hex",
  "watcher_key_id": "KEY-WATCHER-...",
  "nonce": "string",
  "occurred_at": "RFC3339",
  "signature": "base64",
  "idempotency_key": "string"
}

Response: { "result": "ACCEPTED"|"REJECTED", "htlc_id": "...", "state": "...", "reason_code"?: "..." }

  • preimageがhashlockに一致しない場合: ONCHAIN_PROOF_MISMATCH
    (Watcherの署名/nonceを消費する前に拒否)。
  • オンチェーン側内側タイムロック超過: ONCHAIN_TIMEOUTで
    DECIDED_CANCELへ遷移。
  • ZC側外側timelock超過: 既存のTIMELOCK_EXPIREDでDECIDED_CANCELへ遷移。

POST /api/htlc/:htlc_id/capture

受取側キャプチャ(オーソリ型HTLC専用)

Request: { "idempotency_key": "string" }

POST /api/htlc/:htlc_id/void

受取側ボイド(オーソリ型HTLC取消)

Request: { "idempotency_key": "string" }

POST /api/htlc/auth-request

受取側起点オーソリリクエスト

Request:

{
  "auth_id": "AUTH-...",
  "payee_bank_id": "001",
  "payee_account_hash": "...",
  "payer_bank_id": "002",
  "payer_account_hash": "...",
  "amount": { "value": 3000, "currency": "JPY" },
  "purpose": "MERCHANT",
  "description": "商品名等",
  "auth_expires_at": "RFC3339",
  "capture_expires_at": "RFC3339",
  "idempotency_key": "string",
  "eligibility_attestation": {
    "statement_hash": "...",
    "verified_result": "PASS",
    "attester_key_id": "KEY-...",
    "nonce": "...",
    "occurred_at": "RFC3339",
    "signature": "base64"
  }
}
  • 期限は絶対時刻(RFC3339)で渡す。相対秒(かつて本節が記載していた
    auth_timeout_seconds / capture_timeout_seconds)は受理しない——受付側と発行側の
    時計差が期限の解釈差になり、キャプチャ可否が観測者によって変わってしまうため。
  • 必須: auth_id / payee_bank_id / payee_account_hash / payer_bank_id /
    payer_account_hash / amount / auth_expires_at / capture_expires_at /
    idempotency_key。欠落は 400 MISSING_FIELDS。
  • eligibility_attestation(省略可・条件付き必須): 対象のホワイトリスト
    (payee_bank_id/payee_account_hash)に eligibility_template_id が
    設定されている場合は必須。recordAttestation() / assertAttestationFresh()
    (src/shared/attestation.ts)で検証し、verified_result==='PASS' の場合のみ受理
    (HtlcAuthRequests.eligibility_attestation_id に記録、FinalityLog に
    BenefitAttested イベント)。未提供/FAIL/検証失敗は
    ELIGIBILITY_NOT_ATTESTED。

POST /api/htlc/auth/:auth_id/approve

送金側承認

Request: { "idempotency_key": "string" }

POST /api/htlc/auth/:auth_id/decline

送金側拒否

Request: { "reason": "optional reason", "idempotency_key": "string" }

GET /api/htlc/auth-requests

オーソリリクエスト一覧

Query: ?payer_bank_id=001&status=AUTH_REQUESTED

GET /api/htlc/auth/:auth_id

オーソリリクエスト詳細

POST /api/htlc/auth-whitelist

ホワイトリスト登録

Request:

{
  "payee_bank_id": "001",
  "payee_account_hash": "...",
  "allowed_payer_bank_id": "optional",
  "max_amount": 100000,
  "allowed_purposes": ["MERCHANT"],
  "description": "加盟店名",
  "expires_at": "optional RFC3339",
  "eligibility_template_id": "optional, TPL-..."
}
  • eligibility_template_id(省略可): TPL- プレフィックス必須。設定すると
    当該ホワイトリストへの createAuthRequest() は対象者該当性のアテステー
    ション(下記)を要求するようになる。プレフィックス不正は 400 INVALID_TEMPLATE_ID。

GET /api/htlc/auth-whitelist

ホワイトリスト一覧

DELETE /api/htlc/auth-whitelist/:whitelist_id

ホワイトリスト削除

POST /api/gtid/register

GTID leg登録(GtLegRegistered)

Request:

{
  "gtid": "GT-...",
  "legs": [
    { "leg_id": "L1", "role": "PAYER", "bank_id": "001", "account_hash": "h:...", "amount": { "value": 3000, "currency": "JPY" } },
    { "leg_id": "L2", "role": "PAYEE", "bank_id": "002", "account_hash": "h:...", "amount": { "value": 3000, "currency": "JPY" } }
  ],
  "expires_at": "RFC3339",
  "idempotency_key": "string"
}

legs[].amount.currency は JPY / USD / EUR / GBP / CHF のいずれか
(既定 JPY)。それ以外を指定すると INVALID_CURRENCY で拒否される。

PvP(多通貨同時決済): legsが複数通貨に跨る場合、原子性は既存のGTID
all-or-nothing保証(leg_idの辞書順対応付け)のまま、PAYER/PAYEE総額の一致
チェック(AMOUNT_BALANCE_MISMATCH)が通貨ごとに行われる。H予約・DNS
サイクル(DNS-{CCY}-YYYYMMDD-NN)も脚の通貨ごとに独立して割り当てられる。

脚の正規化と leg_id の書き換え(規範): PAYER と PAYEE の本数は一致していなくてよい。

1×M(fan-out)も両側とも複数の一般形 N×M も受理・決済される。ただし ZC は受付時に脚集合を

通貨グループごとに 1:1 の部分取引へ正規化したうえで決済経路へ渡すため、

登録した leg_id はそのまま残らないことがある(fan-out は {元のleg_id}~{連番}、

一般形の分解は {通し番号}~P~{元のleg_id} / {通し番号}~Q~{元のleg_id})。

したがって GET /api/gtid/:gtid が返す脚は登録した脚と 1:1 とは限らず、自分が付けた
leg_id を照会キーとして仮定してはならない
(各口座の金額の総和は保存される)。

書き換えられた脚は legs[].origin_leg_id に登録時の leg_id を持つので、

「自分のどの脚がここで決済されたのか」は照会で辿れる(登録どおりの脚では null)。

通貨をまたいだ相殺は行わない——ある通貨が単独で均衡しない構成は

AMOUNT_BALANCE_MISMATCH で取消され、クロスカレンシーは FX レーン

(POST /api/fx/transfers)が扱う。規則の正は 20_method_design.md §2.2.5.1。

タイミングに関する注記: POST /api/gtid/register 自体は形式が妥当な

legsであれば常に同期的に 201 GTID_ACCEPTED(state: GT_RECEIVED)を返す

— この時点では通貨ごとの一致チェックは実行されない。実際の判定は

Queueコンシューマ(advanceGtid、leg-ready-check後)で非同期に行われ、

不一致なら GT_RECEIVED → GT_PRECHECKED → GT_CANCELLED(reason

AMOUNT_BALANCE_MISMATCH)に遷移する。クライアントは register の 201 を

「決済確定」と解釈してはならず、GET /api/gtid/:gtid で最終状態

(GT_DECIDED_TO_SETTLE / GT_CANCELLED)を確認する必要がある。

クロスカレンシー FX(要件 10_requirements.md 第6章/方式 20_method_design.md 第17章/内部設計 30_internal_design.md 第17章)

レートは整数固定小数(RATE_SCALE = 1e8。rate=670000 は 1 from = 0.0067 to)。
FX送金は FXP を導管とする通貨別脚の GTID として既存 GTID レーンで原子決済される。

前提: レート形成はスコープ外。ZC は価格を作らない。 実効レートのプライシング

(建値・スプレッド・ヘッジ判断)は各 FXP が系の外で決める。ZC 側に為替オラクル・

参照レートフィード・レート算出ロジックは存在しない。API がやるのは、FXP が投入した

レートの (1) 受理・保存(PUT /api/fx/rates、形式検査と FXP 資格ゲートのみ)、

(2) 最良経路の集約・選定(POST /api/fx/quote / transfers、routing.ts)、

(3) 有効期限と min_effective_rate による逆行ガードのみ。詳細は 20_method_design.md §17.3 の前提。

PUT /api/fx/rates

FXプロバイダ(FXP)が系外で決定した方向別レートを upsert(FXP+ペアで1本のACTIVE見積を
上書き)。is_fx_provider=1 の参加行のみ可(非FXPは UNAUTHORIZED)。サーバはレート値の
形式(正の整数 ×RATE_SCALE)のみを検査し、値自体は補正・算出しない(価格形成はFXPの裁量)。

Request:

{
  "fxp_bank_id": "002",
  "from_currency": "JPY",
  "to_currency": "USD",
  "rate": 670000,
  "min_amount": 0,
  "max_amount": null,
  "valid_from": "RFC3339 (optional)",
  "valid_to": "RFC3339"
}

Response: { "result": "QUOTE_ACCEPTED", "quote": { ...FxQuotes row } }

GET /api/fx/rates?from=JPY&to=USD

ペアの ACTIVE 見積一覧(rate 降順)。

DELETE /api/fx/rates/:quote_id

見積を WITHDRAWN にする。未存在/既取下げは 404。

POST /api/fx/quote

最良経路の価格照会(コミットしない)。denomination は PAYER(from建て)/
PAYEE(to建て)。経路なしは 409 FX_NO_ROUTE。

Request:

{ "from_currency": "JPY", "to_currency": "USD", "amount": 1000000, "denomination": "PAYER", "max_bridge_hops": 1 }

Response: { "result": "ROUTE_FOUND", "rate_scale": 100000000, "route": { "hops": [...], "amount_from": 1000000, "amount_to": 6700, "effective_rate": 670000, "expires_at": "..." } }

POST /api/fx/transfers

FX送金を起動。サーバが権威的に再価格付け(クライアント提示の経路は信用しない)。
fxp_accounts は各 FXP の通貨別決済口座(キー "<bankId>:<currency>")。冪等。

Request:

{
  "gtid": "GT-...",
  "idempotency_key": "string",
  "from_currency": "JPY", "to_currency": "USD",
  "amount": 1000000, "denomination": "PAYER",
  "payer": { "bank_id": "001", "account_hash": "h:..." },
  "payee": { "bank_id": "001", "account_hash": "h:..." },
  "fxp_accounts": { "002:JPY": "h:...", "002:USD": "h:..." },
  "min_effective_rate": 660000,
  "expires_at": "RFC3339 (optional)"
}

Response: { "result": "FX_TRANSFER_INITIATED", "gtid", "hashlock", "amount_from", "amount_to", "effective_rate", "route" }。
エラー: 409 FX_NO_ROUTE / 409 FX_RATE_MISMATCH(min_effective_rate割れ)/
409 FX_QUOTE_EXPIRED / 400 FX_FXP_ACCOUNT_MISSING。

クロスレール原子性(HTLC束ね): リクエストに "bind_htlc": true を付けると、
即時決済せず各通貨脚を共有ハッシュロック+段階的タイムロックでロックし、決済を
/claim まで遅延する(20_method_design.md §17.4.2)。Response は
{ "result": "FX_TRANSFER_LOCKED", "gtid", "hashlock", "secret", "amount_from", "amount_to", "legs", "route" }
(secret はここで生成した場合のみ。payee へ別経路で渡す)。

POST /api/fx/transfers/:gtid/claim

secret 公開で HTLC束ね送金を確定。sha256(secret)==hashlock を検証→全脚を一括 CLAIMED→
導管 GTID を登録・前進させて決済。冪等。Request: { "secret": "64-hex" }。
Response: { "result": "FX_TRANSFER_CLAIMED", "gtid", "status": "SETTLED", "gtid_state", "already" }。
エラー: 400 PREIMAGE_MISMATCH(secret不一致)/ 404 GTID_NOT_FOUND /
409 FX_ALREADY_REFUNDED(払戻済みは claim 不可)。

POST /api/fx/transfers/:gtid/refund

タイムロック満了後、未 claim の HTLC束ね送金を払戻(全脚 REFUNDED、資金移動なし)。
Response: { "result": "FX_TRANSFER_REFUNDED", "gtid", "status": "REFUNDED", "refunded_legs" }。
エラー: 409 STATE_GUARD(claim済み or タイムロック未満了)/ 404 GTID_NOT_FOUND。

GET /api/fx/transfers/:gtid

FX送金の状態(FX固有事実+GTID状態+脚)。HTLC束ねの場合は脚ごとロック状態
(leg_locks: LOCKED/CLAIMED/REFUNDED+timelock)も返す。未存在は 404 GTID_NOT_FOUND。

POST /api/rtp/request

RTP請求登録

Request:

{
  "rtp_id": "RTP-...",
  "payee_bank_id": "001",
  "payer_bank_id": "002",
  "amount": { "value": 2000, "currency": "JPY" },
  "expires_at": "RFC3339",
  "idempotency_key": "string",
  "payee_name": "optional",
  "description": "optional",
  "payee_account": "optional"
}

POST /api/rtp/:rtpId/respond

RTP請求への応答

Request: { "action": "ACCEPT|DECLINE", "payer_account_id": "...", "idempotency_key": "string" }

GET /api/rtp/incoming

受信RTP請求一覧(payer側)

Query: ?account=XXXXXXXXXX(口座番号先頭3桁で銀行ID自動判定)

POST /api/transfers/:txid/authorize

TransferAuthorize(Standard/HV: 支払人最終認可)

Request: { "txid": "...", "authorized": true, "idempotency_key": "string" }

POST /api/transfers/:txid/cancel

取消(Decision前のみ)

Request: { "txid": "...", "reason_code": "CANCEL_BY_PAYER", "idempotency_key": "string" }

POST /api/transfers/:txid/no-debit-proof

H_locked の自動解放(未実行証明・20_method_design.md(整合性・ファイナリティ設計))。PayerBank が「デビット未記録」を署名付きで提出し、ZC が X-ZC-Signature を検証のうえ H_locked を解放する。

Request: { "proof_ref": "PROOF-...", "bank_id": "001" }(Header: X-ZC-Signature)

Response: { "ok": true, "result": "H_RELEASED", "txid": "...", "reservation_id": "H-...", "amount": 5000, "event": "NoDebitRecordedProofSubmitted" }

検査順: 署名検証(401 MISSING_SIGNATURE/INVALID_SIGNATURE)が先、proof_ref 必須チェック(400 PROOF_REF_REQUIRED)はそのあと — 未認証の呼び出し元にフィールド欠落を教えないため(bank-ingress HTTP ラッパーと同じ順序)。

ガード: a(PAYER_EXEC_CONFIRMED)/ b(PAYEE_EXEC_CONFIRMED)成立済みは解放不可(422 A_OR_B_CONFIRMED → 補償=Reversal 経路)。現在状態と FinalityLog 履歴の双方で判定。

POST /api/transfers/:txid/credit-failed-proof

Reversal の原因事由証明(三層ゲート第1層、10_requirements.md §4.3.0)。受取銀行が
「資金を先へ動かすことが物理的に不能である」ことを署名付きで提出し、ZC が
X-ZC-Signature を検証のうえ CreditFailedProofSubmitted を FinalityLog に追記する。
この証明が記録されている取引だけが Reversal を起票できる。

Request: { "proof_ref": "PROOF-...", "bank_id": "002", "reason_code": "optional" }(Header: X-ZC-Signature)

Response: { "ok": true, "result": "CREDIT_FAILED_PROOF_RECORDED", "txid": "...", "already": false }

検査順: 署名検証(401 MISSING_SIGNATURE/INVALID_SIGNATURE)が先、proof_ref 必須チェック
(400 PROOF_REF_REQUIRED)はそのあと(no-debit-proof と同じ順序)。

ガード(いずれも 422、ok:false と reason を返す):

  • B_NOT_CONFIRMED — 元取引が SETTLED でない。b 未成立なら巻き戻す対象が無いので、
    収束は取消または CASE であって Reversal ではない(10_requirements.md §4.3.0-4)。
  • PROOF_ISSUER_MISMATCH — 発行者が受取銀行でない。この証明は受取側だけが知り得る事実に
    ついての主張であり、支払側に発行させれば Reversal が自己申告で通ってしまう。
  • ACCOUNT_CONDITION_NOT_A_CAUSE — reason_code が口座都合
    (ACCOUNT_FROZEN / ACCOUNT_CLOSED / ACCOUNT_NOT_FOUND / INSUFFICIENT_FUNDS /
    CLOSING_HOLD)。これらは Custody で吸収する設計であり、証明の形を借りて原因事由に
    格上げすることを禁じる(10_requirements.md §4.3、20_method_design.md §6.3.1)。
  • TX_NOT_FOUND — 元取引なし。

冪等:再送は already: true を返し、FinalityLog を二重に書かない。元取引の state は動かない
(証明は SETTLED な取引「についての」事実であり、その遷移ではない)。

POST /api/transfers/:txid/h-unlock-authorize

H_locked の運用解放(二重統制=4 眼・20_method_design.md(整合性・ファイナリティ設計))。未実行証明が得られない場合に、2 名の異なる承認者と証跡参照を要件として解放する。

Request: { "approver_1": "ops.alice", "approver_2": "ops.bob", "evidence_type": "LEDGER_HASH|QUERY_SIGNATURE|AUTHORITY_CHECK", "evidence_ref": "...", "case_id": "CASE-..." }

Response: { "ok": true, "result": "H_RELEASED", "event": "HUnlockAuthorized", ... }

ガード: approver_1 != approver_2(422 FOUR_EYES_REQUIRED)、evidence_ref 必須(422 EVIDENCE_REQUIRED)、a/b 成立済みは解放不可(422 A_OR_B_CONFIRMED)。

POST /api/transfers/:txid/resume-namecheck

名義確認サスペンド(PRECHECKED_SUSPENDED)からの再開。resumeFromNameCheckSuspended が mandate を再検証のうえ PRECHECKED_SUSPENDED → PRECHECKED を CAS で進める(dedup 点)。

Response: { "result": "RESUMED", "txid": "...", "state": "..." }

ガード: 未存在は 404 NOT_FOUND、並行更新は 409 STATE_CONFLICT、対象外状態は 409 INVALID_STATE。

POST /api/transfers/:txid/misrecord-correct

誤記録訂正(10_requirements.md(法制度・契約構造との整合)「唯一の超例外」)。ZC障害等で a
(PayerExecConfirmed)が実際にはデビットされていないのに誤って記録された場合に限り、
その誤った証跡を訂正する。取消でも Reversal でもない(資金は動いていない)。
correctMisrecord(src/zc/finality/misrecord.ts)が 3 統制(時間ウィンドウ・4眼・証跡参照)を
検証し、MisrecordCorrected を追記(改ざんでなく追記)、PAYER_EXEC_CONFIRMED → SUSPENDED
へ戻し、CASE を1件 open する。b(PAYEE_EXEC_CONFIRMED)成立後は境界が不可逆のため訂正不可。

Request: { "approver_1": "ops.alice", "approver_2": "ops.bob", "evidence_type": "...", "evidence_ref": "...", "note": "optional" }

Response: { "ok": true, "result": "MISRECORD_CORRECTED", "txid": "...", "case_id": "CASE-...", "state_from": "...", "state_to": "..." }

ガード(いずれも 422、ok:false と reason を返す): approver_1 != approver_2
(FOUR_EYES_REQUIRED)、evidence_type/evidence_ref 必須(EVIDENCE_REQUIRED)、
b 成立済み・SETTLED は訂正不可で Reversal 経路(B_CONFIRMED)、訂正対象の a が無い
(NO_MISRECORD)、時間ウィンドウ超過(WINDOW_EXPIRED)、既訂正(ALREADY_CORRECTED)、
並行更新(STATE_CONFLICT)、対象外状態(NOT_CORRECTABLE)、未存在(TX_NOT_FOUND)。


口座確認・EDI・Proxy・QR・RichData

POST /api/account-verify

口座確認リクエスト(単件)

Request:

{
  "verification_id": "V-...",
  "request_bank_id": "001",
  "target_bank_id": "002",
  "target_account_id": "0020000001",
  "name_to_verify": "佐藤 花子",
  "idempotency_key": "string"
}
  • 宛先は target_account_id(口座番号。ZC は先頭 h: を剥がしたものを口座ハッシュと同一視する)。
    request_bank_id は省略時 X-Bank-Id ヘッダから補う。
  • name_to_verify を省略すると口座存在確認のみとなる。
  • 同一 idempotency_key の再送は既存の verification_id をそのまま返す。同一
    (target_bank_id, target_account_id) の有効なキャッシュがあれば銀行を呼ばずに複写する。

POST /api/account-verify/batch

口座確認リクエスト(一括)

Request:

{
  "batch_id": "B-...",
  "request_bank_id": "001",
  "items": [
    { "target_bank_id": "002", "target_account_id": "0020000001", "name_to_verify": "..." }
  ],
  "idempotency_key": "string"
}

各 item は単件と同じ経路を通り、idempotency_key は "{idempotency_key}-{index}" で分割される。

GET /api/account-verify/:verificationId

口座確認結果照会

POST /api/edi/register

EDIレコード登録

Request:

{
  "txid": "TX-...",
  "invoice_number": "INV-2026-001",
  "invoice_date": "2026-03-01",
  "payment_due_date": "2026-03-31",
  "tax_amount": 500,
  "tax_rate": 0.1,
  "discount_amount": 0,
  "note": "optional",
  "sender_ref": "optional",
  "receiver_ref": "optional",
  "line_items": [{"item": "商品A", "quantity": 1, "unit_price": 5000}]
}

GET /api/edi/tx/:txid

取引IDでEDI照会

GET /api/edi/:ediRef

EDI参照IDで照会

POST /api/proxy/register

プロキシ(エイリアス)登録

proxy_type の値域(実装の正: src/types/states.ts#ProxyType: PHONE | EMAIL | NATIONAL_ID)。

Request:

{
  "proxy_type": "PHONE|EMAIL|NATIONAL_ID",
  "proxy_value": "090-xxxx-xxxx",
  "bank_id": "001",
  "account_id": "0010000001",
  "account_holder_name": "田中 太郎"
}

GET /api/proxy/resolve

プロキシ解決

Query: ?type=PHONE&value=090-xxxx-xxxx

DELETE /api/proxy/:proxyId

プロキシ無効化

POST /api/qr/generate

QRコード生成

type の値域(実装の正: src/types/states.ts#QrType: STATIC | DYNAMIC)。

Request:

{
  "type": "STATIC|DYNAMIC",
  "payee_bank_id": "001",
  "payee_account_id": "0010000001",
  "payee_name": "田中商店",
  "amount": 1000,
  "purpose": "MERCHANT",
  "expires_at": "optional RFC3339"
}

type は必須(STATIC / DYNAMIC)。amount は省略可で、指定する場合は
正の整数(最小通貨単位、通貨は JPY 固定)。必須欠落・型不正は 400
(INVALID_REQUEST / MISSING_FIELD / INVALID_AMOUNT)で拒否される。

POST /api/qr/pay

QRコード決済実行

Request:

{
  "qr_ref": "QR-...",
  "payer_bank_id": "002",
  "payer_account_id": "0020000001",
  "amount": 1000,
  "idempotency_key": "string"
}

→ 内部的に POST /api/transfers を呼び出してEXPRESS送金を起動

規範(単一使用):DYNAMIC QR は単一使用。消費は is_used の CAS(... WHERE qr_ref = ? AND is_used = 0)で行い、並行決済の敗者(changes() == 0)は

QR_ALREADY_USED で拒否する。読取り時チェックのみでは TOCTOU により二重使用が

成立しうるため、行述語で原子的に消費すること。STATIC QR は任意回数再利用可。

GET /api/qr/:qrRef

QRコード照会

POST /api/richdata/store

リッチデータ格納

Header: X-Bank-Id: 001(格納主体。省略時は UNKNOWN として記録される)

Request:

{
  "data_type": "EDI|INVOICE|ATTACHMENT_META|REMITTANCE",
  "bank_id": "001",
  "txid": "TX-...",
  "content": { "...任意のJSON..." }
}
  • data_type の値域(実装の正: src/types/states.ts#RichDataType: EDI | INVOICE | ATTACHMENT_META | REMITTANCE)。値ごとに R2 退避時の D1 サマリ項目が異なる(buildSummary、src/zc/richdata/richdata.ts)。
  • 保持期間はリクエストで指定できない:RICHDATA_DEFAULT_RETENTION_DAYS(src/shared/constants.ts)が一律に適用され、expires_at が算出される。かつて本節は retention_days を受理するかのように記載していたが、実装は受け取らない。
  • 本文が 50KB を超え R2_BUCKET バインディングがある場合は R2 へ退避し、D1 には data_type 別のサマリのみを保存する(content_hash は常に全文の SHA-256)。

GET /api/richdata/tx/:txid

取引IDでリッチデータ照会

GET /api/richdata/:dataRef

参照IDでリッチデータ照会


PDFを作成

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

用紙
組み方向
表紙
本文