openapi: 3.1.0

info:
  title: 0Pools Partner API
  version: '2026-05-25'
  summary: Cross-border liquidity pools for partner on-ramp / off-ramp settlement.
  description: |
    The 0Pools Partner API lets approved partners discover pools, request
    firm or indicative quotes, execute on-ramp / off-ramp transactions, and
    reconcile trades, quotes, balances and ledger entries.

    **Gated early access.** Pool discovery is open to any valid publishable
    key, but the entitled routes (`capabilities`, `quote`, `transact`,
    `balance`) require your partner account to be explicitly enabled and
    approved. Until then those routes return `403` with a denial code.

    **Money is always a decimal string.** Every monetary amount and rate is
    serialized as a base-10 decimal string (e.g. `"1000.00"`, `"0.92"`) to
    avoid floating-point drift. Fields suffixed `Bps` are integers
    (basis points).

    **Per-partner scoping.** Every object is scoped to the partner that owns
    the credential. Referencing an object that belongs to another partner
    returns `404` (never `403`) so identifiers are not enumerable.

    **Two credentials, two scopes:**

    | Audience | Credential | Use |
    |---|---|---|
    | Partner web page | `pk_test_*` / `pk_live_*` | Pool discovery only (`GET /pools`). Browser-safe. |
    | Partner server | `sk_test_*` / `sk_live_*` | Everything else. NEVER expose in a browser. |

    Errors follow a single envelope — see
    [`Error`](#components/schemas/Error). Every response carries an
    `X-Request-Id` header for support correlation.
  contact:
    name: 0Pools Partner support
    url: https://0bit.io/support
  license:
    name: Proprietary
    identifier: LicenseRef-Proprietary

servers:
  - url: https://pools-api.0bit.app/v1
    description: Production
  - url: https://pools-api-sandbox.0bit.app/v1
    description: Sandbox (test_ keys). Same surface; pools and balances are simulated.

tags:
  - name: Discovery
    description: |
      Open discovery with a publishable key, plus entitled per-pool
      capability and balance reads with a secret key.
  - name: Quotes
    description: |
      Request, inspect and reject quotes. Quotes are fail-soft — when the
      pool cannot price an order the call still returns `200` with
      `available: false` and an `unavailableReason`.
  - name: Transactions
    description: |
      Execute a quote and follow settlement. Transactions are idempotent on
      the underlying `quoteId`.
  - name: Reconciliation
    description: |
      Cursor-paginated trade history, funding instructions and the funding
      ledger.

security:
  - SecretKey: []

paths:
  /pools:
    get:
      tags: [Discovery]
      operationId: listPools
      summary: List discoverable pools
      description: |
        Returns the pools visible to your account. Open to any valid
        publishable key — this is the only route that accepts `pk_*`.
        `available` reflects whether the pool can currently price and
        execute orders; it may be `false` transiently.
      security:
        - PublishableKey: []
      responses:
        '200':
          description: Pool list.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
          content:
            application/json:
              schema:
                type: object
                required: [pools]
                properties:
                  pools:
                    type: array
                    items:
                      $ref: '#/components/schemas/PoolListItem'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'

  /pools/{id}/capabilities:
    get:
      tags: [Discovery]
      operationId: getPoolCapabilities
      summary: Get pool capabilities
      description: |
        Returns the trading parameters for a single pool: supported delivery
        networks, sides, tier, fee/spread caps and order bounds. Entitled
        route — requires an enabled, approved partner account.
      parameters:
        - $ref: '#/components/parameters/PoolId'
      responses:
        '200':
          description: Pool capabilities.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PoolCapabilities'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/ForbiddenEntitled'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'

  /pools/{id}/quote:
    post:
      tags: [Quotes]
      operationId: createQuote
      summary: Request a quote
      description: |
        Requests a firm (default) or indicative quote for a pool order.

        **Fail-soft:** when the pool cannot price the order the call still
        returns HTTP `200` with `available: false`, no `quoteId`, and an
        `unavailableReason` of `pool_dry`, `engine_unavailable` or
        `rate_unavailable`. Only a quote with `available: true` and a
        `quoteId` is executable. Entitled route.
      parameters:
        - $ref: '#/components/parameters/PoolId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/QuoteRequest'
      responses:
        '200':
          description: |
            Quote result. Inspect `available` — a fail-soft unavailable
            quote is also returned with status `200`.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/QuoteResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/ForbiddenEntitled'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'

  /pools/{id}/transact:
    post:
      tags: [Transactions]
      operationId: transact
      summary: Execute a quote
      description: |
        Executes a previously issued executable quote. The `Idempotency-Key`
        header is **required**; execution is idempotent on the underlying
        `quoteId`, so a retried request collapses to the original result with
        `idempotent: true`. Entitled route.
      parameters:
        - $ref: '#/components/parameters/PoolId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [quoteId]
              properties:
                quoteId:
                  type: string
                  description: An executable quote id from `POST /pools/{id}/quote`.
                  examples: ['qt_5f3a9c2e']
      responses:
        '200':
          description: Transaction accepted (or idempotent replay).
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TransactResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          description: Insufficient pool funds to reserve the order.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          $ref: '#/components/responses/ForbiddenEntitled'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: |
            Conflict — the quote is expired, already consumed, its
            reservation could not be honored, or the `Idempotency-Key`
            was reused with a different payload.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '501':
          description: |
            Not implemented — `off_ramp` execution is not yet available for
            this pool.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /pools/transactions/{quoteId}:
    get:
      tags: [Transactions]
      operationId: getTransactionStatus
      summary: Poll transaction status
      description: |
        Returns the current status of the transaction for a quote and, as a
        side effect, **advances settlement** for that transaction. Poll this
        to drive an order toward a terminal state. The terminal `returned`
        status (auto-refund) is surfaced here and in the ledger; there is no
        webhook.
      parameters:
        - $ref: '#/components/parameters/QuoteIdPath'
      responses:
        '200':
          description: Transaction status.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TransactionStatusResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/ForbiddenSecretRead'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'

  /pools/trades:
    get:
      tags: [Reconciliation]
      operationId: listTrades
      summary: List trades
      description: |
        Cursor-paginated trade history scoped to your partner account.
        Use `nextCursor` to page; `hasMore` indicates whether more pages
        exist.
      parameters:
        - $ref: '#/components/parameters/Cursor'
        - $ref: '#/components/parameters/Limit100'
        - name: side
          in: query
          required: false
          description: Filter by side.
          schema:
            $ref: '#/components/schemas/Side'
        - name: status
          in: query
          required: false
          description: Filter by transaction status.
          schema:
            $ref: '#/components/schemas/PoolTxnStatus'
        - name: created_after
          in: query
          required: false
          description: Only trades created at or after this timestamp (RFC 3339).
          schema:
            type: string
            format: date-time
        - name: created_before
          in: query
          required: false
          description: Only trades created at or before this timestamp (RFC 3339).
          schema:
            type: string
            format: date-time
      responses:
        '200':
          description: Trade page.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
          content:
            application/json:
              schema:
                type: object
                required: [data, nextCursor, hasMore]
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/TradeItem'
                  nextCursor:
                    type: [string, 'null']
                    description: Opaque cursor for the next page, or null on the last page.
                  hasMore:
                    type: boolean
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/ForbiddenSecretRead'
        '429':
          $ref: '#/components/responses/RateLimited'

  /pools/trades/{transactId}:
    get:
      tags: [Reconciliation]
      operationId: getTrade
      summary: Get a trade
      description: Returns a single trade by its transaction id.
      parameters:
        - name: transactId
          in: path
          required: true
          description: The transaction id.
          schema:
            type: string
            examples: ['tx_8b2d4a1f']
      responses:
        '200':
          description: Trade.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TradeItem'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/ForbiddenSecretRead'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'

  /pools/quotes/{quoteId}:
    get:
      tags: [Quotes]
      operationId: getQuote
      summary: Get a quote
      description: Returns the full detail and lifecycle status of a quote.
      parameters:
        - $ref: '#/components/parameters/QuoteIdPath'
      responses:
        '200':
          description: Quote detail.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/QuoteDetail'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/ForbiddenSecretRead'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'

  /pools/quotes/{quoteId}/reject:
    post:
      tags: [Quotes]
      operationId: rejectQuote
      summary: Reject a quote
      description: |
        Marks an active quote as `rejected` so it can no longer be executed.
        **Not idempotent** — rejecting an already-rejected (or otherwise
        non-active) quote returns `409`.
      parameters:
        - $ref: '#/components/parameters/QuoteIdPath'
      responses:
        '200':
          description: The quote, now rejected.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/QuoteDetail'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/ForbiddenSecretRead'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: The quote is not active and cannot be rejected.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'

  /pools/{id}/balance:
    get:
      tags: [Discovery]
      operationId: getPoolBalance
      summary: Get pool balance
      description: |
        Returns your available balance view for a pool, its tier and the
        set of pools you are allowed to access. `balance` is null when the
        balance gate is not enabled for your account. Entitled route.
      parameters:
        - $ref: '#/components/parameters/PoolId'
      responses:
        '200':
          description: Pool balance.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PoolBalance'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/ForbiddenEntitled'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'

  /pools/funding:
    get:
      tags: [Reconciliation]
      operationId: getFundingInstructions
      summary: Get funding instructions
      description: |
        Returns pay-in funding instructions for crediting your pool balance.
        EUR-only — `currency` defaults to `EUR`; any other currency returns
        `400`. Returns `404` when the balance gate is not enabled for your
        account.
      parameters:
        - name: currency
          in: query
          required: false
          description: Funding currency. Only `EUR` is supported.
          schema:
            type: string
            default: EUR
            enum: [EUR]
      responses:
        '200':
          description: Funding instructions.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FundingInstructions'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/ForbiddenSecretRead'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'

  /pools/ledger:
    get:
      tags: [Reconciliation]
      operationId: getLedger
      summary: Get funding ledger
      description: |
        Returns the cursor-paginated funding ledger for a currency, with the
        current available balance and signed entries. Returns `404` when the
        balance gate is not enabled for your account.
      parameters:
        - name: currency
          in: query
          required: false
          description: Ledger currency.
          schema:
            type: string
            default: EUR
        - $ref: '#/components/parameters/Cursor'
        - $ref: '#/components/parameters/Limit200'
      responses:
        '200':
          description: Ledger page.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LedgerResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/ForbiddenSecretRead'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'

components:
  securitySchemes:
    PublishableKey:
      type: http
      scheme: bearer
      description: |
        Publishable key (`pk_test_*` / `pk_live_*`). Browser-safe; scoped to
        pool discovery only (`GET /pools`).
    SecretKey:
      type: http
      scheme: bearer
      description: |
        Secret key (`sk_test_*` / `sk_live_*`). Server-side only — never
        expose in a browser. Required by every route except `GET /pools`.

  headers:
    X-Request-Id:
      description: Unique id for this response; quote it in support requests.
      schema:
        type: string

  parameters:
    PoolId:
      name: id
      in: path
      required: true
      description: Pair-shaped pool id, e.g. `EUR-USDT`.
      schema:
        type: string
        examples: ['EUR-USDT']
    QuoteIdPath:
      name: quoteId
      in: path
      required: true
      description: The quote id.
      schema:
        type: string
        examples: ['qt_5f3a9c2e']
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      description: |
        Caller-supplied idempotency token (send a UUID). Execution is
        idempotent on the underlying `quoteId`.
      schema:
        type: string
    Cursor:
      name: cursor
      in: query
      required: false
      description: Opaque pagination cursor from a previous `nextCursor`.
      schema:
        type: string
    Limit100:
      name: limit
      in: query
      required: false
      description: Page size.
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 50
    Limit200:
      name: limit
      in: query
      required: false
      description: Page size.
      schema:
        type: integer
        minimum: 1
        maximum: 200
        default: 50

  responses:
    BadRequest:
      description: Invalid request.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/X-Request-Id'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Unauthorized:
      description: Missing or invalid credential.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/X-Request-Id'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    NotFound:
      description: |
        Not found. Also returned when an object belongs to another partner
        (per-partner scoping), so ids are not enumerable.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/X-Request-Id'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    RateLimited:
      description: Too many requests.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/X-Request-Id'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    ForbiddenEntitled:
      description: |
        Access denied on an entitled route. The `code` is one of:
        `pools_not_enabled`, `pool_access_suspended`, `kyc_not_approved`,
        `pool_not_allowed`, `key_mode_mismatch`.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/X-Request-Id'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    ForbiddenSecretRead:
      description: |
        Access denied on a secret-key read. The only denial `code` surfaced
        here is `key_mode_mismatch`.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/X-Request-Id'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

  schemas:
    Side:
      type: string
      description: Order direction.
      enum: [on_ramp, off_ramp]

    CryptoNetwork:
      type: string
      description: Network the crypto leg is quoted on.
      enum: [tron, ethereum, bsc, polygon, solana]
      default: tron

    PoolTxnStatus:
      type: string
      description: |
        Transaction lifecycle status. `returned` is a terminal auto-refund
        state surfaced only via the status poll and the ledger (no webhook).
      enum: [quoted, reserved, settled, released, failed, returned]

    QuoteStatus:
      type: string
      description: Quote lifecycle status.
      enum: [active, consumed, expired, rejected]

    Money:
      type: string
      description: Decimal amount serialized as a base-10 string.
      examples: ['1000.00']

    PoolListItem:
      type: object
      required: [id, pair, fiatCurrency, cryptoCurrency, available]
      properties:
        id:
          type: string
          description: Pair-shaped pool id.
          examples: ['EUR-USDT']
        pair:
          type: string
          examples: ['EUR-USDT']
        fiatCurrency:
          type: string
          examples: ['EUR']
        cryptoCurrency:
          type: string
          examples: ['USDT']
        available:
          type: boolean
          description: Whether the pool can currently price and execute orders.

    PoolCapabilities:
      type: object
      required:
        - poolId
        - pair
        - fiatCurrency
        - cryptoCurrency
        - supportedNetworks
        - sides
        - tier
        - drawFeeBps
        - spreadCapBps
        - minOrderUsdt
        - maxOrderUsdt
        - cryptoDepositAddress
      properties:
        poolId:
          type: string
          examples: ['EUR-USDT']
        pair:
          type: string
          examples: ['EUR-USDT']
        fiatCurrency:
          type: string
          examples: ['EUR']
        cryptoCurrency:
          type: string
          examples: ['USDT']
        supportedNetworks:
          type: array
          description: Networks supported for the crypto leg.
          items:
            $ref: '#/components/schemas/CryptoNetwork'
        sides:
          type: array
          items:
            $ref: '#/components/schemas/Side'
        tier:
          type: string
          examples: ['standard']
        drawFeeBps:
          type: integer
          description: Draw fee in basis points.
        spreadCapBps:
          type: integer
          description: Maximum spread in basis points.
        minOrderUsdt:
          $ref: '#/components/schemas/Money'
        maxOrderUsdt:
          oneOf:
            - $ref: '#/components/schemas/Money'
            - type: 'null'
          description: Maximum order size, or null if uncapped.
        cryptoDepositAddress:
          type: [string, 'null']
          description: Deposit address for the crypto leg, when applicable.

    QuoteRequest:
      type: object
      required: [side, fiatCurrency, cryptoCurrency, amount]
      properties:
        side:
          $ref: '#/components/schemas/Side'
        fiatCurrency:
          type: string
          examples: ['EUR']
        cryptoCurrency:
          type: string
          examples: ['USDT']
        amount:
          $ref: '#/components/schemas/Money'
        cryptoNetwork:
          $ref: '#/components/schemas/CryptoNetwork'
        type:
          type: string
          description: |
            `firm` (default) returns an executable, time-boxed quote.
            `indicative` returns a non-executable price preview.
          enum: [firm, indicative]
          default: firm
        destAddress:
          type: string
          description: |
            Delivery address for the crypto leg. Required for `on_ramp`.
            For EVM delivery networks this is a `0x`-prefixed 40-hex address.
          examples: ['0x1234567890abcdef1234567890abcdef12345678']
        destNetwork:
          type: string
          description: |
            EVM delivery network. Defaults to the quoted `cryptoNetwork`.
          examples: ['ethereum']

    QuoteResponse:
      type: object
      description: |
        Quote result. When `available` is `false` (fail-soft) the call still
        returns HTTP `200`, there is no `quoteId`, and `unavailableReason`
        explains why.
      required: [available]
      properties:
        available:
          type: boolean
        type:
          type: string
          enum: [firm, indicative]
        executable:
          type: boolean
          description: True only for an `available`, `firm` quote.
        quoteId:
          type: string
          description: Present only when `available` is true.
          examples: ['qt_5f3a9c2e']
        rate:
          $ref: '#/components/schemas/Money'
        spreadBps:
          type: integer
          description: Spread in basis points (at most 50).
          maximum: 50
        feeBps:
          type: integer
        minOrderUsdt:
          $ref: '#/components/schemas/Money'
        maxOrderUsdt:
          oneOf:
            - $ref: '#/components/schemas/Money'
            - type: 'null'
        expiresAt:
          type: string
          format: date-time
          description: Quote expiry, roughly 15 seconds after issue.
        unavailableReason:
          type: string
          description: Present only when `available` is false.
          enum: [pool_dry, engine_unavailable, rate_unavailable]

    TransactResponse:
      type: object
      required: [transactId, status, quoteId, idempotent]
      properties:
        transactId:
          type: string
          examples: ['tx_8b2d4a1f']
        status:
          $ref: '#/components/schemas/PoolTxnStatus'
        quoteId:
          type: string
          examples: ['qt_5f3a9c2e']
        idempotent:
          type: boolean
          description: True when this response is a replay of a prior execution.

    TransactionStatusResponse:
      type: object
      required: [transactId, quoteId, status, poolId, side, createdAt, settledAt]
      properties:
        transactId:
          type: string
        quoteId:
          type: string
        status:
          $ref: '#/components/schemas/PoolTxnStatus'
        poolId:
          type: string
          examples: ['EUR-USDT']
        side:
          $ref: '#/components/schemas/Side'
        createdAt:
          type: string
          format: date-time
        settledAt:
          type: [string, 'null']
          format: date-time

    TradeItem:
      type: object
      required:
        - transactId
        - quoteId
        - poolId
        - pair
        - side
        - status
        - fiatCurrency
        - cryptoCurrency
        - cryptoNetwork
        - fiatAmount
        - cryptoAmount
        - quotedRate
        - spreadBps
        - feeBps
        - totalBps
        - engineFillTxId
        - createdAt
        - settledAt
      properties:
        transactId:
          type: string
          examples: ['tx_8b2d4a1f']
        quoteId:
          type: string
          examples: ['qt_5f3a9c2e']
        poolId:
          type: string
          examples: ['EUR-USDT']
        pair:
          type: string
          examples: ['EUR-USDT']
        side:
          $ref: '#/components/schemas/Side'
        status:
          $ref: '#/components/schemas/PoolTxnStatus'
        fiatCurrency:
          type: string
          examples: ['EUR']
        cryptoCurrency:
          type: string
          examples: ['USDT']
        cryptoNetwork:
          $ref: '#/components/schemas/CryptoNetwork'
        fiatAmount:
          $ref: '#/components/schemas/Money'
        cryptoAmount:
          $ref: '#/components/schemas/Money'
        quotedRate:
          $ref: '#/components/schemas/Money'
        spreadBps:
          type: integer
        feeBps:
          type: integer
        totalBps:
          type: integer
        engineFillTxId:
          type: [string, 'null']
          description: Internal fill reference, when available.
        createdAt:
          type: string
          format: date-time
        settledAt:
          type: [string, 'null']
          format: date-time

    QuoteDetail:
      type: object
      required:
        - quoteId
        - poolId
        - pair
        - side
        - cryptoNetwork
        - fiatAmount
        - cryptoAmount
        - rate
        - spreadBps
        - feeBps
        - status
        - expiresAt
        - consumedAt
        - rejectedAt
        - createdAt
      properties:
        quoteId:
          type: string
          examples: ['qt_5f3a9c2e']
        poolId:
          type: string
          examples: ['EUR-USDT']
        pair:
          type: string
          examples: ['EUR-USDT']
        side:
          $ref: '#/components/schemas/Side'
        cryptoNetwork:
          $ref: '#/components/schemas/CryptoNetwork'
        fiatAmount:
          $ref: '#/components/schemas/Money'
        cryptoAmount:
          $ref: '#/components/schemas/Money'
        rate:
          $ref: '#/components/schemas/Money'
        spreadBps:
          type: integer
        feeBps:
          type: integer
        status:
          $ref: '#/components/schemas/QuoteStatus'
        expiresAt:
          type: string
          format: date-time
        consumedAt:
          type: [string, 'null']
          format: date-time
        rejectedAt:
          type: [string, 'null']
          format: date-time
        createdAt:
          type: string
          format: date-time

    PoolBalance:
      type: object
      required: [poolId, pair, available, tier, allowedPools, balance]
      properties:
        poolId:
          type: string
          examples: ['EUR-USDT']
        pair:
          type: string
          examples: ['EUR-USDT']
        available:
          type: boolean
        tier:
          type: string
          examples: ['standard']
        allowedPools:
          type: array
          description: Pool ids you are allowed to access.
          items:
            type: string
          examples:
            - ['EUR-USDT', 'EUR-USDC']
        balance:
          oneOf:
            - $ref: '#/components/schemas/Money'
            - type: 'null'
          description: Available balance, or null when the balance gate is off.

    FundingInstructions:
      type: object
      required: [currency, iban, reference, instructions]
      properties:
        currency:
          type: string
          examples: ['EUR']
        iban:
          type: [string, 'null']
          description: Destination account identifier, or null when unavailable.
        reference:
          type: string
          description: Payment reference to include with the pay-in.
        instructions:
          type: string
          description: Human-readable funding instructions.

    LedgerEntry:
      type: object
      required: [delta, reason, ref, balanceAfter, createdAt]
      properties:
        delta:
          type: string
          description: Signed decimal change to the balance.
          examples: ['-250.00']
        reason:
          type: string
          enum: [payin_credit, buy_debit, buy_refund]
        ref:
          type: string
          description: Reference linking the entry to its source object.
        balanceAfter:
          $ref: '#/components/schemas/Money'
        createdAt:
          type: string
          format: date-time

    LedgerResponse:
      type: object
      required: [currency, available, entries, nextCursor]
      properties:
        currency:
          type: string
          examples: ['EUR']
        available:
          $ref: '#/components/schemas/Money'
        entries:
          type: array
          items:
            $ref: '#/components/schemas/LedgerEntry'
        nextCursor:
          type: [string, 'null']
          description: Opaque cursor for the next page, or null on the last page.

    Error:
      type: object
      description: Unified error envelope for every non-2xx response.
      required: [type, code, message, request_id, doc_url, statusCode]
      properties:
        type:
          type: string
          enum:
            - invalid_request
            - unauthorized
            - forbidden
            - not_found
            - conflict
            - rate_limited
            - server_error
        code:
          type: string
          description: |
            Machine-readable code. On entitled routes a `forbidden` error
            carries one of `pools_not_enabled`, `pool_access_suspended`,
            `kyc_not_approved`, `pool_not_allowed`, `key_mode_mismatch`;
            secret-only reads surface only `key_mode_mismatch`.
          examples: ['pool_not_allowed']
        message:
          type: string
        request_id:
          type: string
          description: Matches the `X-Request-Id` response header.
        doc_url:
          type: [string, 'null']
          description: Link to relevant documentation, when available.
        statusCode:
          type: integer
          description: HTTP status code, echoed for convenience.
          examples: [403]
