# Ambit Rail API — OpenAPI SSoT (T38 / U4.1, spec-first)
# ─────────────────────────────────────────────────────────────
# STATUS: v0.1.0-draft. Published before implementation (spec-first, 方案 §8.4).
# Human docs are GENERATED from this file (hand-written reference pages = VIOLATION).
# Docs are public; production keys are gated behind KYB + partner agreement (§8.4 r1).
# The API is a projection of the kernel four commands (verify / quote / settle / attest)
# and adds NO capability beyond them (§8.4 r2): partners can create intents; the final
# money-moving step (USDT release) always remains behind internal maker-checker.
openapi: 3.1.0
info:
  title: Ambit Rail API
  version: 0.1.0-draft
  summary: Regulated AUD⇄USDT conversion desk — AUD corridor endpoint for partners.
  description: |
    Ambit (Legger Pty Ltd, Australia) operates a regulated AUD⇄USDT conversion desk:
    named virtual accounts (VA) for AUD collection, payer-name matching, quote-locked
    conversion, maker-checker gated USDT release, and per-transaction attestation
    receipts backed by double-entry ledgering and daily three-way reconciliation.

    ## Design guarantees (compliance gates are first-class API states)
    - A deposit whose payer name does not match the VA holder is **suspended**
      (`suspended_name_mismatch`) — it never auto-posts.
    - A release with incomplete Travel Rule data is **blocked** (`blocked_tr_incomplete`),
      fail-closed.
    - Trading freezes automatically if daily reconciliation is not balanced.
    These are not error edge-cases; they are documented, testable sandbox scenarios.

    ## Environments
    - Sandbox: deterministic mock rails, self-serve keys, resettable data.
    - Production: keys issued only after partner KYB + agreement.

    ## Conventions
    - All mutating requests REQUIRE an `Idempotency-Key` header.
    - Errors use RFC 7807 `application/problem+json`.
    - Webhook event names follow `domain.resource.event`.
    - Money is expressed as integer minor units (AUD: e4, USDT: e6) plus a string
      decimal mirror for display; never floats.
  x-audience: payment aggregators, PSPs, institutional partners — and their AI agents
  x-machine-readable-entrypoints:
    - /developers/llms.txt
    - /developers/openapi.yaml
servers:
  - url: https://sandbox.api.ambitrail.com/v1
    description: Sandbox (planned — see x-status)
  - url: https://api.ambitrail.com/v1
    description: Production (planned — keys gated behind KYB)
x-status:
  lifecycle: spec-first-draft
  sandbox: not yet live (T40)
  production: not yet live (T28; gated on Q23/Q24)

security:
  - ApiKeyAuth: []

tags:
  - name: customers
    description: "verify — customer identity, KYC lifecycle (Sumsub-backed)"
  - name: virtual-accounts
    description: "settle — named AUD collection accounts (BSB + account + PayID)"
  - name: quotes
    description: "quote — locked pricing with expiry"
  - name: orders
    description: "settle — quote consumption through to gated release"
  - name: deposits
    description: "settle — AUD receipts, name-matching states (read-only)"
  - name: receipts
    description: "attest — verifiable transaction attestations"
  - name: webhooks
    description: event subscriptions and delivery

paths:
  /customers:
    post:
      tags: [customers]
      operationId: createCustomer
      summary: Create a customer (individual or company)
      description: |
        Creates the customer record that all money movement hangs off.
        KYC ownership model (own-KYC via kyc-session vs reliance on partner KYC)
        is pending decision Q23 — v0.1 documents the own-KYC flow only.
      parameters: [{ $ref: "#/components/parameters/IdempotencyKey" }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CustomerCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Customer" }
        default: { $ref: "#/components/responses/Problem" }
  /customers/{customerId}:
    get:
      tags: [customers]
      operationId: getCustomer
      summary: Fetch a customer
      parameters: [{ $ref: "#/components/parameters/CustomerId" }]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Customer" }
        default: { $ref: "#/components/responses/Problem" }
  /customers/{customerId}/kyc-session:
    post:
      tags: [customers]
      operationId: createKycSession
      summary: Start a hosted KYC session (Sumsub-backed)
      description: |
        Returns a hosted verification link / SDK token for the end customer.
        Outcome arrives via `customer.kyc.updated` webhook. Until KYC is GREEN with a
        verifiable evidence source, all money actions for this customer are fail-closed.
      parameters: [{ $ref: "#/components/parameters/CustomerId" }, { $ref: "#/components/parameters/IdempotencyKey" }]
      responses:
        "201":
          description: Session created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/KycSession" }
        default: { $ref: "#/components/responses/Problem" }

  /virtual-accounts:
    post:
      tags: [virtual-accounts]
      operationId: createVirtualAccount
      summary: Issue a named AUD virtual account for a KYC-passed customer
      description: |
        VA is issued in the customer's verified legal name. Deposits from any other
        payer name will suspend (see Deposit.status). KYC must be GREEN — otherwise 403
        `blocked_kyc_incomplete`.
      parameters: [{ $ref: "#/components/parameters/IdempotencyKey" }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [customer_id]
              properties:
                customer_id: { type: string }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/VirtualAccount" }
        default: { $ref: "#/components/responses/Problem" }
  /virtual-accounts/{vaId}:
    get:
      tags: [virtual-accounts]
      operationId: getVirtualAccount
      summary: Fetch a virtual account
      parameters:
        - name: vaId
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/VirtualAccount" }
        default: { $ref: "#/components/responses/Problem" }

  /quotes:
    post:
      tags: [quotes]
      operationId: createQuote
      summary: Lock a conversion quote (AUD → USDT)
      description: Locked rate with `expires_at`. Consuming an expired quote fails.
      parameters: [{ $ref: "#/components/parameters/IdempotencyKey" }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/QuoteCreate" }
      responses:
        "201":
          description: Locked
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Quote" }
        default: { $ref: "#/components/responses/Problem" }
  /quotes/{quoteId}:
    get:
      tags: [quotes]
      operationId: getQuote
      summary: Fetch a quote
      parameters:
        - name: quoteId
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Quote" }
        default: { $ref: "#/components/responses/Problem" }

  /orders:
    post:
      tags: [orders]
      operationId: createOrder
      summary: Create an order from a locked quote
      description: |
        Binds quote → expected deposit → gated release. The USDT release step is NEVER
        triggered by this API: it requires internal maker-checker approval (AU-side).
        Travel Rule beneficiary data is required before release can be approved
        (fail-closed, `blocked_tr_incomplete`).
      parameters: [{ $ref: "#/components/parameters/IdempotencyKey" }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/OrderCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Order" }
        default: { $ref: "#/components/responses/Problem" }
  /orders/{orderId}:
    get:
      tags: [orders]
      operationId: getOrder
      summary: Fetch an order
      parameters:
        - name: orderId
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Order" }
        default: { $ref: "#/components/responses/Problem" }

  /deposits:
    get:
      tags: [deposits]
      operationId: listDeposits
      summary: List deposits (read-only)
      parameters:
        - name: customer_id
          in: query
          schema: { type: string }
        - name: status
          in: query
          schema: { $ref: "#/components/schemas/DepositStatus" }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Deposit" }
        default: { $ref: "#/components/responses/Problem" }
  /deposits/{depositId}:
    get:
      tags: [deposits]
      operationId: getDeposit
      summary: Fetch a deposit
      parameters:
        - name: depositId
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Deposit" }
        default: { $ref: "#/components/responses/Problem" }

  /receipts/{receiptId}:
    get:
      tags: [receipts]
      operationId: getReceipt
      summary: Fetch an attestation receipt
      description: |
        Machine-verifiable record of a completed conversion: amounts, fee waterfall,
        rate, on-chain tx hash, content hash (+ signature, GA with U1.3/T46).
      parameters:
        - name: receiptId
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Receipt" }
        default: { $ref: "#/components/responses/Problem" }
  /receipts/{receiptId}/verify:
    get:
      tags: [receipts]
      operationId: verifyReceipt
      summary: Verify a receipt by content hash (PUBLIC — no auth)
      security: []
      parameters:
        - name: receiptId
          in: path
          required: true
          schema: { type: string }
        - name: content_hash
          in: query
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Verification result
          content:
            application/json:
              schema:
                type: object
                required: [valid]
                properties:
                  valid: { type: boolean }
                  issued_at: { type: string, format: date-time }
        default: { $ref: "#/components/responses/Problem" }

  /webhook-endpoints:
    post:
      tags: [webhooks]
      operationId: createWebhookEndpoint
      summary: Register a webhook endpoint
      parameters: [{ $ref: "#/components/parameters/IdempotencyKey" }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url, events]
              properties:
                url: { type: string, format: uri }
                events:
                  type: array
                  items: { $ref: "#/components/schemas/WebhookEventType" }
      responses:
        "201":
          description: Created (returns signing secret ONCE)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/WebhookEndpoint" }
        default: { $ref: "#/components/responses/Problem" }
    get:
      tags: [webhooks]
      operationId: listWebhookEndpoints
      summary: List webhook endpoints (with delivery log pointers)
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/WebhookEndpoint" }
        default: { $ref: "#/components/responses/Problem" }

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-Api-Key
      description: |
        Sandbox: self-serve keys. Production: issued only after partner KYB + agreement.
        Mutating requests are additionally HMAC-signed (X-Ambit-Signature over
        timestamp + method + path + body; X-Ambit-Timestamp within ±300s) — final
        scheme frozen at T28.
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      description: Client-generated key; retries with the same key return the original result.
      schema: { type: string, maxLength: 128 }
    CustomerId:
      name: customerId
      in: path
      required: true
      schema: { type: string }
  responses:
    Problem:
      description: RFC 7807 problem
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
  schemas:
    Problem:
      type: object
      description: RFC 7807. Compliance blocks carry a machine-readable `compliance_reason`.
      required: [type, title, status]
      properties:
        type: { type: string, format: uri }
        title: { type: string }
        status: { type: integer }
        detail: { type: string }
        compliance_reason:
          type: string
          enum:
            - blocked_kyc_incomplete
            - blocked_kyc_evidence_missing
            - blocked_tr_incomplete
            - suspended_name_mismatch
            - trading_frozen_by_recon
            - funding_reversed
    Money:
      type: object
      required: [currency, minor, decimal]
      properties:
        currency: { type: string, enum: [AUD, USDT] }
        minor:
          type: integer
          description: "Integer minor units. AUD: e4. USDT: e6. Never floats."
        decimal: { type: string, description: "String decimal mirror for display." }
    CustomerCreate:
      type: object
      required: [type, legal_name]
      properties:
        type: { type: string, enum: [individual, company] }
        legal_name: { type: string, description: VA will be issued in this verified name. }
        email: { type: string, format: email }
        external_ref: { type: string, description: Partner-side customer reference. }
    Customer:
      allOf:
        - $ref: "#/components/schemas/CustomerCreate"
        - type: object
          required: [id, kyc_status, created_at]
          properties:
            id: { type: string }
            kyc_status: { type: string, enum: [none, pending, approved, rejected, frozen] }
            kyc_evidence_source:
              type: string
              description: Only webhook/api-verified evidence unlocks money actions (ADR-16).
            created_at: { type: string, format: date-time }
    KycSession:
      type: object
      required: [session_id, verification_url, expires_at]
      properties:
        session_id: { type: string }
        verification_url: { type: string, format: uri }
        sdk_token: { type: string }
        expires_at: { type: string, format: date-time }
    VirtualAccount:
      type: object
      required: [id, customer_id, account_name, bsb, account_number, status]
      properties:
        id: { type: string }
        customer_id: { type: string }
        account_name: { type: string, description: Customer's verified legal name — name-match enforced. }
        bsb: { type: string }
        account_number: { type: string }
        payid: { type: string }
        status: { type: string, enum: [active, inactive] }
    QuoteCreate:
      type: object
      required: [customer_id, sell, buy_currency]
      properties:
        customer_id: { type: string }
        sell: { $ref: "#/components/schemas/Money" }
        buy_currency: { type: string, enum: [USDT] }
    Quote:
      type: object
      required: [id, customer_id, sell, buy, rate, expires_at, status]
      properties:
        id: { type: string }
        customer_id: { type: string }
        sell: { $ref: "#/components/schemas/Money" }
        buy: { $ref: "#/components/schemas/Money" }
        rate: { type: string, description: String decimal, side unfavourable to drift. }
        expires_at: { type: string, format: date-time }
        status: { type: string, enum: [locked, consumed, expired] }
    TravelRuleBeneficiary:
      type: object
      description: |
        Travel Rule data required before release approval (AUSTRAC, in force 2026-07-01,
        no de-minimis). Field-level source: research §14. Ownership model pending Q23.
      required: [full_name, wallet_address]
      properties:
        full_name: { type: string }
        wallet_address: { type: string }
        wallet_type: { type: string, enum: [custodial_vasp, self_hosted] }
        vasp_name: { type: string }
        country: { type: string }
    OrderCreate:
      type: object
      required: [quote_id, beneficiary]
      properties:
        quote_id: { type: string }
        beneficiary: { $ref: "#/components/schemas/TravelRuleBeneficiary" }
        external_ref: { type: string }
    Order:
      type: object
      required: [id, quote_id, customer_id, status, created_at]
      properties:
        id: { type: string }
        quote_id: { type: string }
        customer_id: { type: string }
        deposit_id: { type: string }
        status:
          type: string
          description: |
            Compliance gates are first-class states, not errors. `awaiting_manual_release`
            reflects the structural rule: automation stops before release (maker-checker).
          enum:
            - awaiting_funds
            - funds_received
            - awaiting_manual_release
            - released
            - settled
            - suspended_name_mismatch
            - blocked_tr_incomplete
            - funding_reversed
            - cancelled
        receipt_id: { type: string }
        created_at: { type: string, format: date-time }
    DepositStatus:
      type: string
      enum: [received, matched, posted, suspended_name_mismatch, returned, reversed]
    Deposit:
      type: object
      required: [id, va_id, amount, payer_name, status, received_at]
      properties:
        id: { type: string }
        va_id: { type: string }
        customer_id: { type: string }
        amount: { $ref: "#/components/schemas/Money" }
        payer_name: { type: string }
        name_match: { type: string, enum: [pending, matched, mismatch] }
        status: { $ref: "#/components/schemas/DepositStatus" }
        received_at: { type: string, format: date-time }
    Receipt:
      type: object
      required: [id, order_id, initial_amount, final_amount, rate, content_hash, issued_at]
      properties:
        id: { type: string }
        order_id: { type: string }
        initial_amount: { $ref: "#/components/schemas/Money" }
        fees:
          type: array
          items:
            type: object
            required: [label, amount]
            properties:
              label: { type: string }
              amount: { $ref: "#/components/schemas/Money" }
        final_amount: { $ref: "#/components/schemas/Money" }
        rate: { type: string }
        destination_tx_hash: { type: string }
        content_hash: { type: string, description: "Hash over canonical receipt JSON; verify via GET /receipts/{id}/verify." }
        signature: { type: string, description: "Detached signature — GA with U1.3/T46." }
        pdf_url: { type: string, format: uri }
        issued_at: { type: string, format: date-time }
    WebhookEventType:
      type: string
      enum:
        - customer.kyc.updated
        - va.assigned
        - deposit.received
        - deposit.matched
        - deposit.suspended
        - deposit.reversed
        - quote.expired
        - order.state.changed
        - order.released
        - order.settled
        - receipt.issued
    WebhookEndpoint:
      type: object
      required: [id, url, events, status]
      properties:
        id: { type: string }
        url: { type: string, format: uri }
        events:
          type: array
          items: { $ref: "#/components/schemas/WebhookEventType" }
        status: { type: string, enum: [active, disabled] }
        secret: { type: string, description: "Returned once at creation; HMAC-SHA256 event signing." }
    WebhookEnvelope:
      type: object
      description: Delivery envelope for all events. Signed; retried with backoff; delivery log queryable.
      required: [id, type, created_at, data]
      properties:
        id: { type: string }
        type: { $ref: "#/components/schemas/WebhookEventType" }
        created_at: { type: string, format: date-time }
        data: { type: object }

x-sandbox-scenarios:
  description: |
    Deterministic magic-amount fixtures (T40). Send a deposit with the given AUD cents
    suffix in sandbox to trigger each compliance path end-to-end.
  scenarios:
    - amount_suffix: ".00"
      result: happy path — matched → posted → order releasable → receipt issued
    - amount_suffix: ".13"
      result: payer name mismatch — deposit suspended_name_mismatch, manual-review queue
    - amount_suffix: ".31"
      result: deposit later reversed — funding_reversed cascades to order/release locks
    - amount_suffix: ".77"
      result: TR data incomplete — release approval blocked_tr_incomplete (fail-closed)
