第15巻 API契約定義(1) ― 参加行→ZC受付とクロスカレンシーFX
目次
第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 }(不正式は400CONDITION_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でリッチデータ照会