{"openapi":"3.1.0","info":{"title":"Infinihash BaaS — Rails & Settlement API","version":"0.2.0","description":"Infinihash Billing-as-a-Service. COMPLY / MOVE / HOLD.\n\n- **MOVE** — invoice, collect, screen, convert, settle. Canonical settlement spine:\n  `created -> collecting -> screening -> screened -> converting -> settling -> completed | declined | failed`.\n  Every hop is validated and written to an immutable audit trail.\n- **HOLD** — Managed Escrow is defined in this specification but is **not available in production**: the settlement rail in force there forwards funds to the destination wallet as a single transfer, so there is no custodial hold to escrow against, and `POST /api/v1/escrow/{id}/fund` refuses with `409 escrow_hold_unsupported`. The sandbox refuses it identically, and deliberately: its mock settlement adapter declares the same non-custodial answer the production rail does, so escrow is unavailable on **every** current deployment and an integration proven against the sandbox is proven against production. Refunds are unsupported on **every** current deployment (`409 refund_unsupported`): no adapter implements a refund leg. Call `GET /api/v1/capabilities` on the host you are integrating against for the authoritative answer.\n- **COMPLY** — KYT screening is FAIL-CLOSED. Settlement will not convert and escrow will not release without a passing screen; blocked escrows park in `release_pending` for the manual review queue.\n\n**Idempotency** — every money-moving POST requires an `Idempotency-Key` header. See docs/IDEMPOTENCY.md.\n\n**Webhooks** — signed `X-Infinihash-Signature: t=<unix>,v1=<hmac_sha256(secret, \"<t>.<body>\")>`, retried with exponential backoff (60s, 5m, 15m, 1h, 6h; 6 attempts), delivery-tracked.\n\nThese are the events this service emits. It is the complete list — an event name not on it is not sent, and subscribing to one means receiving nothing:\n\n- **Invoicing** — `invoice.sent`\n- **Settlement** — `settlement.completed`, `settlement.settling`, `settlement.declined`, `settlement.failed`, `settlement.amount_mismatch`, `settlement.rail_unknown`, `settlement.stalled`, `settlement.returned_after_disbursement`. A successful settlement emits `settlement.completed`; the legacy `settled` status is mapped onto it, so `settlement.settled` is never sent. `settlement.settling` is the common in-flight case, not an error: the rail has accepted the disbursement but has not yet returned an on-chain hash, so the settlement is held short of `completed` until proof arrives and a later `settlement.completed` (or `settlement.stalled`) follows. `settlement.returned_after_disbursement` means the rail reported an ACH return (or failure) for a payment whose stablecoin had already been delivered: the settlement stays `completed`, the payment becomes `returned`, and the shortfall is a reserve decision.\n- **Reserve** — `reserve.hold_blocked`, `reserve.release_ready`\n- **Disputes** — `alert.created`, `alert.matched`, `alert.unmatched`, `alert.notify_iso`, `alert.routed_to_review`, `alert.auto_refunded` (NOT a confirmation that Rails refunded anyone: refunds are unsupported; this event only mirrors a refund the card network already made), `case.created`, `case.status_changed`, `ratio.threshold_breached`, `program.credit_suspended`, `program.credit_reinstated`\n- **Escrow** — `escrow.created`, `escrow.funded`, `escrow.released`, `escrow.release_blocked`, `escrow.refunded`, `escrow.expired`, `escrow.milestone_completed`, `escrow.approval_recorded`. Managed Escrow is unavailable on every current deployment (see **HOLD** above), so in practice none of these fire: since 2026-10-02 creation itself is refused with `409 escrow_not_offered` on a rail that cannot hold value, and funding with `409 escrow_hold_unsupported`.\n\nThere is no payment-received event: collection outcomes are reported through the settlement events above. KYB emits no webhooks at all — poll `GET /admin/kyb/invites` for application status.\n\nInfinihash orchestrates; partners execute collection, conversion and payout. No Infinihash private-key custody.","contact":{"name":"Infinihash","url":"https://infinihash.com"}},"servers":[{"url":"https://rails-api.infinihash.com","description":"production"},{"url":"https://onboard.infinihash.com","description":"production — same service; the hostname the white-labeled KYB wizard is served from"},{"url":"https://rails-sandbox.infinihash.com","description":"sandbox — the same application code with a mock settlement adapter (not the Bridge rail; /health reports the adapter in use) and mock screening. No real money moves and no email is sent. Sandbox keys are issued on request after a person reviews your application; there is no self-serve key."},{"url":"http://127.0.0.1:4100","description":"local development only (npm start). Not reachable from outside the host."}],"tags":[{"name":"Health"},{"name":"MOVE — Invoicing"},{"name":"MOVE — Payments & Settlement"},{"name":"HOLD — Managed Escrow","x-infinihash-availability":{"status":"unavailable","reason":"Managed escrow requires a settlement rail that can custody value between parties. The live Bridge rail cannot: its single ACH-in -> stablecoin-out transfer moves funds to the destination wallet at collection time, so there is no custodial hold to release from later. Escrows can be created and validated, but funding, release and refund are refused at the door rather than bricking an escrow with money inside.","errorCode":"escrow_hold_unsupported","httpStatus":409,"capability":"managed_escrow"}},{"name":"Admin"},{"name":"Reconciliation"},{"name":"Feature flags"},{"name":"Webhooks"},{"name":"disputes","description":"Dispute & chargeback engine. Deployed in production. Third-party alert sources (Ethoca, Verifi CDRN/RDR) are not contracted and default off; the manual, network and acquirer sources are available. Automatic refunds require a rail that can refund — see GET /api/v1/capabilities."}],"components":{"securitySchemes":{"tenantApiKey":{"type":"http","scheme":"bearer","description":"Tenant API key (`ihb_...`). Also accepted as `X-API-Key`."},"internalToken":{"type":"http","scheme":"bearer","description":"INTERNAL_SERVICE_TOKEN. Admin/ops surface only."}},"schemas":{"Escrow":{"type":"object","properties":{"id":{"type":"string"},"reference":{"type":"string"},"status":{"type":"string","enum":["created","awaiting_funds","funded","release_pending","released","refunded","expired","failed"]},"rail":{"type":"string","enum":["fiat","stablecoin"]},"releaseRule":{"type":"string","enum":["manual","milestone","dual_approval","compliance_gate"]},"requiredApprovals":{"type":"integer"},"amountCents":{"type":"integer"},"feeCents":{"type":"integer"},"netCents":{"type":"integer"},"currency":{"type":"string"},"payoutChain":{"type":"string"},"payoutAddress":{"type":"string"},"payoutStable":{"type":"string"},"refundAddress":{"type":"string"},"fundingRef":{"type":"string"},"releaseTxHash":{"type":"string"},"refundTxHash":{"type":"string"},"screeningRef":{"type":"string"},"screeningRisk":{"type":"string"},"declineReason":{"type":"string"},"expiresAt":{"type":"string","format":"date-time"},"fundedAt":{"type":"string","format":"date-time"},"releasedAt":{"type":"string","format":"date-time"},"milestones":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"seq":{"type":"integer"},"description":{"type":"string"},"amountCents":{"type":"integer"},"status":{"type":"string","enum":["pending","completed","cancelled"]}}}},"approvals":{"type":"array","items":{"type":"object","properties":{"approverRef":{"type":"string"},"decision":{"type":"string","enum":["approve","reject"]},"note":{"type":"string"}}}},"history":{"type":"array","description":"Audited state transitions","items":{"type":"object","properties":{"from":{"type":"string"},"to":{"type":"string"},"reason":{"type":"string"},"actor":{"type":"string"},"at":{"type":"string","format":"date-time"}}}}}},"Settlement":{"type":"object","properties":{"id":{"type":"string"},"paymentId":{"type":"string"},"tenantId":{"type":"string"},"status":{"type":"string","enum":["created","collecting","screening","screened","converting","settling","completed","declined","failed"],"description":"Canonical settlement spine. Legacy rows may also carry pending/reserved/disbursing/settled."},"fiatInCents":{"type":"integer"},"feeCents":{"type":"integer"},"reserveCents":{"type":"integer"},"fxRate":{"type":"string"},"usdtOut":{"type":"string"},"usdtStable":{"type":"string"},"destChain":{"type":"string"},"destWallet":{"type":"string"},"txHash":{"type":"string"},"screeningRef":{"type":"string"},"declineReason":{"type":"string"}}},"Error":{"type":"object","properties":{"error":{"type":"string"},"code":{"type":"string"},"unmet":{"type":"array","items":{"type":"string"}}}}}},"security":[{"tenantApiKey":[]}],"paths":{"/health":{"get":{"tags":["Health"],"summary":"Liveness + adapter in use","security":[],"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"type":"object","properties":{"service":{"type":"string"},"status":{"type":"string"},"version":{"type":"string"},"adapter":{"type":"string"}}}}}}}}},"/api/docs":{"get":{"tags":["Health"],"summary":"This OpenAPI document","security":[],"responses":{"200":{"description":"OpenAPI 3.1 JSON"}}}},"/api/v1/invoices":{"post":{"tags":["MOVE — Invoicing"],"summary":"Create an invoice","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["number","customerId","lineItems"],"properties":{"number":{"type":"string"},"customerId":{"type":"string"},"currency":{"type":"string","default":"USD"},"terms":{"type":"string","default":"Net 14"},"dueDate":{"type":"string","format":"date-time"},"notes":{"type":"string"},"discountCents":{"type":"integer","default":0},"lineItems":{"type":"array","items":{"type":"object","required":["description","unitCents"],"properties":{"description":{"type":"string"},"quantity":{"type":"integer","default":1},"unitCents":{"type":"integer"}}}}}}}}},"responses":{"201":{"description":"created"},"400":{"description":"validation error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"code":{"type":"string"},"unmet":{"type":"array","items":{"type":"string"}}}}}}},"401":{"description":"bad api key","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"code":{"type":"string"},"unmet":{"type":"array","items":{"type":"string"}}}}}}}}},"get":{"tags":["MOVE — Invoicing"],"summary":"List invoices","parameters":[{"name":"status","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"list"}}}},"/api/v1/invoices/{id}":{"get":{"tags":["MOVE — Invoicing"],"summary":"Get an invoice","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"invoice"},"404":{"description":"not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"code":{"type":"string"},"unmet":{"type":"array","items":{"type":"string"}}}}}}}}}},"/api/v1/invoices/{id}/send":{"post":{"tags":["MOVE — Invoicing"],"summary":"Send an invoice (emits invoice.sent)","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"sent"}}}},"/api/v1/invoices/{id}/pdf":{"get":{"tags":["MOVE — Invoicing"],"summary":"Invoice PDF","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"PDF","content":{"application/pdf":{"schema":{"type":"string","format":"binary"}}}}}}},"/api/v1/invoices/{id}/pay":{"post":{"tags":["MOVE — Payments & Settlement"],"summary":"Collect payment and run the settlement spine","description":"MONEY-MOVING. The destination wallet is KYT-screened BEFORE the collection rail is touched: a declining or unavailable screen returns 409 kyt_declined and no rail transfer is created, no payment is recorded and no settlement is started. Only after a passing screen does this collect on the enabled collection rail and drive the settlement spine: screen (fail-closed, again, before disbursement) -> fee -> reserve -> convert -> disburse. Replaying the same Idempotency-Key returns the original response and creates no second payment or settlement. The rail executes on behalf of the TENANT's own KYB-approved partner customer: a tenant with no approved KYB application linked returns 409 partner_customer_unlinked (or partner_customer_not_approved) before anything reaches the rail. For ACH-push collection the response carries sourceDepositInstructions.","parameters":[{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","maxLength":255},"description":"Required on money-moving endpoints. Replaying the same key with the same body returns the original response with Idempotent-Replay: true; a different body returns 409 idempotency_key_reuse."},{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"amountCents":{"type":"integer"},"source":{"type":"object"}}}}}},"responses":{"201":{"description":"payment + settlement","content":{"application/json":{"schema":{"type":"object","properties":{"payment":{"type":"object"},"settlement":{"type":"object","properties":{"id":{"type":"string"},"paymentId":{"type":"string"},"tenantId":{"type":"string"},"status":{"type":"string","enum":["created","collecting","screening","screened","converting","settling","completed","declined","failed"],"description":"Canonical settlement spine. Legacy rows may also carry pending/reserved/disbursing/settled."},"fiatInCents":{"type":"integer"},"feeCents":{"type":"integer"},"reserveCents":{"type":"integer"},"fxRate":{"type":"string"},"usdtOut":{"type":"string"},"usdtStable":{"type":"string"},"destChain":{"type":"string"},"destWallet":{"type":"string"},"txHash":{"type":"string"},"screeningRef":{"type":"string"},"declineReason":{"type":"string"}}},"sourceDepositInstructions":{"type":["object","null"],"description":"How the payer funds the transfer. On the Bridge rail an ACH-push transfer waits in awaiting_funds (payment.status 'processing') until funds arrive at the account and with the deposit message given here, exactly as returned by the rail (e.g. payment_rail, amount, currency, bank_name, bank_routing_number, bank_account_number, deposit_message). null on rails that collect synchronously.","additionalProperties":true}}}}}},"400":{"description":"missing Idempotency-Key","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"code":{"type":"string"},"unmet":{"type":"array","items":{"type":"string"}}}}}}},"409":{"description":"Refused without starting anything. Two causes, distinguished by `code`:\n\n- `kyt_declined` - the destination wallet did not pass KYT screening. FAIL-CLOSED: this is also the answer when the screening provider is unreachable, when the screen is incomplete and awaiting manual review, and when no screening credential is configured. The screen runs BEFORE the collection rail is touched, so no rail transfer exists, no payment was recorded and no settlement was started - there is nothing to reconcile or cancel, and the response says so with `railTransferCreated: false`.\n- `idempotency_key_reuse` - the same Idempotency-Key was replayed with a different body.\n\nAn illegal settlement state transition also surfaces here. | partner_customer_unlinked / partner_customer_not_approved / partner_customer_lookup_failed: no approved partner customer for this tenant; nothing was sent to the rail.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"code":{"type":"string","enum":["kyt_declined","idempotency_key_reuse"]},"screeningRef":{"type":"string","nullable":true,"description":"kyt_declined only - the screening provider's reference, when one was issued"},"risk":{"type":"string","nullable":true,"description":"kyt_declined only"},"stage":{"type":"string","nullable":true,"description":"kyt_declined only; 'collection' for this endpoint"},"railTransferCreated":{"type":"boolean","description":"kyt_declined only; always false - stated explicitly because it is the property the fail-closed claim rests on"}}}}}},"501":{"description":"conversion vendor disabled by feature flag","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"code":{"type":"string"},"unmet":{"type":"array","items":{"type":"string"}}}}}}}}}},"/api/v1/webhooks/bridge":{"post":{"tags":["Webhooks"],"summary":"Inbound rail callback (ACH state)","security":[{"internalToken":[]}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"railRef":{"type":"string"},"state":{"type":"string"},"return_code":{"type":"string"}}}}}},"responses":{"200":{"description":"ok"},"404":{"description":"unknown railRef","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"code":{"type":"string"},"unmet":{"type":"array","items":{"type":"string"}}}}}}}}}},"/api/v1/escrow":{"post":{"tags":["HOLD — Managed Escrow"],"summary":"Create a HOLD","description":"NOT OFFERED on a rail that cannot hold value (decision 2026-10-02): creation returns 409 escrow_not_offered and funding 409 escrow_hold_unsupported. Check GET /api/v1/capabilities. MONEY-MOVING. Creates the escrow and moves it to awaiting_funds. The platform fee is computed at creation; netCents is what the beneficiary receives on release.\n\n**NOT AVAILABLE IN PRODUCTION.** Managed Escrow availability depends on the settlement adapter the deployment is running. In production that rail forwards funds to the destination wallet as a single transfer, so there is no custodial hold: funding refuses with `409 escrow_hold_unsupported` and release/refund with `409 escrow_disbursement_unsupported`, and an escrow created there can never be funded, released or refunded. The sandbox's mock adapter does hold, so these calls complete there without moving money — do not read a sandbox success as production behaviour. `GET /api/v1/capabilities` on the host you are calling is the authoritative answer.","parameters":[{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","maxLength":255},"description":"Required on money-moving endpoints. Replaying the same key with the same body returns the original response with Idempotent-Replay: true; a different body returns 409 idempotency_key_reuse."},{"name":"X-Actor","in":"header","required":false,"schema":{"type":"string"},"description":"Optional, UNVERIFIED label for the person acting. It is NOT the audit actor: the StateTransition actor is always the authenticated principal (tenant:<slug> for an API key). The value is only logged as claimedActor."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["reference","amountCents"],"properties":{"reference":{"type":"string","description":"tenant-unique external reference"},"amountCents":{"type":"integer"},"currency":{"type":"string","default":"USD"},"rail":{"type":"string","enum":["fiat","stablecoin"],"default":"stablecoin","description":"Funding rail. fiat is gated off by escrow.rail.fiat until a hold-capable fiat vendor is contracted."},"releaseRule":{"type":"string","enum":["manual","milestone","dual_approval","compliance_gate"],"default":"manual"},"requiredApprovals":{"type":"integer","default":2},"buyerRef":{"type":"string"},"sellerRef":{"type":"string"},"payoutChain":{"type":"string"},"payoutAddress":{"type":"string","description":"Beneficiary-controlled address; required for the stablecoin rail"},"payoutStable":{"type":"string","default":"USDT"},"refundAddress":{"type":"string"},"invoiceId":{"type":"string"},"expiresAt":{"type":"string","format":"date-time"},"metadata":{"type":"object"},"milestones":{"type":"array","items":{"type":"object","required":["description"],"properties":{"seq":{"type":"integer"},"description":{"type":"string"},"amountCents":{"type":"integer"}}}}}}}}},"responses":{"201":{"description":"escrow","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"reference":{"type":"string"},"status":{"type":"string","enum":["created","awaiting_funds","funded","release_pending","released","refunded","expired","failed"]},"rail":{"type":"string","enum":["fiat","stablecoin"]},"releaseRule":{"type":"string","enum":["manual","milestone","dual_approval","compliance_gate"]},"requiredApprovals":{"type":"integer"},"amountCents":{"type":"integer"},"feeCents":{"type":"integer"},"netCents":{"type":"integer"},"currency":{"type":"string"},"payoutChain":{"type":"string"},"payoutAddress":{"type":"string"},"payoutStable":{"type":"string"},"refundAddress":{"type":"string"},"fundingRef":{"type":"string"},"releaseTxHash":{"type":"string"},"refundTxHash":{"type":"string"},"screeningRef":{"type":"string"},"screeningRisk":{"type":"string"},"declineReason":{"type":"string"},"expiresAt":{"type":"string","format":"date-time"},"fundedAt":{"type":"string","format":"date-time"},"releasedAt":{"type":"string","format":"date-time"},"milestones":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"seq":{"type":"integer"},"description":{"type":"string"},"amountCents":{"type":"integer"},"status":{"type":"string","enum":["pending","completed","cancelled"]}}}},"approvals":{"type":"array","items":{"type":"object","properties":{"approverRef":{"type":"string"},"decision":{"type":"string","enum":["approve","reject"]},"note":{"type":"string"}}}},"history":{"type":"array","description":"Audited state transitions","items":{"type":"object","properties":{"from":{"type":"string"},"to":{"type":"string"},"reason":{"type":"string"},"actor":{"type":"string"},"at":{"type":"string","format":"date-time"}}}}}}}}},"400":{"description":"validation error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"code":{"type":"string"},"unmet":{"type":"array","items":{"type":"string"}}}}}}},"501":{"description":"rail or escrow disabled by feature flag","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"code":{"type":"string"},"unmet":{"type":"array","items":{"type":"string"}}}}}}}}},"get":{"tags":["HOLD — Managed Escrow"],"summary":"List escrows","parameters":[{"name":"status","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"list"}},"description":"NOT OFFERED on a rail that cannot hold value (decision 2026-10-02): creation returns 409 escrow_not_offered and funding 409 escrow_hold_unsupported. Check GET /api/v1/capabilities. **NOT AVAILABLE IN PRODUCTION.** Managed Escrow availability depends on the settlement adapter the deployment is running. In production that rail forwards funds to the destination wallet as a single transfer, so there is no custodial hold: funding refuses with `409 escrow_hold_unsupported` and release/refund with `409 escrow_disbursement_unsupported`, and an escrow created there can never be funded, released or refunded. The sandbox's mock adapter does hold, so these calls complete there without moving money — do not read a sandbox success as production behaviour. `GET /api/v1/capabilities` on the host you are calling is the authoritative answer."}},"/api/v1/escrow/{id}":{"get":{"tags":["HOLD — Managed Escrow"],"summary":"Get an escrow with its audit history","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","description":"escrow id or tenant reference"}}],"responses":{"200":{"description":"escrow","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"reference":{"type":"string"},"status":{"type":"string","enum":["created","awaiting_funds","funded","release_pending","released","refunded","expired","failed"]},"rail":{"type":"string","enum":["fiat","stablecoin"]},"releaseRule":{"type":"string","enum":["manual","milestone","dual_approval","compliance_gate"]},"requiredApprovals":{"type":"integer"},"amountCents":{"type":"integer"},"feeCents":{"type":"integer"},"netCents":{"type":"integer"},"currency":{"type":"string"},"payoutChain":{"type":"string"},"payoutAddress":{"type":"string"},"payoutStable":{"type":"string"},"refundAddress":{"type":"string"},"fundingRef":{"type":"string"},"releaseTxHash":{"type":"string"},"refundTxHash":{"type":"string"},"screeningRef":{"type":"string"},"screeningRisk":{"type":"string"},"declineReason":{"type":"string"},"expiresAt":{"type":"string","format":"date-time"},"fundedAt":{"type":"string","format":"date-time"},"releasedAt":{"type":"string","format":"date-time"},"milestones":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"seq":{"type":"integer"},"description":{"type":"string"},"amountCents":{"type":"integer"},"status":{"type":"string","enum":["pending","completed","cancelled"]}}}},"approvals":{"type":"array","items":{"type":"object","properties":{"approverRef":{"type":"string"},"decision":{"type":"string","enum":["approve","reject"]},"note":{"type":"string"}}}},"history":{"type":"array","description":"Audited state transitions","items":{"type":"object","properties":{"from":{"type":"string"},"to":{"type":"string"},"reason":{"type":"string"},"actor":{"type":"string"},"at":{"type":"string","format":"date-time"}}}}}}}}},"404":{"description":"not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"code":{"type":"string"},"unmet":{"type":"array","items":{"type":"string"}}}}}}}},"description":"NOT OFFERED on a rail that cannot hold value (decision 2026-10-02): creation returns 409 escrow_not_offered and funding 409 escrow_hold_unsupported. Check GET /api/v1/capabilities. **NOT AVAILABLE IN PRODUCTION.** Managed Escrow availability depends on the settlement adapter the deployment is running. In production that rail forwards funds to the destination wallet as a single transfer, so there is no custodial hold: funding refuses with `409 escrow_hold_unsupported` and release/refund with `409 escrow_disbursement_unsupported`, and an escrow created there can never be funded, released or refunded. The sandbox's mock adapter does hold, so these calls complete there without moving money — do not read a sandbox success as production behaviour. `GET /api/v1/capabilities` on the host you are calling is the authoritative answer."}},"/api/v1/escrow/{id}/fund":{"post":{"tags":["HOLD — Managed Escrow"],"summary":"Fund the HOLD","description":"NOT OFFERED on a rail that cannot hold value (decision 2026-10-02): creation returns 409 escrow_not_offered and funding 409 escrow_hold_unsupported. Check GET /api/v1/capabilities. MONEY-MOVING. Funding amount must equal the escrow amount. Idempotent: funding an already-funded escrow returns it unchanged.\n\n**NOT AVAILABLE IN PRODUCTION.** Managed Escrow availability depends on the settlement adapter the deployment is running. In production that rail forwards funds to the destination wallet as a single transfer, so there is no custodial hold: funding refuses with `409 escrow_hold_unsupported` and release/refund with `409 escrow_disbursement_unsupported`, and an escrow created there can never be funded, released or refunded. The sandbox's mock adapter does hold, so these calls complete there without moving money — do not read a sandbox success as production behaviour. `GET /api/v1/capabilities` on the host you are calling is the authoritative answer.","parameters":[{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","maxLength":255},"description":"Required on money-moving endpoints. Replaying the same key with the same body returns the original response with Idempotent-Replay: true; a different body returns 409 idempotency_key_reuse."},{"name":"X-Actor","in":"header","required":false,"schema":{"type":"string"},"description":"Optional, UNVERIFIED label for the person acting. It is NOT the audit actor: the StateTransition actor is always the authenticated principal (tenant:<slug> for an API key). The value is only logged as claimedActor."},{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"amountCents":{"type":"integer"},"fundingRef":{"type":"string"},"depositTxHash":{"type":"string"},"source":{"type":"object"}}}}}},"responses":{"200":{"description":"funded","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"reference":{"type":"string"},"status":{"type":"string","enum":["created","awaiting_funds","funded","release_pending","released","refunded","expired","failed"]},"rail":{"type":"string","enum":["fiat","stablecoin"]},"releaseRule":{"type":"string","enum":["manual","milestone","dual_approval","compliance_gate"]},"requiredApprovals":{"type":"integer"},"amountCents":{"type":"integer"},"feeCents":{"type":"integer"},"netCents":{"type":"integer"},"currency":{"type":"string"},"payoutChain":{"type":"string"},"payoutAddress":{"type":"string"},"payoutStable":{"type":"string"},"refundAddress":{"type":"string"},"fundingRef":{"type":"string"},"releaseTxHash":{"type":"string"},"refundTxHash":{"type":"string"},"screeningRef":{"type":"string"},"screeningRisk":{"type":"string"},"declineReason":{"type":"string"},"expiresAt":{"type":"string","format":"date-time"},"fundedAt":{"type":"string","format":"date-time"},"releasedAt":{"type":"string","format":"date-time"},"milestones":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"seq":{"type":"integer"},"description":{"type":"string"},"amountCents":{"type":"integer"},"status":{"type":"string","enum":["pending","completed","cancelled"]}}}},"approvals":{"type":"array","items":{"type":"object","properties":{"approverRef":{"type":"string"},"decision":{"type":"string","enum":["approve","reject"]},"note":{"type":"string"}}}},"history":{"type":"array","description":"Audited state transitions","items":{"type":"object","properties":{"from":{"type":"string"},"to":{"type":"string"},"reason":{"type":"string"},"actor":{"type":"string"},"at":{"type":"string","format":"date-time"}}}}}}}}},"400":{"description":"amount mismatch","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"code":{"type":"string"},"unmet":{"type":"array","items":{"type":"string"}}}}}}},"409":{"description":"illegal transition — or, on any current deployment, Managed Escrow is unavailable on this deployment: `escrow_hold_unsupported` — the settlement rail cannot custody escrowed value. This is the refusal every caller gets here today.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"code":{"type":"string"},"unmet":{"type":"array","items":{"type":"string"}}}}}}},"501":{"description":"rail disabled","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"code":{"type":"string"},"unmet":{"type":"array","items":{"type":"string"}}}}}}}},"x-infinihash-availability":{"status":"unavailable","reason":"Managed escrow requires a settlement rail that can custody value between parties. The live Bridge rail cannot: its single ACH-in -> stablecoin-out transfer moves funds to the destination wallet at collection time, so there is no custodial hold to release from later. Escrows can be created and validated, but funding, release and refund are refused at the door rather than bricking an escrow with money inside.","errorCode":"escrow_hold_unsupported","httpStatus":409,"capability":"managed_escrow"}}},"/api/v1/escrow/{id}/release":{"post":{"tags":["HOLD — Managed Escrow"],"summary":"Release the HOLD to the beneficiary","description":"NOT OFFERED on a rail that cannot hold value (decision 2026-10-02): creation returns 409 escrow_not_offered and funding 409 escrow_hold_unsupported. Check GET /api/v1/capabilities. MONEY-MOVING. Evaluates the release rule AND the compliance gate (KYT screen of the payout address, fail-closed). If any condition is unmet the escrow moves to release_pending, appears in /admin/review-queue, and the call returns 409 with the unmet list. Never releases on a failed or unavailable screen.\n\n**NOT AVAILABLE IN PRODUCTION.** Managed Escrow availability depends on the settlement adapter the deployment is running. In production that rail forwards funds to the destination wallet as a single transfer, so there is no custodial hold: funding refuses with `409 escrow_hold_unsupported` and release/refund with `409 escrow_disbursement_unsupported`, and an escrow created there can never be funded, released or refunded. The sandbox's mock adapter does hold, so these calls complete there without moving money — do not read a sandbox success as production behaviour. `GET /api/v1/capabilities` on the host you are calling is the authoritative answer.","parameters":[{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","maxLength":255},"description":"Required on money-moving endpoints. Replaying the same key with the same body returns the original response with Idempotent-Replay: true; a different body returns 409 idempotency_key_reuse."},{"name":"X-Actor","in":"header","required":false,"schema":{"type":"string"},"description":"Optional, UNVERIFIED label for the person acting. It is NOT the audit actor: the StateTransition actor is always the authenticated principal (tenant:<slug> for an API key). The value is only logged as claimedActor."},{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"released","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"reference":{"type":"string"},"status":{"type":"string","enum":["created","awaiting_funds","funded","release_pending","released","refunded","expired","failed"]},"rail":{"type":"string","enum":["fiat","stablecoin"]},"releaseRule":{"type":"string","enum":["manual","milestone","dual_approval","compliance_gate"]},"requiredApprovals":{"type":"integer"},"amountCents":{"type":"integer"},"feeCents":{"type":"integer"},"netCents":{"type":"integer"},"currency":{"type":"string"},"payoutChain":{"type":"string"},"payoutAddress":{"type":"string"},"payoutStable":{"type":"string"},"refundAddress":{"type":"string"},"fundingRef":{"type":"string"},"releaseTxHash":{"type":"string"},"refundTxHash":{"type":"string"},"screeningRef":{"type":"string"},"screeningRisk":{"type":"string"},"declineReason":{"type":"string"},"expiresAt":{"type":"string","format":"date-time"},"fundedAt":{"type":"string","format":"date-time"},"releasedAt":{"type":"string","format":"date-time"},"milestones":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"seq":{"type":"integer"},"description":{"type":"string"},"amountCents":{"type":"integer"},"status":{"type":"string","enum":["pending","completed","cancelled"]}}}},"approvals":{"type":"array","items":{"type":"object","properties":{"approverRef":{"type":"string"},"decision":{"type":"string","enum":["approve","reject"]},"note":{"type":"string"}}}},"history":{"type":"array","description":"Audited state transitions","items":{"type":"object","properties":{"from":{"type":"string"},"to":{"type":"string"},"reason":{"type":"string"},"actor":{"type":"string"},"at":{"type":"string","format":"date-time"}}}}}}}}},"409":{"description":"release conditions not met (unmet[] returned) — or, on any current deployment, Managed Escrow is unavailable on this deployment: `escrow_disbursement_unsupported` — the settlement rail cannot disburse escrowed value. The escrow is left unmodified.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"code":{"type":"string"},"unmet":{"type":"array","items":{"type":"string"}}}}}}},"502":{"description":"payout rail failure","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"code":{"type":"string"},"unmet":{"type":"array","items":{"type":"string"}}}}}}}},"x-infinihash-availability":{"status":"unavailable","reason":"Managed escrow requires a settlement rail that can custody value between parties. The live Bridge rail cannot: its single ACH-in -> stablecoin-out transfer moves funds to the destination wallet at collection time, so there is no custodial hold to release from later. Escrows can be created and validated, but funding, release and refund are refused at the door rather than bricking an escrow with money inside.","errorCode":"escrow_hold_unsupported","httpStatus":409,"capability":"managed_escrow"}}},"/api/v1/escrow/{id}/refund":{"post":{"tags":["HOLD — Managed Escrow"],"summary":"Refund the HOLD to the payer","parameters":[{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","maxLength":255},"description":"Required on money-moving endpoints. Replaying the same key with the same body returns the original response with Idempotent-Replay: true; a different body returns 409 idempotency_key_reuse."},{"name":"X-Actor","in":"header","required":false,"schema":{"type":"string"},"description":"Optional, UNVERIFIED label for the person acting. It is NOT the audit actor: the StateTransition actor is always the authenticated principal (tenant:<slug> for an API key). The value is only logged as claimedActor."},{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"reason":{"type":"string"},"refundAddress":{"type":"string"}}}}}},"responses":{"200":{"description":"refunded","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"reference":{"type":"string"},"status":{"type":"string","enum":["created","awaiting_funds","funded","release_pending","released","refunded","expired","failed"]},"rail":{"type":"string","enum":["fiat","stablecoin"]},"releaseRule":{"type":"string","enum":["manual","milestone","dual_approval","compliance_gate"]},"requiredApprovals":{"type":"integer"},"amountCents":{"type":"integer"},"feeCents":{"type":"integer"},"netCents":{"type":"integer"},"currency":{"type":"string"},"payoutChain":{"type":"string"},"payoutAddress":{"type":"string"},"payoutStable":{"type":"string"},"refundAddress":{"type":"string"},"fundingRef":{"type":"string"},"releaseTxHash":{"type":"string"},"refundTxHash":{"type":"string"},"screeningRef":{"type":"string"},"screeningRisk":{"type":"string"},"declineReason":{"type":"string"},"expiresAt":{"type":"string","format":"date-time"},"fundedAt":{"type":"string","format":"date-time"},"releasedAt":{"type":"string","format":"date-time"},"milestones":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"seq":{"type":"integer"},"description":{"type":"string"},"amountCents":{"type":"integer"},"status":{"type":"string","enum":["pending","completed","cancelled"]}}}},"approvals":{"type":"array","items":{"type":"object","properties":{"approverRef":{"type":"string"},"decision":{"type":"string","enum":["approve","reject"]},"note":{"type":"string"}}}},"history":{"type":"array","description":"Audited state transitions","items":{"type":"object","properties":{"from":{"type":"string"},"to":{"type":"string"},"reason":{"type":"string"},"actor":{"type":"string"},"at":{"type":"string","format":"date-time"}}}}}}}}},"400":{"description":"refundAddress required","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"code":{"type":"string"},"unmet":{"type":"array","items":{"type":"string"}}}}}}},"409":{"description":"illegal transition — or, on any current deployment, Managed Escrow is unavailable on this deployment: `escrow_disbursement_unsupported` — the settlement rail cannot disburse escrowed value. The escrow is left unmodified.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"code":{"type":"string"},"unmet":{"type":"array","items":{"type":"string"}}}}}}}},"x-infinihash-availability":{"status":"unavailable","reason":"Managed escrow requires a settlement rail that can custody value between parties. The live Bridge rail cannot: its single ACH-in -> stablecoin-out transfer moves funds to the destination wallet at collection time, so there is no custodial hold to release from later. Escrows can be created and validated, but funding, release and refund are refused at the door rather than bricking an escrow with money inside.","errorCode":"escrow_hold_unsupported","httpStatus":409,"capability":"managed_escrow"},"description":"NOT OFFERED on a rail that cannot hold value (decision 2026-10-02): creation returns 409 escrow_not_offered and funding 409 escrow_hold_unsupported. Check GET /api/v1/capabilities. MONEY-MOVING. Returns escrowed value to the buyer and moves the escrow to `refunded`. Refundable from `created`, `awaiting_funds`, `funded`, `release_pending` and `failed`; a `refundAddress` is required once value is actually held. The disbursement guard runs BEFORE the state transition, so a refusal leaves the escrow exactly as refundable as it was.\n\n**NOT AVAILABLE IN PRODUCTION.** Managed Escrow availability depends on the settlement adapter the deployment is running. In production that rail forwards funds to the destination wallet as a single transfer, so there is no custodial hold: funding refuses with `409 escrow_hold_unsupported` and release/refund with `409 escrow_disbursement_unsupported`, and an escrow created there can never be funded, released or refunded. The sandbox's mock adapter does hold, so these calls complete there without moving money — do not read a sandbox success as production behaviour. `GET /api/v1/capabilities` on the host you are calling is the authoritative answer."}},"/api/v1/escrow/{id}/expire":{"post":{"tags":["HOLD — Managed Escrow"],"summary":"Expire an unfunded HOLD","description":"NOT OFFERED on a rail that cannot hold value (decision 2026-10-02): creation returns 409 escrow_not_offered and funding 409 escrow_hold_unsupported. Check GET /api/v1/capabilities. Only created/awaiting_funds escrows can expire. A funded escrow must be refunded, not expired. A sweeper also auto-expires past expiresAt.\n\n**NOT AVAILABLE IN PRODUCTION.** Managed Escrow availability depends on the settlement adapter the deployment is running. In production that rail forwards funds to the destination wallet as a single transfer, so there is no custodial hold: funding refuses with `409 escrow_hold_unsupported` and release/refund with `409 escrow_disbursement_unsupported`, and an escrow created there can never be funded, released or refunded. The sandbox's mock adapter does hold, so these calls complete there without moving money — do not read a sandbox success as production behaviour. `GET /api/v1/capabilities` on the host you are calling is the authoritative answer.","parameters":[{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","maxLength":255},"description":"Required on money-moving endpoints. Replaying the same key with the same body returns the original response with Idempotent-Replay: true; a different body returns 409 idempotency_key_reuse."},{"name":"X-Actor","in":"header","required":false,"schema":{"type":"string"},"description":"Optional, UNVERIFIED label for the person acting. It is NOT the audit actor: the StateTransition actor is always the authenticated principal (tenant:<slug> for an API key). The value is only logged as claimedActor."},{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"expired","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"reference":{"type":"string"},"status":{"type":"string","enum":["created","awaiting_funds","funded","release_pending","released","refunded","expired","failed"]},"rail":{"type":"string","enum":["fiat","stablecoin"]},"releaseRule":{"type":"string","enum":["manual","milestone","dual_approval","compliance_gate"]},"requiredApprovals":{"type":"integer"},"amountCents":{"type":"integer"},"feeCents":{"type":"integer"},"netCents":{"type":"integer"},"currency":{"type":"string"},"payoutChain":{"type":"string"},"payoutAddress":{"type":"string"},"payoutStable":{"type":"string"},"refundAddress":{"type":"string"},"fundingRef":{"type":"string"},"releaseTxHash":{"type":"string"},"refundTxHash":{"type":"string"},"screeningRef":{"type":"string"},"screeningRisk":{"type":"string"},"declineReason":{"type":"string"},"expiresAt":{"type":"string","format":"date-time"},"fundedAt":{"type":"string","format":"date-time"},"releasedAt":{"type":"string","format":"date-time"},"milestones":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"seq":{"type":"integer"},"description":{"type":"string"},"amountCents":{"type":"integer"},"status":{"type":"string","enum":["pending","completed","cancelled"]}}}},"approvals":{"type":"array","items":{"type":"object","properties":{"approverRef":{"type":"string"},"decision":{"type":"string","enum":["approve","reject"]},"note":{"type":"string"}}}},"history":{"type":"array","description":"Audited state transitions","items":{"type":"object","properties":{"from":{"type":"string"},"to":{"type":"string"},"reason":{"type":"string"},"actor":{"type":"string"},"at":{"type":"string","format":"date-time"}}}}}}}}},"409":{"description":"funded escrow must be refunded","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"code":{"type":"string"},"unmet":{"type":"array","items":{"type":"string"}}}}}}}}}},"/api/v1/escrow/{id}/conditions":{"get":{"tags":["HOLD — Managed Escrow"],"summary":"Dry-run the release rule + compliance gate","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"screen","in":"query","schema":{"type":"string","enum":["true","false"]},"description":"set false to skip the live KYT screen"}],"responses":{"200":{"description":"evaluation","content":{"application/json":{"schema":{"type":"object","properties":{"releasable":{"type":"boolean"},"unmet":{"type":"array","items":{"type":"string"}},"screening":{"type":"object"}}}}}}},"description":"NOT OFFERED on a rail that cannot hold value (decision 2026-10-02): creation returns 409 escrow_not_offered and funding 409 escrow_hold_unsupported. Check GET /api/v1/capabilities. **NOT AVAILABLE IN PRODUCTION.** Managed Escrow availability depends on the settlement adapter the deployment is running. In production that rail forwards funds to the destination wallet as a single transfer, so there is no custodial hold: funding refuses with `409 escrow_hold_unsupported` and release/refund with `409 escrow_disbursement_unsupported`, and an escrow created there can never be funded, released or refunded. The sandbox's mock adapter does hold, so these calls complete there without moving money — do not read a sandbox success as production behaviour. `GET /api/v1/capabilities` on the host you are calling is the authoritative answer."}},"/api/v1/escrow/{id}/milestones/{mid}/complete":{"post":{"tags":["HOLD — Managed Escrow"],"summary":"Mark a milestone complete","parameters":[{"name":"X-Actor","in":"header","required":false,"schema":{"type":"string"},"description":"Optional, UNVERIFIED label for the person acting. It is NOT the audit actor: the StateTransition actor is always the authenticated principal (tenant:<slug> for an API key). The value is only logged as claimedActor."},{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"mid","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"milestone"},"404":{"description":"not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"code":{"type":"string"},"unmet":{"type":"array","items":{"type":"string"}}}}}}}},"description":"NOT OFFERED on a rail that cannot hold value (decision 2026-10-02): creation returns 409 escrow_not_offered and funding 409 escrow_hold_unsupported. Check GET /api/v1/capabilities. **NOT AVAILABLE IN PRODUCTION.** Managed Escrow availability depends on the settlement adapter the deployment is running. In production that rail forwards funds to the destination wallet as a single transfer, so there is no custodial hold: funding refuses with `409 escrow_hold_unsupported` and release/refund with `409 escrow_disbursement_unsupported`, and an escrow created there can never be funded, released or refunded. The sandbox's mock adapter does hold, so these calls complete there without moving money — do not read a sandbox success as production behaviour. `GET /api/v1/capabilities` on the host you are calling is the authoritative answer."}},"/api/v1/escrow/{id}/approvals":{"post":{"tags":["HOLD — Managed Escrow"],"summary":"Record an approval or rejection","description":"NOT OFFERED on a rail that cannot hold value (decision 2026-10-02): creation returns 409 escrow_not_offered and funding 409 escrow_hold_unsupported. Check GET /api/v1/capabilities. approverRef is unique per escrow, so one approver cannot satisfy dual approval alone.\n\n**NOT AVAILABLE IN PRODUCTION.** Managed Escrow availability depends on the settlement adapter the deployment is running. In production that rail forwards funds to the destination wallet as a single transfer, so there is no custodial hold: funding refuses with `409 escrow_hold_unsupported` and release/refund with `409 escrow_disbursement_unsupported`, and an escrow created there can never be funded, released or refunded. The sandbox's mock adapter does hold, so these calls complete there without moving money — do not read a sandbox success as production behaviour. `GET /api/v1/capabilities` on the host you are calling is the authoritative answer.","parameters":[{"name":"X-Actor","in":"header","required":false,"schema":{"type":"string"},"description":"Optional, UNVERIFIED label for the person acting. It is NOT the audit actor: the StateTransition actor is always the authenticated principal (tenant:<slug> for an API key). The value is only logged as claimedActor."},{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["approverRef"],"properties":{"approverRef":{"type":"string"},"decision":{"type":"string","enum":["approve","reject"],"default":"approve"},"note":{"type":"string"}}}}}},"responses":{"201":{"description":"approval"},"400":{"description":"bad decision","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"code":{"type":"string"},"unmet":{"type":"array","items":{"type":"string"}}}}}}}}}},"/admin/tenants":{"post":{"tags":["Admin"],"summary":"Create a tenant","security":[{"internalToken":[]}],"responses":{"201":{"description":"tenant"}}}},"/admin/tenants/{id}/keys":{"post":{"tags":["Admin"],"summary":"Mint a tenant API key (shown once)","security":[{"internalToken":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"201":{"description":"apiKey"}}}},"/admin/tenants/{id}/webhooks":{"post":{"tags":["Webhooks"],"summary":"Register a webhook endpoint (returns the signing secret once)","security":[{"internalToken":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["url"],"properties":{"url":{"type":"string"},"secret":{"type":"string"}}}}}},"responses":{"201":{"description":"endpoint + secret"}}}},"/admin/fee-rules":{"post":{"tags":["Admin"],"summary":"Create a fee rule","security":[{"internalToken":[]}],"responses":{"201":{"description":"rule"}}}},"/admin/settlements/{id}/history":{"get":{"tags":["Admin"],"summary":"Audited state transitions for a settlement","security":[{"internalToken":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"history"}}}},"/admin/review-queue":{"get":{"tags":["Admin"],"summary":"Manual review queue (blocked escrows + in-flight settlements)","security":[{"internalToken":[]}],"responses":{"200":{"description":"queue"}}}},"/admin/webhooks/deliveries":{"get":{"tags":["Webhooks"],"summary":"Delivery tracking","security":[{"internalToken":[]}],"parameters":[{"name":"status","in":"query","schema":{"type":"string","enum":["pending","delivered","failed","exhausted"]}},{"name":"event","in":"query","schema":{"type":"string"}},{"name":"tenantId","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"deliveries"}}}},"/admin/webhooks/sweep":{"post":{"tags":["Webhooks"],"summary":"Force a retry pass now","security":[{"internalToken":[]}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"limit":{"type":"integer"},"force":{"type":"boolean"}}}}}},"responses":{"200":{"description":"sweep result"}}}},"/admin/recon":{"get":{"tags":["Reconciliation"],"summary":"Reconciliation export (settlements + escrow)","security":[{"internalToken":[]}],"parameters":[{"name":"from","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"tenantId","in":"query","schema":{"type":"string"}},{"name":"type","in":"query","schema":{"type":"string","enum":["all","settlements","escrow"]}},{"name":"format","in":"query","schema":{"type":"string","enum":["json","csv"]}}],"responses":{"200":{"description":"export with summary, in-flight and unbalanced lists"}}}},"/admin/recon/daily":{"get":{"tags":["Reconciliation"],"summary":"Daily close export for one UTC day (CSV attachment)","security":[{"internalToken":[]}],"parameters":[{"name":"date","in":"query","schema":{"type":"string","format":"date"},"description":"YYYY-MM-DD, defaults to yesterday UTC"},{"name":"tenantId","in":"query","schema":{"type":"string"}},{"name":"format","in":"query","schema":{"type":"string","enum":["csv","json"]}}],"responses":{"200":{"description":"CSV or JSON"}}}},"/admin/flags":{"get":{"tags":["Feature flags"],"summary":"Effective flag values","security":[{"internalToken":[]}],"parameters":[{"name":"tenantId","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"flags"}}},"put":{"tags":["Feature flags"],"summary":"Enable/disable a vendor or capability","security":[{"internalToken":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["key","enabled"],"properties":{"key":{"type":"string","enum":["vendor.collection.mock","vendor.collection.bridge","vendor.conversion.mock","vendor.conversion.bridge","vendor.conversion.zerohash","escrow.enabled","escrow.rail.stablecoin","escrow.rail.fiat","kyt.screening.enforce","kyt.adapter.mock","idempotency.require","webhooks.retry.enabled"]},"enabled":{"type":"boolean"},"tenantId":{"type":"string","nullable":true},"note":{"type":"string"}}}}}},"responses":{"200":{"description":"updated + effective flags"},"400":{"description":"unknown flag","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"code":{"type":"string"},"unmet":{"type":"array","items":{"type":"string"}}}}}}}}}},"/demo/overview":{"get":{"tags":["Admin"],"summary":"Read-only demo console aggregates","security":[],"responses":{"200":{"description":"aggregates"}}}},"/api/v1/disputes/alerts":{"post":{"tags":["disputes"],"summary":"Ingest a dispute alert (match -> rules -> action -> thresholds)","responses":{"200":{"description":"duplicate alert (source,externalId) - no side effects"},"201":{"description":"alert ingested"},"400":{"description":"Idempotency-Key required / invalid amount"},"501":{"description":"alert source disabled - no vendor contract"}},"parameters":[{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string"},"description":"required: ingest can trigger a refund"},{"name":"source","in":"query","schema":{"type":"string","enum":["mock","manual","network","acquirer","ethoca","verifi_cdrn","verifi_rdr"]}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}}},"get":{"tags":["disputes"],"summary":"List alerts","responses":{"200":{"description":"ok"}},"parameters":[{"name":"status","in":"query","schema":{"type":"string"}},{"name":"kind","in":"query","schema":{"type":"string"}},{"name":"source","in":"query","schema":{"type":"string"}}]}},"/api/v1/disputes/queue":{"get":{"tags":["disputes"],"summary":"Unmatched-alert queue (fail-safe; nothing is dropped)","responses":{"200":{"description":"ok"}}}},"/api/v1/disputes/alerts/{id}":{"get":{"tags":["disputes"],"summary":"Alert detail with rule audit, match candidates and refund","responses":{"200":{"description":"ok"}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"x-infinihash-availability":{"status":"unavailable","reason":"No settlement adapter on this deployment implements a refund leg, so no rail can execute one. Dispute rules that would auto-refund are routed to human review instead of recording a refund that did not happen.","errorCode":"refund_unsupported","httpStatus":409,"capability":"refunds"}}},"/api/v1/disputes/alerts/{id}/match":{"post":{"tags":["disputes"],"summary":"Operator manually binds a queued alert to a payment","responses":{"200":{"description":"ok"}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}}}},"/api/v1/disputes/cases":{"post":{"tags":["disputes"],"summary":"Open a dispute case","responses":{"201":{"description":"created"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}}},"get":{"tags":["disputes"],"summary":"List cases","responses":{"200":{"description":"ok"}}}},"/api/v1/disputes/cases/{id}":{"get":{"tags":["disputes"],"summary":"Case detail with full transition history","responses":{"200":{"description":"ok"}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}]}},"/api/v1/disputes/cases/{id}/checklist":{"get":{"tags":["disputes"],"summary":"Evidence checklist: required vs provided vs missing","responses":{"200":{"description":"ok"}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}]}},"/api/v1/disputes/cases/{id}/gather":{"post":{"tags":["disputes"],"summary":"open -> gathering","responses":{"200":{"description":"ok"}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}]}},"/api/v1/disputes/cases/{id}/evidence":{"post":{"tags":["disputes"],"summary":"Attach evidence metadata + object-store ref","responses":{"201":{"description":"attached"},"409":{"description":"case is closed"}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}}}},"/api/v1/disputes/cases/{id}/submit":{"post":{"tags":["disputes"],"summary":"gathering -> submitted","responses":{"200":{"description":"submitted"},"409":{"description":"evidence_incomplete or representment_expired"}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}}}},"/api/v1/disputes/cases/{id}/outcome":{"post":{"tags":["disputes"],"summary":"submitted -> won | lost","responses":{"200":{"description":"decided"},"409":{"description":"invalid_state_transition"}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}}}},"/api/v1/disputes/metrics":{"get":{"tags":["disputes"],"summary":"Chargeback ratios by program / merchant / MID / channel","responses":{"200":{"description":"ok"}},"parameters":[{"name":"from","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"scope","in":"query","schema":{"type":"string"},"description":"comma list: program,merchant,mid,channel"}]}},"/api/v1/disputes/controls":{"get":{"tags":["disputes"],"summary":"Program dispute config + recent control events","responses":{"200":{"description":"ok"}}}},"/api/v1/disputes/controls/evaluate":{"post":{"tags":["disputes"],"summary":"Force a ratio/threshold evaluation","responses":{"200":{"description":"ok"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}}}},"/api/v1/disputes/credit-status":{"get":{"tags":["disputes"],"summary":"Credit-enablement gate","responses":{"200":{"description":"enabled"},"409":{"description":"credit_suspended - dispute ratio policy breached"}}}},"/webhooks/disputes/{source}":{"post":{"tags":["disputes"],"summary":"Vendor alert callback (internal token + adapter HMAC)","responses":{"200":{"description":"duplicate"},"201":{"description":"ingested"},"401":{"description":"bad_signature"},"501":{"description":"source disabled / vendor not implemented"}},"parameters":[{"name":"source","in":"path","required":true,"schema":{"type":"string"}},{"name":"tenantId","in":"query","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}}}},"/admin/disputes/seed-rules":{"post":{"tags":["disputes","admin"],"summary":"Install the platform default rule set","responses":{"200":{"description":"ok"}}}},"/admin/disputes/rules":{"get":{"tags":["disputes","admin"],"summary":"Dispute rules (deterministic evaluation order)","responses":{"200":{"description":"ok"}}},"post":{"tags":["disputes","admin"],"summary":"Create a dispute rule","responses":{"201":{"description":"created"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}}}},"/admin/disputes/evaluate-all":{"post":{"tags":["disputes","admin"],"summary":"Force a threshold sweep across every configured scope","responses":{"200":{"description":"ok"}}}},"/admin/disputes/sweep-expired":{"post":{"tags":["disputes","admin"],"summary":"Expire representment windows that lapsed","responses":{"200":{"description":"ok"}}}},"/admin/disputes/queue":{"get":{"tags":["disputes","admin"],"summary":"Cross-tenant unmatched-alert queue","responses":{"200":{"description":"ok"}}}},"/admin/disputes/config/{tenantId}":{"get":{"tags":["disputes","admin"],"summary":"Program dispute config","responses":{"200":{"description":"ok"}},"parameters":[{"name":"tenantId","in":"path","required":true,"schema":{"type":"string"}}]},"put":{"tags":["disputes","admin"],"summary":"Set thresholds, caps and blackout windows","responses":{"200":{"description":"ok"}},"parameters":[{"name":"tenantId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}}}},"/admin/disputes/controls/{tenantId}/reinstate":{"post":{"tags":["disputes","admin"],"summary":"Reinstate credit enablement (actor + reason REQUIRED, both logged)","responses":{"200":{"description":"reinstated"},"400":{"description":"actor_required / reason_required"}},"parameters":[{"name":"tenantId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}}}},"/api/v1/capabilities":{"get":{"tags":["Health"],"summary":"What this deployment can actually serve","description":"Computed from the settlement adapter this deployment is running, through the same predicates the money path uses — so it cannot drift from behaviour the way a written-down status table would. Read this before integrating: it is the authoritative statement of which operations in this document will succeed here, and for those that will not, the exact error code you would receive.\n\n`available: true` means configured and coded such that a call can succeed. It is not a liveness probe — only that distinction is knowable without spending money.\n\nUnauthenticated: it carries no tenant data, and a prospect needs it before they have a key.","security":[],"responses":{"200":{"description":"Capability report","content":{"application/json":{"schema":{"type":"object","properties":{"service":{"type":"string"},"settlementAdapter":{"type":"string","enum":["bridge","mock","disabled"],"description":"The rail in force. Named the way /health names it."},"capabilities":{"type":"object","description":"One entry per product surface. Each has `available`; when false it also carries `reason`, the `code` and `status` a call would return, and where documented.","additionalProperties":{"type":"object","properties":{"available":{"type":"boolean"},"reason":{"type":"string"},"code":{"type":"string"},"status":{"type":"integer"},"endpoint":{"type":"string"},"docs":{"type":"string"}}}},"ts":{"type":"string","format":"date-time"}}}}}}}}},"/onboard/signup":{"post":{"tags":["Health"],"summary":"Request access (self-serve onboarding)","description":"Start an onboarding application without an Infinihash employee. Creates the local application only — it never causes a billable partner KYB or identity inquiry, which happens after a human approves.\n\nAlways answers **202** with the same body whether or not this email already has an application, so the endpoint cannot be used to enumerate who is onboarding. It deliberately does not return the onboarding link: the link is emailed after approval.\n\nPublic and unauthenticated; rate limited to 10 requests per hour per source address.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["legalName","contactEmail"],"properties":{"legalName":{"type":"string","minLength":2,"maxLength":300,"description":"Registered business name, as it appears on formation documents."},"contactEmail":{"type":"string","format":"email","description":"Work email the secure onboarding link will be sent to."},"entityType":{"type":"string","maxLength":60},"website":{"type":"string","maxLength":120}}}}}},"responses":{"202":{"description":"Request accepted. Same response for a repeat submission."},"400":{"description":"missing_fields | invalid_legal_name | invalid_email"},"429":{"description":"Rate limited — too many requests from this source."}}}},"/api/v1/records/export":{"get":{"tags":["COMPLY — Records"],"summary":"Export your money records (signed)","description":"Records retention: StateTransition is append-only and ledger rows cannot be deleted inside the 5-year BSA window (enforced in Postgres). This is the tenant's own evidence export; the tenant is taken from the API key.","parameters":[{"name":"from","in":"query","schema":{"type":"string","format":"date-time"},"description":"inclusive lower bound on createdAt (default: beginning)"},{"name":"to","in":"query","schema":{"type":"string","format":"date-time"},"description":"inclusive upper bound on createdAt (default: now)"},{"name":"format","in":"query","schema":{"type":"string","enum":["json","csv"]},"description":"json (default) or one flat csv table"}],"responses":{"200":{"description":"The tenant's invoices, payments, settlements, reserve entries and state transitions in range. Headers: X-Infinihash-Export-SHA256 (hex sha256 of the body) and X-Infinihash-Export-Signature (`ed25519;keyid=<id>;sig=<base64>` over the exact body bytes, verifiable with GET /api/v1/records/signing-key — or `unsigned` when this deployment has no signing key)."},"400":{"description":"bad_range / bad_format"},"413":{"description":"export_too_large — more than 50,000 rows of one type; narrow from/to"}},"security":[{"tenantApiKey":[]}]}},"/api/v1/records/signing-key":{"get":{"tags":["COMPLY — Records"],"summary":"Public key that verifies record exports","responses":{"200":{"description":"{alg:'ed25519', keyId, publicKeyPem}"},"404":{"description":"export_unsigned — this deployment does not sign exports"}}}},"/api/v1/usage":{"get":{"tags":["MOVE — Payments & Settlement"],"summary":"Monthly usage statement","description":"Volume collected, fees withheld at the rail (the billed amount), settled amounts and reserves for one calendar month (UTC), computed from the ledger. Billing model: per transaction, withheld at collection — see docs/ADR-002-entitlement-and-billing.md.","parameters":[{"name":"month","in":"query","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}$"},"description":"YYYY-MM (default: current month)"}],"security":[{"tenantApiKey":[]}],"responses":{"200":{"description":"statement"},"400":{"description":"bad_month"}}}}}}