openapi: 3.0.3
info:
  title: Unigox API Gateway
  description: |
    Build fiat on/off-ramp flows in your app with Unigox partner APIs.

    This documentation contains the generated API reference only.
    For onboarding guides, tutorials, tools, and support pages, use the guide pages in the main sidebar.

    ## Welcome

    Unigox is a global P2P fiat on/off-ramp gateway for B2B partners. We focus
    on local rails coverage, fast execution, and simple partner integration.

    Key partner value:
    - coverage across Africa, LatAm, Asia, Middle East, Europe, and Australia
    - support for local rails and institutions relevant in emerging markets
    - compliance flows for KYC, verification, and webhook integrity

    ## API Reference

    Endpoint groups are ordered for partner integration flow:
    `Health` → `Supported Resources` → `Exchange Pairs` → `User Management` → `On-Ramp` → `Off-Ramp` → `Orders` → `Webhooks`.
  version: 1.0.0
  contact:
    name: Unigox API Support
    email: support@unigox.com

externalDocs:
  description: Unigox Python SDK
  url: https://github.com/Unigox/sdk_python/blob/main/partner_authorize_crypto_transfer_sdk.py

servers:
  - url: https://api-staging.unigox.com
    description: Sandbox server
  - url: https://api.unigox.com
    description: Production server

tags:
  - name: Health
    description: Service health and status endpoints
    x-page-icon: hand
  - name: Supported Resources
    description: Discover payment supported resources including cryptocurrencies, blockchains, fiat currencies, and rails
    x-page-icon: list-check
  - name: Exchange Pairs
    description: Get information about supported trading pairs
    x-page-icon: shuffle
  - name: User Management
    description: Endpoints to create and manage user accounts
    x-page-icon: user
  - name: On-Ramp
    description: On-ramp operations (fiat to crypto) — partner initiates a buy order, vendor provides liquidity, end-user sends fiat
    x-page-icon: arrow-down-to-bracket
  - name: Liquidity
    description: Real-time aggregated liquidity data — available trading pairs, amount ranges, and payment methods
    x-page-icon: chart-bar
  - name: Off-Ramp
    description: Off-ramp operations (crypto to fiat) — sell crypto and receive fiat
    x-page-icon: arrow-right-arrow-left
  - name: Orders
    description: Order management, status tracking, and lifecycle actions
    x-page-icon: receipt
  - name: Webhooks
    description: |
      Real-time notifications for order and KYC status changes.
      The system sends a signed HTTP POST to your configured webhook URL on every relevant event.

      **Event model**

      Two event types are delivered:

      | Event type | Fired when |
      |---|---|
      | `order.status.changed` | An order transitions to a new status |
      | `user.kyc.updated` | A user's KYC verification is approved or declined |

      All events share the same envelope (`event_id`, `event_type`, `created_at`, `data`)
      and the same signature scheme. The structure of the `data` object differs per event type.

      **Payload shape — `order.status.changed`**

      ```json
      {
        "event_id": "evt_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
        "event_type": "order.status.changed",
        "created_at": "2026-02-01T12:00:05Z",
        "data": {
          "order_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
          "status": "crypto_received",
          "user_uuid": "550e8400-e29b-41d4-a716-446655440000",
          "crypto_amount": "101.500000",
          "crypto_currency": "USDT",
          "fiat_amount": "152880.00",
          "fiat_currency": "NGN",
          "provider": "p2p",
          "payment_details_id": "987"
        }
      }
      ```

      **Payload shape — `user.kyc.updated`**

      Fired when a user's KYC status transitions to `verified` or `rejected`.
      Use this event to unlock order creation for the user on your side.

      ```json
      {
        "event_id": "evt_2d8418dd-e3a8-4463-a11e-6b54f354ca90",
        "event_type": "user.kyc.updated",
        "created_at": "2026-03-10T11:37:42Z",
        "data": {
          "user_uuid": "550e8400-e29b-41d4-a716-446655440000",
          "kyc_status": "VERIFIED"
        }
      }
      ```

      When KYC is declined, `rejection_reasons` is included:

      ```json
      {
        "event_id": "evt_fc75484d-b374-4d05-b54e-820f3dd80e6d",
        "event_type": "user.kyc.updated",
        "created_at": "2026-03-10T12:00:00Z",
        "data": {
          "user_uuid": "550e8400-e29b-41d4-a716-446655440000",
          "kyc_status": "REJECTED",
          "rejection_reasons": ["Document expired", "Photo quality low"]
        }
      }
      ```

      `kyc_status` values: `VERIFIED` | `REJECTED`

      **Signature verification**

      Every delivery includes two headers for HMAC-SHA256 verification:
      - `X-Unigox-Signature: sha256=<hex>` — HMAC of `"<timestamp>.<raw_body>"`
      - `X-Unigox-Timestamp: <unix_seconds>`

      Verification pseudocode:
      ```
      expected = HMAC-SHA256(webhook_secret, "<timestamp>.<raw_body>")
      valid    = constant_time_compare(expected, signature_from_header)
      ```

      **Delivery guarantees**

      - At-least-once delivery. Your endpoint may receive the same event more than once.
      - De-duplicate by `event_id`.
      - Respond with any 2xx status within 10 seconds to acknowledge receipt.

      **Retry policy**

      Failed deliveries (non-2xx or timeout) are retried with exponential backoff:
      1 min → 5 min → 15 min → 1 h → 6 h → 12 h → 24 h (max 10 attempts over ~3.5 days).

      **Partner-facing statuses** (`order.status.changed`)

      | Status | Description |
      |---|---|
      | `created` | Order created |
      | `awaiting_liquidity_provider` | Waiting for vendor match |
      | `awaiting_crypto_transfer_authorization` | Partner must authorize crypto transfer |
      | `crypto_received` | Crypto received in escrow |
      | `fiat_payment_started` | Buyer submitted fiat payment proof |
      | `fiat_payment_review_started` | Payment proof under admin review |
      | `awaiting_fiat_received_confirmation` | Proof approved, awaiting seller confirmation |
      | `completed` | Order completed, crypto released |
      | `cancelled` | Order cancelled or payment declined |
      | `failed` | Escrow error, or fiat-anchored safety bound exceeded (see below) |
      | `dispute_started` | Dispute opened |

      **Fiat-anchored safety bound failure**

      For fiat-anchored off-ramp orders (`anchor_type: "fiat"`), if the crypto cost we quote exceeds
      1.20× the expected amount at the time of settlement, the order moves to `failed` status asynchronously
      (not during `/initiate`). The deposit is automatically refunded from escrow back to the partner wallet.
      Create a new quote to retry the trade flow.

      **Settlement notifications (recipient received the fiat)**

      Some liquidity providers automatically confirm when the fiat payout reaches the
      recipient's bank account or mobile wallet. When the provider matched to an order
      supports this, the order moves to `completed` on its own and you receive an
      `order.status.changed` webhook with `status=completed` — that webhook is your signal
      that the recipient has actually received the money.

      Whether the matched provider sends this is reported per order by the
      `has_fiat_settlement_notification` field on the **quote** and **order** responses:

      - `true` — the matched provider sends an automatic settlement confirmation. You will
        receive the `completed` webhook once fiat is delivered; no manual action is needed.
      - `false` — no automatic confirmation for this order. Confirm delivery yourself by
        calling `confirm-fiat-received` (or rely on your own banking/payment systems).

      To match **only** providers that send settlement notifications, pass
      `has_fiat_settlement_notification: true` in the quote request — providers that can't
      offer it are then excluded from matching.

      Availability is provider- and corridor-dependent: not every currency has a provider
      that sends settlement notifications, so always read the flag on each quote rather than
      assuming it. Manual `confirm-fiat-received` and the automatic confirmation are mutually
      idempotent — whichever happens first wins and the other is a safe no-op.
    x-page-icon: bell

paths:
  # ============================================
  # 1. HEALTH CHECK
  # ============================================
  /health:
    get:
      tags:
        - Health
      summary: Health check
      description: Returns the health status of the API gateway
      operationId: healthCheck
      responses:
        '200':
          description: Service is healthy
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: ok
                  service:
                    type: string
                    example: api
                  time:
                    type: string
                    format: date-time
                    example: "2024-01-01T12:00:00Z"

  # ============================================
  # 2. DISCOVERY - CAPABILITIES
  # ============================================
  # Step 1: Discover supported cryptocurrencies
  /api/v1/supported/crypto-currencies:
    get:
      tags:
        - Supported Resources
      summary: Cryptocurrencies
      description: Get a list of all supported cryptocurrencies with their blockchains and contract addresses. Useful for discovering available crypto assets for trading.
      operationId: getCryptoCurrencies
      responses:
        '200':
          description: Successfully retrieved cryptocurrencies
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CryptoCurrenciesResponse'

  # Step 2: Discover supported blockchains
  /api/v1/supported/blockchains:
    get:
      tags:
        - Supported Resources
      summary: Blockchains
      description: Get a list of all supported blockchains with their associated cryptocurrencies. Useful for discovering which blockchains are available and what tokens they support.
      operationId: getBlockchains
      responses:
        '200':
          description: Successfully retrieved blockchains
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BlockchainsResponse'

  # Step 3: Discover supported fiat currencies
  /api/v1/supported/fiat-currencies:
    get:
      tags:
        - Supported Resources
      summary: Fiat currencies
      description: Get a list of all supported fiat currencies with their onramp/offramp capabilities. Useful for discovering available fiat currencies for onramp and offramp operations.
      operationId: getFiatCurrencies
      responses:
        '200':
          description: Successfully retrieved fiat currencies
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FiatCurrenciesResponse'
        '500':
          description: Internal Server Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  # Step 4: Discover payment rails for a country/currency
  /api/v1/supported/payment-rails:
    get:
      tags:
        - Supported Resources
      summary: Payment rails
      description: |
        Get payment rail capabilities for a specific country, currency, and direction (onramp/offramp). Returns available payment rails with their required fields and supported currencies.

        Partner rule:
        - if `institution_required` is `true`, send `institution_id` chosen from `/api/v1/supported/institutions`
        - if `institution_required` is `false`, institution selection is not needed for the rail itself

        Current partner API omission support is explicitly available for:
        - `iban-sepa`
        - `nip-nigeria`
        - `imps-neft-india`

        For `iban-sepa` and `nip-nigeria`, partners may omit `institution_id` and the backend resolves the generic `other-bank` payment method automatically.

        `imps-neft-india` returns `institution_required: false` and has **no** `institution_id` field: the IFSC code encodes both bank and branch and routes the payout on its own. Required fields are `bank_name`, `ifsc_code`, `account_number`, and `full_name`. Sending `institution_id` is accepted but ignored (it resolves to the generic `imps-or-neft-transfer` method); the receiving bank comes from `bank_name` + `ifsc_code`.

        **Multi-format rails:** some rails accept more than one field-set depending on the
        chosen institution's type, and return a `formats` array. For example GHIPSS (Ghana)
        and EthSwitch (Ethiopia) accept a **mobile-money** format keyed on `phone_number`
        and a **bank** format keyed on `account_number`. For these, the top-level `fields`
        is only a backward-compatible best-effort single set; use `formats` instead:
        1. pick an institution from `/api/v1/supported/institutions` (each carries a fine-grained `institution_type`),
        2. find the format whose `institution_types` contains that `institution_type`,
        3. submit that format's `fields`.
        Each format also carries `has_liquidity`, indicating whether a vendor/provider can
        currently settle it in the corridor (omitted when undetermined).

        Example multi-format rail (GHIPSS):
        ```json
        {
          "slug": "ghipss",
          "name": "GHIPSS",
          "institution_required": false,
          "fields": [ { "key": "account_number", "required": true }, { "key": "full_name", "required": false } ],
          "formats": [
            {
              "id": "mobile-money",
              "name": "Mobile Money",
              "institution_types": ["mobile-money", "mobile-wallets"],
              "has_liquidity": true,
              "fields": [ { "key": "phone_number", "required": true }, { "key": "full_name", "required": false } ]
            },
            {
              "id": "banks",
              "name": "Bank Account",
              "institution_types": ["traditional-banks", "digital-banks"],
              "has_liquidity": false,
              "fields": [ { "key": "account_number", "required": true }, { "key": "full_name", "required": false } ]
            }
          ]
        }
        ```
        Here a partner paying out to an M-Pesa/TeleBirr-style mobile wallet must use the
        `mobile-money` format (`phone_number`), not `account_number`.
      operationId: getPaymentRails
      parameters:
        - name: country
          in: query
          description: ISO 2-letter country code (e.g., NG, US, IN)
          schema:
            type: string
            example: NG
        - name: currency
          in: query
          description: Fiat currency code (e.g., NGN, USD, EUR). Either country or currency must be provided.
          schema:
            type: string
            example: NGN
        - name: direction
          in: query
          required: true
          description: Transaction direction - either "onramp" (fiat to crypto) or "offramp" (crypto to fiat)
          schema:
            type: string
            enum: [onramp, offramp]
            example: offramp
      responses:
        '200':
          description: Successfully retrieved payment rails
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaymentRailsResponse'
              example:
                success: true
                data:
                  country: NG
                  currency: NGN
                  direction: offramp
                  rails:
                    - slug: nip-nigeria
                      name: NIP Nigeria (NIBSS Instant Payments)
                      institution_required: false
                      institution_lookup_key: null
                      currencies:
                        - NGN
                      settlement:
                        speed: INSTANT
                        typical_minutes: 2
                        note: null
                      limits: null
                      # Backward-compatible single field-set. For a multi-format rail it is a
                      # best-effort guess; use `formats` for the authoritative per-institution set.
                      fields:
                        - key: account_number
                          label: Account Number
                          type: text
                          required: true
                          pattern: "^\\d{10}$"
                          placeholder: "10-digit account number"
                        - key: full_name
                          label: Full Name
                          type: text
                          required: false
                          placeholder: "Enter full name"
                      # Present only for multi-format rails: the field set depends on the chosen
                      # institution's type. Match an institution's `institution_type` to a format's
                      # `institution_types`, then submit that format's `fields`.
                      formats:
                        - id: banks
                          name: Bank Account
                          institution_types: ["traditional-banks", "digital-banks"]
                          has_liquidity: true
                          fields:
                            - key: account_number
                              label: Account Number
                              type: text
                              required: true
                            - key: full_name
                              label: Full Name
                              type: text
                              required: false
                        - id: mobile-money
                          name: Mobile Money
                          institution_types: ["mobile-money", "mobile-wallets"]
                          has_liquidity: false
                          fields:
                            - key: phone_number
                              label: Phone Number
                              type: tel
                              required: true
                              placeholder: "Enter phone number"
                            - key: full_name
                              label: Full Name
                              type: text
                              required: false
                  links:
                    search_institutions: /api/v1/supported/institutions
        '400':
          description: Bad request - missing required parameters or invalid values
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Internal Server Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  # Step 5: Discover institutions (banks) for a payment rail (used when institution_required: true)
  /api/v1/supported/institutions:
    get:
      tags:
        - Supported Resources
      summary: Institutions (banks) for a payment rail
      description: >-
        Get a list of financial institutions (banks) available for a specific
        payment rail. Useful for populating bank-selection dropdowns. Call with
        just `rail` to page through every bank, or pass `search`/`code` to
        filter. For `imps-neft-india` this is the embedded India bank directory
        (~1,510 banks); selecting one is optional, since the bank is identified
        at payment-detail creation by `bank_name` + `ifsc_code`.
      operationId: getInstitutions
      parameters:
        - name: rail
          in: query
          required: true
          description: Payment rail slug (e.g., iban-sepa, pix-brazil, cvu-cbu)
          schema:
            type: string
            example: iban-sepa
        - name: institution_id
          in: query
          description: Optional institution ID to filter by specific institution
          schema:
            type: string
            example: revolut
        - name: country
          in: query
          description: ISO 2-letter country code for filtering
          schema:
            type: string
            example: IN
        - name: currency
          in: query
          description: ISO 3-letter currency code for filtering
          schema:
            type: string
            example: INR
        - name: search
          in: query
          description: Free-text search by institution name
          schema:
            type: string
            example: hdfc
        - name: code
          in: query
          description: >-
            Filter by the institution's bank code (the `code` field of an
            institution): the 4-letter IFSC bank prefix for `imps-neft-india`
            (e.g. `HDFC`), the NIBSS code for `nip-nigeria`, etc. Not a full
            IFSC — use the 4-letter prefix.
          schema:
            type: string
            example: HDFC
        - name: limit
          in: query
          description: Maximum number of results to return
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
            example: 20
        - name: offset
          in: query
          description: Number of results to skip for pagination
          schema:
            type: integer
            minimum: 0
            default: 0
            example: 0
      responses:
        '200':
          description: Successfully retrieved institutions
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InstitutionsResponse'
              example:
                success: true
                data:
                  institutions:
                    - id: hdfc-bank
                      name: HDFC Bank
                      type: BANK
                      code: HDFC
                      branch: null
                      supported_rails:
                        - imps-neft-india
                  pagination:
                    total: 1
                    limit: 20
                    offset: 0
        '400':
          description: Bad request - invalid rail or other validation errors
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Internal Server Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  # Fiat corridors snapshot - all-in rates per direction
  /api/v1/supported/corridors:
    get:
      tags:
        - Supported Resources
      summary: Fiat Corridors
      description: |
        Get a snapshot of supported fiat corridors with the all-in best rates Unigox would quote at a 500 USDT reference notional, split by direction (`onramp` / `offramp`) and partner type (`licensed` / `p2p`).

        Every `rate` is post-fee. `unigox_fee_pct` discloses the platform fee for the corridor; `mid_market_rate` is the Open Exchange Rates reference for the same currency.
      operationId: getSupportedCorridors
      responses:
        '200':
          description: Snapshot retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SupportedCorridorsResponse'
              example:
                success: true
                data:
                  notional_usdt: 500
                  generated_at: '2026-04-27T15:56:11Z'
                  currencies:
                    - code: NGN
                      name: Nigerian Naira
                      mid_market_rate: 1358.97
                      mid_market_updated_at: '2026-04-27T15:56:11Z'
                      unigox_fee_pct: 0.25
                      offramp:
                        supported: true
                        p2p:
                          rate: 1542.6
                        licensed:
                          rate: 1376.857
                      onramp:
                        supported: true
                        p2p:
                          rate: 1370
                        licensed:
                          rate: 1389.35
                    - code: BRL
                      name: Brazilian Real
                      mid_market_rate: 4.9911
                      mid_market_updated_at: '2026-04-27T15:56:11Z'
                      unigox_fee_pct: 0.5
                      offramp:
                        supported: true
                        p2p:
                          rate: 4.4648
                          min_notional_fiat: 3465
        '500':
          description: Internal Server Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  # ============================================
  # 3. EXCHANGE PAIRS
  # ============================================
  /api/v1/supported-exchange-pairs:
    get:
      tags:
        - Exchange Pairs
      summary: Supported exchange pairs
      description: Get a list of all supported cryptocurrency and fiat currency exchange pairs. Shows which currency combinations can be traded.
      operationId: getSupportedPairs
      responses:
        '200':
          description: Successfully retrieved supported pairs
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SupportedPairsResponse'
              example:
                success: true
                data:
                  - crypto_currency_code: USDC
                    fiat_currency_code: ARS
                    type: BUY
                  - crypto_currency_code: USDC
                    fiat_currency_code: ARS
                    type: SELL
                  - crypto_currency_code: USDC
                    fiat_currency_code: AUD
                    type: BUY
                  - crypto_currency_code: USDC
                    fiat_currency_code: AUD
                    type: SELL
                  - crypto_currency_code: USDC
                    fiat_currency_code: CAD
                    type: BUY
                  - crypto_currency_code: USDC
                    fiat_currency_code: CAD
                    type: SELL
                  - crypto_currency_code: USDC
                    fiat_currency_code: EUR
                    type: BUY
                  - crypto_currency_code: USDC
                    fiat_currency_code: EUR
                    type: SELL
                  - crypto_currency_code: USDC
                    fiat_currency_code: GBP
                    type: BUY
                  - crypto_currency_code: USDC
                    fiat_currency_code: GBP
                    type: SELL
                  - crypto_currency_code: USDC
                    fiat_currency_code: GEL
                    type: BUY
                  - crypto_currency_code: USDC
                    fiat_currency_code: GEL
                    type: SELL
                  - crypto_currency_code: USDC
                    fiat_currency_code: GHS
                    type: BUY

  # ============================================
  # 5. ACCOUNT MANAGEMENT
  # ============================================
  # 5. PARTNER AUTHENTICATION
  # ============================================
  /api/v1/partner/verify-auth:
    get:
      tags:
        - User Management
      summary: Verify API key authentication
      description: |
        Verifies that the provided API key is valid and authentication is working correctly.
      operationId: verifyAuth
      security:
        - ApiKeyAuth: []
      responses:
        '200':
          description: Authentication successful
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/APIResponse'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          message:
                            type: string
                            example: "Authentication successful"
                          authenticated:
                            type: boolean
                            example: true
              example:
                success: true
                data:
                  message: "Authentication successful"
                  authenticated: true
        '401':
          description: Unauthorized - invalid or missing API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                data:
                  error_key: "unauthorized"

  # ============================================
  # 6. PARTNER USER MANAGEMENT
  # ============================================
  # Step 1: Create or get partner user
  /api/v1/partner/users:
    post:
      tags:
        - User Management
      summary: Create or get partner user
      description: Creates a new user relation or returns existing if already created. Returns user_ref (same as input) for subsequent API calls. Partners can only access their own users.
      operationId: createPartnerUser
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [user_ref, email]
              properties:
                user_ref:
                  type: string
                  description: Partner's unique user ID (usually primary key or UUID)
                  example: "partner_unique_id_123"
                email:
                  type: string
                  format: email
                  description: User's email address
                  example: user@example.com
      responses:
        '201':
          description: Successfully created user
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateUserResponse'
        '200':
          description: User already exists
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateUserResponse'
        '400':
          description: Bad Request
        '401':
          description: Unauthorized (missing or invalid X-API-Key)
        '500':
          description: Internal Server Error

  # Step 2: Get partner user by id
  /api/v1/partner/users/{user_uuid}:
    get:
      tags:
        - User Management
      summary: Partner user by ID
      description: Get user details including KYC status and payment profiles using the user_uuid returned during user creation. Partners can only access their own users.
      operationId: getPartnerUser
      security:
        - ApiKeyAuth: []
      parameters:
        - name: user_uuid
          in: path
          required: true
          description: The user_uuid returned by POST /partner/users. Partners can only access their own users.
          schema:
            type: string
            format: uuid
          example: "550e8400-e29b-41d4-a716-446655440000"
      responses:
        '200':
          description: User found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GetUserResponse'
        '401':
          description: Unauthorized
        '404':
          description: User not found

  # ============================================
  # 7. PARTNER KYC VERIFICATION FLOW
  # ============================================
  # Step 3: Submit KYC data for a partner user
  /api/v1/partner/users/{user_uuid}/kyc-submissions:
    post:
      tags:
        - User Management
      summary: Submit KYC data for a partner user
      description: |
        Submit KYC (Know Your Customer) data for a partner user. Supports three methods:
        - direct_data: Partner submits PII data directly, user is verified
        - external_token: Partner provides external KYC provider credentials (SumSub token or Persona inquiry_id)
        - handoff: Unigox handles KYC verification and generates verification URL

        **Important for EUR/AUD/GBP off-ramp:** EUR, AUD, and GBP payouts require the user's
        physical address. Include `address`, `city`, and `postal_code` in the PII data
        when submitting KYC. If not provided at submission time, you can add them later with
        `PATCH /partner/users/{user_uuid}/kyc`. Without address, EUR/AUD/GBP orders will be created
        with status `pending_address` until the address is provided.
      operationId: submitKYC
      security:
        - ApiKeyAuth: []
      parameters:
        - name: user_uuid
          in: path
          required: true
          description: The user_uuid returned by POST /partner/users. Partners can only access their own users.
          schema:
            type: string
            format: uuid
          example: "550e8400-e29b-41d4-a716-446655440000"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - $ref: '#/components/schemas/KYCSubmitDirectData'
                - $ref: '#/components/schemas/KYCSubmitExternalToken'
                - $ref: '#/components/schemas/KYCSubmitHandoff'
            examples:
              direct_data:
                summary: Direct data submission
                value:
                  method: direct_data
                  data:
                    pii:
                      first_name: John
                      last_name: Doe
                      dob: "1990-01-01"
                      country_code: NG
                      address: "123 Street Name, Lagos"
                      postal_code: "100001"
              external_token_with_sumsub:
                summary: External token (SumSub)
                value:
                  method: external_token
                  data:
                    provider: sumsub
                    token: "sumsub_external_session_id"
              external_token_with_persona:
                summary: External token (Persona)
                value:
                  method: external_token
                  data:
                    provider: persona
                    inquiry_id: "inq_CWQzc5afzD4dmF66EJqjdPC2T79C"
              handoff:
                summary: Handoff to Unigox
                value:
                  method: handoff
                  data:
                    pii:
                      first_name: John
                      last_name: Doe
                      country_code: NG
      responses:
        '200':
          description: KYC submitted successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KYCSubmitResponse'
        '400':
          description: >-
            Bad Request — invalid method, missing required data, or a date of birth that
            fails validation. The response body is `{ "success": false, "data": { "error_key": "...", "message": "..." } }`.
            Possible `error_key` values include:
            `underage_not_allowed` — "User must be at least 18 years old. The provided
            date_of_birth indicates an age under 18."; and
            `invalid_date_of_birth` — "date_of_birth is invalid. Provide a real date in
            YYYY-MM-DD format that is in the past."
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                underage:
                  summary: Date of birth under 18
                  value:
                    success: false
                    data:
                      error_key: underage_not_allowed
                      message: "User must be at least 18 years old. The provided date_of_birth indicates an age under 18."
                invalid_dob:
                  summary: Malformed or future date of birth
                  value:
                    success: false
                    data:
                      error_key: invalid_date_of_birth
                      message: "date_of_birth is invalid. Provide a real date in YYYY-MM-DD format that is in the past."
        '401':
          description: Unauthorized
        '404':
          description: User not found
        '500':
          description: Internal Server Error

  # Step 4: Upload KYC document for a partner user (for direct_data method)
  /api/v1/partner/users/{user_uuid}/kyc/documents:
    post:
      tags:
        - User Management
      summary: Upload KYC document for a partner user
      description: |
        Upload a KYC document for a partner user. Choose one of two upload methods:

        ---

        ### Method 1: File Upload
        Send the document as a file attachment.
        ```
        POST /api/v1/partner/users/{user_uuid}/kyc/documents
        Content-Type: multipart/form-data

        document: <file>
        document_type: identity_front
        ```

        ### Method 2: Single URL Upload
        Send a URL pointing to one document image.
        ```json
        {
          "upload_method": "url_link",
          "document_type": "identity_front",
          "url": "https://example.com/id-front.jpg"
        }
        ```

        ### Method 3: Batch URL Upload
        Upload multiple documents in one call (up to 10).
        ```json
        {
          "upload_method": "url_link",
          "documents": [
            { "document_type": "identity_front", "url": "https://example.com/id-front.jpg" },
            { "document_type": "selfie", "url": "https://example.com/selfie.jpg" }
          ]
        }
        ```

        **URL requirements:** HTTPS only, 10MB per file, JPEG/PNG/GIF/PDF.

        ---

        **Supported document types:** `identity_front` (required), `selfie` (required), `identity_back`, `passport`, `national_id`, `driver_license`, and more.

        **Automatic Verification:** After uploading all required documents (selfie + identity_front), verification starts automatically.
      operationId: uploadPartnerKYCDocument
      security:
        - ApiKeyAuth: []
      parameters:
        - name: user_uuid
          in: path
          required: true
          description: Public UUID of the user
          schema:
            type: string
            format: uuid
          example: "550e8400-e29b-41d4-a716-446655440000"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - title: File Upload (multipart)
                  description: Upload document as a file attachment. Use Content-Type multipart/form-data.
                  type: object
                  required: [document, document_type]
                  properties:
                    document:
                      type: string
                      format: binary
                      description: Document file (JPEG, PNG, GIF, or PDF, max 10MB)
                    document_type:
                      type: string
                      description: Type of document being uploaded
                      enum: [identity_front, identity_back, selfie, passport, national_id, driver_license, voter_id, resident_card, id_card]
                      example: identity_front
                - title: Single URL Upload
                  description: Upload one document by providing an HTTPS URL.
                  type: object
                  required: [upload_method, document_type, url]
                  properties:
                    upload_method:
                      type: string
                      enum: [url_link]
                      example: url_link
                    document_type:
                      type: string
                      enum: [identity_front, identity_back, selfie, passport, national_id, driver_license, voter_id, resident_card, id_card]
                      example: identity_front
                    url:
                      type: string
                      format: uri
                      description: HTTPS URL of the image. 10MB max.
                      example: "https://example.com/id-front.jpg"
                - title: Batch URL Upload
                  description: Upload multiple documents in one call (up to 10).
                  type: object
                  required: [upload_method, documents]
                  properties:
                    upload_method:
                      type: string
                      enum: [url_link]
                      example: url_link
                    documents:
                      type: array
                      maxItems: 10
                      items:
                        type: object
                        required: [document_type, url]
                        properties:
                          document_type:
                            type: string
                            enum: [identity_front, identity_back, selfie, passport, national_id, driver_license]
                            example: identity_front
                          url:
                            type: string
                            format: uri
                            example: "https://example.com/id-front.jpg"
              discriminator:
                propertyName: upload_method
            examples:
              single_url:
                summary: Single document via URL
                value:
                  upload_method: url_link
                  document_type: identity_front
                  url: "https://example.com/id-front.jpg"
              batch_url:
                summary: Batch upload (multiple documents)
                value:
                  upload_method: url_link
                  documents:
                    - document_type: identity_front
                      url: "https://example.com/id-front.jpg"
                    - document_type: selfie
                  url: "https://example.com/selfie.jpg"
      responses:
        '200':
          description: Document uploaded successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  data:
                    type: object
                    properties:
                      document_id:
                        type: string
                        description: AiPrise document UUID
                        example: "f1d5f922-bee2-4c7e-88ef-18022958c4c2"
                      document_type:
                        type: string
                        example: "identity_front"
                      file_name:
                        type: string
                        example: "passport_photo.jpg"
                      message:
                        type: string
                        description: |
                          Success message. If all required documents are uploaded, message indicates
                          that verification started automatically.
                        example: "Document uploaded successfully"
                      verification_session_id:
                        type: string
                        description: |
                          Verification session ID (only present if verification started automatically
                          after uploading the last required document: selfie + identity_front)
                        example: "4fd410c9-ba9b-46cf-a4f0-132204a9a564"
                      verification_status:
                        type: string
                        enum: [PENDING, IN_PROGRESS, COMPLETED, FAILED]
                        description: |
                          Verification status (only present if verification started automatically
                          after uploading the last required document: selfie + identity_front)
                        example: "IN_PROGRESS"
        '400':
          description: Bad Request - invalid file type, missing document, or user not ready for uploads
        '401':
          description: Unauthorized
        '404':
          description: User not found or no active KYC session
        '500':
          description: Internal Server Error

  # Step 4b: Update optional KYC fields for a partner user
  /api/v1/partner/users/{user_uuid}/kyc:
    patch:
      tags:
        - User Management
      summary: Update KYC data for a partner user
      description: |
        Update optional PII fields on existing KYC data. The user stays verified.
        Only optional fields can be updated.
        Required fields (first_name, last_name, country_code) cannot be changed.

        For EUR/AUD/GBP offramp, provide address, city, and postal_code here.
        These will auto-populate into payment details for EUR/AUD/GBP payouts.
      operationId: updateKYC
      security:
        - ApiKeyAuth: []
      parameters:
        - name: user_uuid
          in: path
          required: true
          description: The user_uuid returned by POST /partner/users.
          schema:
            type: string
            format: uuid
          example: "550e8400-e29b-41d4-a716-446655440000"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                address:
                  type: string
                  description: Street address
                  example: "Svitrigailos g 29"
                city:
                  type: string
                  description: City
                  example: "Vilnius"
                postal_code:
                  type: string
                  example: "03229"
                dob:
                  type: string
                  format: date
                  description: >-
                    Date of birth (YYYY-MM-DD). Must be in the past and indicate an age of at
                    least 18; a future, malformed, or under-18 date is rejected with HTTP 400.
                  example: "1990-01-01"
                phone_number:
                  type: string
                  description: E.164 format
                  example: "+37012345678"
                middle_name:
                  type: string
                id_number:
                  type: string
                id_type:
                  type: string
                  enum: [NATIONAL_ID, DRIVER_LICENSE, PASSPORT]
      responses:
        '200':
          description: KYC data updated successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  data:
                    type: object
                    properties:
                      updated_fields:
                        type: array
                        items:
                          type: string
                        example: ["address", "city", "postal_code"]
                      message:
                        type: string
                        example: "KYC data updated successfully"
        '400':
          description: >-
            Bad request — no valid fields, attempting to change a required field, or a date
            of birth that fails validation. Body: `{ "success": false, "data": { "error_key": "...", "message": "..." } }`.
            DOB-related `error_key` values: `underage_not_allowed` (age under 18) and
            `invalid_date_of_birth` (malformed or future date).
        '401':
          description: Unauthorized
        '404':
          description: User not found

  # Step 5: Get verification status for a partner user (check KYC status)
  /api/v1/partner/users/{user_uuid}/verification-status:
    get:
      tags:
        - User Management
      summary: Verification status for a partner user
      description: |
        Get the current verification status for a partner user. This endpoint checks the verification status
        with the KYC provider (e.g., AiPrise) and returns the current status, verification URL (if available),
        and other relevant information. Optionally, you can provide a verification_id to check a specific
        verification session.

        Interpretation of terminal states:
        - `VERIFIED` — verification passed and the user is approved
        - `VERIFICATION_REJECTED` — verification completed with a rejection outcome
        - `FAILED` — verification could not be completed due to an error
        - `COMPLETED` — provider workflow finished processing, but partners should still treat `VERIFIED`
          as the success state and `VERIFICATION_REJECTED` / `FAILED` as terminal non-success states
      operationId: getVerificationStatus
      security:
        - ApiKeyAuth: []
      parameters:
        - name: user_uuid
          in: path
          required: true
          description: The user_uuid returned by POST /partner/users. Partners can only access their own users.
          schema:
            type: string
            format: uuid
          example: "550e8400-e29b-41d4-a716-446655440000"
        - name: verification_id
          in: query
          required: false
          description: Optional verification session ID. If not provided, the latest verification session for the user will be checked.
          schema:
            type: string
          example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
      responses:
        '200':
          description: Successfully retrieved verification status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VerificationStatusResponse'
              example:
                success: true
                data:
                  status: "IN_PROGRESS"
                  verification_url: "https://verify-sandbox.aiprise.com/?verification_session_id=abc123"
                  verification_seconds_left: 3600
        '401':
          description: Unauthorized
        '404':
          description: User not found
        '500':
          description: Internal Server Error

  # ============================================
  # 6. PARTNER PAYMENT DETAILS MANAGEMENT
  # ============================================
  # Step 6: Create payment details for a partner user (for offramp operations)
  /api/v1/partner/users/{user_uuid}/payment-details:
    post:
      tags:
        - User Management
      summary: Create payment details for a partner user
      description: |
        Create payment details (beneficiary details) for a partner user. Used for offramp operations where crypto is converted to fiat and sent to the user's payment account. Partners can only access their own users.

        Institution handling:
        - for most rails, `institution_id` must be provided from `/api/v1/supported/institutions`
        - partners may omit `institution_id` only for the explicitly supported rails below; the backend will assign the generic `other-bank` payment method automatically

        Currently supported omission rails:
        - `iban-sepa`
        - `nip-nigeria`
      operationId: createPaymentDetails
      security:
        - ApiKeyAuth: []
      parameters:
        - name: user_uuid
          in: path
          required: true
          description: The user_uuid returned by POST /partner/users. Partners can only access their own users.
          schema:
            type: string
            format: uuid
          example: "550e8400-e29b-41d4-a716-446655440000"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreatePaymentDetailsRequest'
            examples:
              iban-sepa:
                summary: SEPA payment details (Europe, institution optional)
                value:
                  currency: EUR
                  rail: iban-sepa
                  country_code: DE
                  details:
                    full_name: "John Doe"
                    iban: DE89370400440532013000
              cvu-argentina:
                summary: CVU payment details (Argentina - Mobile Money/Wallets)
                value:
                  currency: ARS
                  rail: cvu-cbu
                  institution_id: uala
                  country_code: AR
                  details:
                    cvu: "0000003100000001234567"
                    alias: "john.doe"
                    phone_number: "+541112345678"
              cbu-argentina:
                summary: CBU payment details (Argentina - Traditional/Digital Banks)
                value:
                  currency: ARS
                  rail: cvu-cbu
                  institution_id: banco-santander-argentina
                  country_code: AR
                  details:
                    cbu: "0000003100000001234567"
                    alias: "john.doe"
                    phone_number: "+541112345678"
      responses:
        '201':
          description: Payment details created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreatePaymentDetailsResponse'
        '400':
          description: Bad Request - invalid payment details or unsupported combination
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                data:
                  error_key: "invalid_payment_details"
        '401':
          description: Unauthorized
        '404':
          description: User not found
        '422':
          description: >-
            Recipient details are incomplete or invalid for the payout corridor. The recipient fields
            you supply are validated against the provider's per-corridor requirements; if a required
            field is missing or malformed, the recipient is not created and the offending field names
            are listed in the error message. Fix the details and resubmit. Address fields
            auto-populated from KYC are not part of this check (a missing address does not block
            creation — see the KYC endpoints).
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: false
                  error:
                    type: string
                    description: Human-readable message naming the missing or invalid recipient fields.
              example:
                success: false
                error: "recipient details are incomplete or invalid for this payout method: iban — please correct and resubmit"
        '500':
          description: Internal Server Error

    # Step 7: Get all payment details for a partner user
    get:
      tags:
        - User Management
      summary: Payment details for a partner user
      description: Retrieve all payment details (payment profiles) for a partner user. Returns all saved beneficiary details that can be used for offramp operations. Partners can only access their own users.
      operationId: getPaymentDetails
      security:
        - ApiKeyAuth: []
      parameters:
        - name: user_uuid
          in: path
          required: true
          description: The user_uuid returned by POST /partner/users. Partners can only access their own users.
          schema:
            type: string
            format: uuid
          example: "550e8400-e29b-41d4-a716-446655440000"
      responses:
        '200':
          description: Successfully retrieved payment details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GetPaymentDetailsResponse'
        '401':
          description: Unauthorized
        '404':
          description: User not found
        '500':
          description: Internal Server Error

  # Step 7b / 8: Get or delete a single payment detail for a partner user
  /api/v1/partner/users/{user_uuid}/payment-details/{payment_details_id}:
    get:
      tags:
        - User Management
      summary: Get a single payment detail for a partner user
      description: |
        Retrieve one payment detail (payment profile) by its `payment_details_id`, returning the same shape as one element of the list endpoint (`rail`, `currency`, `institution_name`, `institution_id`, `details`).

        Use this when you already hold a `payment_details_id` instead of listing every payment detail for the user — it stays fast on accounts with many saved payment details.

        Partners can only access their own users. A payment detail that belongs to a different user (or another partner) returns `404` rather than revealing that it exists.
      operationId: getPaymentDetailById
      security:
        - ApiKeyAuth: []
      parameters:
        - name: user_uuid
          in: path
          required: true
          description: The user_uuid returned by POST /partner/users. Partners can only access their own users.
          schema:
            type: string
            format: uuid
          example: "550e8400-e29b-41d4-a716-446655440000"
        - name: payment_details_id
          in: path
          required: true
          description: The payment_details_id returned when creating or listing payment details.
          schema:
            type: string
          example: "987"
      responses:
        '200':
          description: Successfully retrieved the payment detail
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GetPaymentDetailResponse'
        '400':
          description: Bad Request - malformed payment_details_id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Unauthorized
        '404':
          description: User not found, or payment detail not found / not owned by this user
        '500':
          description: Internal Server Error

    delete:
      tags:
        - User Management
      summary: Delete payment details for a partner user
      description: Delete a specific payment details (payment profile) for a partner user by its ID. Partners can only access their own users.
      operationId: deletePaymentDetails
      security:
        - ApiKeyAuth: []
      parameters:
        - name: user_uuid
          in: path
          required: true
          description: The user_uuid returned by POST /partner/users. Partners can only access their own users.
          schema:
            type: string
            format: uuid
          example: "550e8400-e29b-41d4-a716-446655440000"
        - name: payment_details_id
          in: path
          required: true
          description: The payment_details_id returned when creating payment details.
          schema:
            type: string
          example: "987"
      responses:
        '200':
          description: Payment details deleted successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  data:
                    type: object
                    properties:
                      message:
                        type: string
                        example: "Payment details deleted successfully"
        '401':
          description: Unauthorized
        '404':
          description: User not found or payment details not found
        '409':
          description: Conflict - payment details linked to active offers
        '500':
          description: Internal Server Error

  # ============================================
  # 8. LIQUIDITY
  # ============================================
  /api/v1/partner/liquidity:
    get:
      tags:
        - Liquidity
      summary: Get aggregated liquidity by trading pairs
      description: |
        Returns real-time aggregated liquidity data across all active off-ramp offers.

        Use this endpoint for **liquidity discovery** — to determine which crypto/fiat pairs
        are currently available, what fiat amount ranges are supported, and which payment
        methods are accepted. No pricing or vendor details are returned.

        **Trade type semantics:** `trade_type` is expressed in partner terms.
        `SELL` means the partner sells crypto and receives fiat (the most common off-ramp direction).
        `BUY` means the partner buys crypto by sending fiat.

        **Eligibility:** Only offers that pass the same gates as the vendor-matching engine
        are included — the offer must be authorized, the vendor must be active and online,
        and at least one payment method option must be active.

        **Aggregation:** Results are grouped by `(crypto_currency_code, fiat_currency_code, trade_type)`.
        Amount ranges span the minimum and maximum across all eligible offers in the group.
        Payment methods are deduplicated and sorted alphabetically.

        **Authentication:** None. This is a public endpoint — no API key required.
      security: []
      responses:
        '200':
          description: Liquidity pairs retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  data:
                    type: object
                    properties:
                      pairs:
                        type: array
                        items:
                          $ref: '#/components/schemas/LiquidityPair'
              example:
                success: true
                data:
                  pairs:
                    - crypto_currency_code: USDC
                      fiat_currency_code: EUR
                      trade_type: SELL
                      min_amount: 50.00
                      max_amount: 10000.00
                      min_amount_usd: 54.35
                      max_amount_usd: 10870.00
                      payment_methods:
                        - sepa
                        - wise
                    - crypto_currency_code: USDC
                      fiat_currency_code: NGN
                      trade_type: SELL
                      min_amount: 50000.00
                      max_amount: 5000000.00
                      min_amount_usd: 31.25
                      max_amount_usd: 3125.00
                      payment_methods:
                        - gtbank
                        - zenith-bank
        '500':
          description: Internal server error

  # ============================================
  # 9. ON-RAMP OPERATIONS
  # ============================================
  /api/v1/partner/onramp/estimate:
    post:
      tags:
        - On-Ramp
      summary: Get on-ramp price estimate
      description: |
        Get indicative on-ramp pricing (fiat to crypto) without creating a quote.
        This endpoint is lightweight and does not reserve liquidity.

        Use this when you need quick pricing discovery before calling `/partner/onramp/quote`.
        Optional filters: `payment_method_slug`, `payment_network_slug`, `provider_scope`, `country_code`.
        This endpoint is indicative and does not apply user/payment-details ownership checks; for executable pricing use quote.
      operationId: getOnRampEstimate
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PartnerPriceEstimateRequest'
            example:
              crypto_currency: USDT
              fiat_currency: VND
              fiat_amount: "26345"
              payment_method_slug: zalopay
              payment_network_slug: napas-vietnam
              country_code: VN
              provider_scope: all
      responses:
        '200':
          description: Estimate retrieved successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/APIResponse'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/PartnerPriceEstimateResponse'
              example:
                success: true
                data:
                  rate: "26345.00"
                  crypto_amount: "1.005000"
                  fiat_amount: "26345.00"
                  fee_breakdown:
                    platform_fee: "0.005000"
                    platform_fee_pct: 0.5
                    partner_fee: "0.00"
                    partner_fee_pct: 0
                    total_fee: "0.005000"
                  payment_method_slug: zalopay
                  payment_network_slug: napas-vietnam
                  provider_scope: all
                  is_indicative: true
        '400':
          description: |
            Bad request — invalid parameters, missing required fields,
            unsupported cryptocurrency, or invalid provider_scope.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerErrorResponse'
              examples:
                invalid_amount:
                  summary: Invalid amount configuration
                  value:
                    success: false
                    error:
                      code: INVALID_REQUEST
                      message: "exactly one of crypto_amount or fiat_amount must be provided"
                unsupported_crypto:
                  summary: Unsupported cryptocurrency
                  value:
                    success: false
                    error:
                      code: INVALID_REQUEST
                      message: "unsupported cryptocurrency: BTC"
                invalid_provider_scope:
                  summary: Invalid provider_scope value
                  value:
                    success: false
                    error:
                      code: INVALID_REQUEST
                      message: "invalid provider_scope: must be all, licensed_only, or p2p_only"
        '401':
          description: Unauthorized — missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerErrorResponse'
              example:
                success: false
                error:
                  code: UNAUTHORIZED
                  message: "missing X-API-Key header"
        '409':
          description: No offers available for the requested pair/amount
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerErrorResponse'
              example:
                success: false
                error:
                  code: NO_OFFERS_AVAILABLE
                  message: "no matching offers found for the provided criteria"
        '500':
          description: Internal Server Error

  /api/v1/partner/onramp/quote:
    post:
      tags:
        - On-Ramp
      summary: Get on-ramp quote
      description: |
        Get a price quote for an on-ramp operation (fiat to crypto). Returns the exchange rate,
        amounts, and a quote ID that can be used to initiate the order. Quotes expire after 60 seconds.

        The `user_uuid` is the public UUID returned by `POST /partner/users` (the `id` field).

        **Fee structure:** The `fee_breakdown` object shows all fee components transparently:
        `platform_fee` (our fee in crypto), `partner_fee` (partner's markup, currently 0, configurable
        via dashboard in a future release), and `total_fee` (sum of both).

        **Payment method:** Provide `payment_method_slug` and/or `payment_network_slug` to narrow routing.
        If omitted, the best available option for the pair is selected automatically.
      operationId: getOnRampQuote
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PartnerOnRampQuoteRequest'
            example:
              user_uuid: "23d60610-9737-43b2-a751-a2c6a7bba457"
              fiat_amount: "26345"
              fiat_currency: VND
              crypto_currency: USDT
              payment_method_slug: zalopay
              payment_network_slug: napas-vietnam
              country_code: VN
      responses:
        '200':
          description: Quote retrieved successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/APIResponse'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/PartnerOnRampQuoteResponse'
              example:
                success: true
                data:
                  quote_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                  rate: "26345.00"
                  crypto_amount: "1.005000"
                  fiat_amount: "26345.00"
                  fee_breakdown:
                    platform_fee: "0.005000"
                    platform_fee_pct: 0.5
                    partner_fee: "0.00"
                    partner_fee_pct: 0
                    total_fee: "0.005000"
                  payment_method_slug: zalopay
                  payment_network_slug: napas-vietnam
                  expires_at: "2026-03-30T19:15:50Z"
        '400':
          description: Bad request — invalid parameters or missing required fields
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerErrorResponse'
              example:
                success: false
                error:
                  code: INVALID_REQUEST
                  message: "Missing required field: fiat_currency"
        '401':
          description: Unauthorized — invalid or missing API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerErrorResponse'
              example:
                success: false
                error:
                  code: UNAUTHORIZED
                  message: "Invalid or missing API key"
        '404':
          description: User not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerErrorResponse'
              example:
                success: false
                error:
                  code: INVALID_REQUEST
                  message: "User not found"
        '409':
          description: No offers available for the requested pair
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerErrorResponse'
              example:
                success: false
                error:
                  code: NO_OFFERS_AVAILABLE
                  message: "No offers available for USDT/VND"
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerErrorResponse'

  /api/v1/partner/onramp/initiate:
    post:
      tags:
        - On-Ramp
      summary: Initiate on-ramp order
      description: |
        Initiate an on-ramp order using a previously obtained quote. The quote must still be active
        (not expired or already used).

        On success, returns the created order with status `awaiting_liquidity_provider`.
        Once a vendor accepts, the order moves to `awaiting_vendor_escrow_funding`, then
        `awaiting_fiat_transfer` when the crypto is locked in escrow.

        At `awaiting_fiat_transfer` the partner must call `confirm-payment-sent` to advance the order.
        If the order type is `payment_request`, call `submit-payer-details` first before confirming.
      operationId: initiateOnRamp
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PartnerInitiateRequest'
            example:
              quote_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
              reference_id: "your-internal-order-id"
      responses:
        '201':
          description: On-ramp order created successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/APIResponse'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/PartnerInitiateResponse'
              example:
                success: true
                data:
                  order_id: "ce3cfd6a-1234-5678-abcd-ef1234567890"
                  status: awaiting_liquidity_provider
                  quote_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                  reference_id: "your-internal-order-id"
                  order_type: onramp
                  amounts:
                    crypto_amount: "1.010025"
                    fiat_amount: "26476.72"
                    crypto_currency: USDT
                    fiat_currency: VND
                    rate: "26345.00"
                  fee_breakdown:
                    platform_fee: "0.005000"
                    platform_fee_pct: 0.5
                    partner_fee: "0.00"
                    partner_fee_pct: 0
                    total_fee: "0.005000"
                  timeline: []
                  created_at: "2026-03-30T19:00:05Z"
        '400':
          description: Bad request — missing required fields
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerErrorResponse'
        '401':
          description: Unauthorized — invalid or missing API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerErrorResponse'
        '404':
          description: Quote not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerErrorResponse'
              example:
                success: false
                error:
                  code: QUOTE_NOT_FOUND
                  message: "Quote not found"
        '409':
          description: Quote expired or already used
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerErrorResponse'
              examples:
                quote_expired:
                  summary: Quote has expired
                  value:
                    success: false
                    error:
                      code: QUOTE_EXPIRED
                      message: "Quote has expired, please request a new quote"
                quote_already_used:
                  summary: Quote already consumed
                  value:
                    success: false
                    error:
                      code: QUOTE_ALREADY_USED
                      message: "Quote has already been used"
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerErrorResponse'

  /api/v1/partner/orders/{order_id}/submit-payer-details:
    post:
      tags:
        - On-Ramp
      summary: Submit payer details
      description: |
        Submit the end-user's payment details for a `payment_request` on-ramp order.
        Only required when the order type is `payment_request` — regular on-ramp orders do not need this step.

        **Order types:**
        - **Regular on-ramp** — the vendor sends fiat to the end-user directly. No payer details needed.
          The partner receives vendor payment details in the `vendor_payment_details` field of `GET /orders/:id`
          and forwards them to the end-user to initiate the transfer.
        - **`payment_request` on-ramp** — the vendor sends a payment request (e.g. M-Pesa push) to the end-user.
          The end-user's account details must be submitted here first so the vendor knows where to send the request.
          Check `payment_request: true` in the order response to identify this type.

        Call this endpoint after initiating the order and before calling `confirm-payment-sent`.
        The order must be in `awaiting_fiat_transfer` status.

        Provide either a `payment_details_id` (referencing a previously saved payment profile) or
        an inline `payment_details` object with the payer's account information.
      operationId: submitPayerDetails
      security:
        - ApiKeyAuth: []
      parameters:
        - name: order_id
          in: path
          required: true
          description: Order UUID returned by the initiate endpoint
          schema:
            type: string
            format: uuid
          example: "ce3cfd6a-1234-5678-abcd-ef1234567890"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SubmitPayerDetailsRequest'
            examples:
              by_id:
                summary: Using a saved payment profile
                value:
                  payment_details_id: 1444
              inline:
                summary: Providing details inline
                value:
                  payment_details:
                    payment_method_id: 5
                    payment_network_id: 12
                    details:
                      phone: "+254712345678"
                      name: "John Doe"
                    country_code: KE
      responses:
        '200':
          description: Payer details submitted successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/APIResponse'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          message:
                            type: string
                            example: "payer details submitted successfully"
              example:
                success: true
                data:
                  message: "payer details submitted successfully"
        '400':
          description: Bad request — invalid or missing payer details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerErrorResponse'
        '401':
          description: Unauthorized — invalid or missing API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerErrorResponse'
        '404':
          description: Order not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerErrorResponse'
              example:
                success: false
                error:
                  code: ORDER_NOT_FOUND
                  message: "Order not found"
        '409':
          description: Order is not in the correct status or not a payment_request order
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerErrorResponse'
              example:
                success: false
                error:
                  code: INVALID_STATUS
                  message: "Order is not in awaiting_fiat_transfer status"
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerErrorResponse'

  /api/v1/partner/orders/{order_id}/confirm-payment-sent:
    post:
      tags:
        - On-Ramp
      summary: Confirm fiat payment sent
      description: |
        Confirm that the end-user has initiated the fiat payment to the vendor.
        This advances the order from `awaiting_fiat_transfer` to `fiat_transfer_pending`,
        signalling to the vendor that the fiat is on its way.

        For `payment_request` orders, call `submit-payer-details` first.
        No request body required.
      operationId: confirmPaymentSent
      security:
        - ApiKeyAuth: []
      parameters:
        - name: order_id
          in: path
          required: true
          description: Order UUID
          schema:
            type: string
            format: uuid
          example: "ce3cfd6a-1234-5678-abcd-ef1234567890"
      responses:
        '200':
          description: Payment confirmed successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/APIResponse'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          order_id:
                            type: string
                            format: uuid
                            example: "ce3cfd6a-1234-5678-abcd-ef1234567890"
                          status:
                            type: string
                            example: fiat_transfer_pending
              example:
                success: true
                data:
                  order_id: "ce3cfd6a-1234-5678-abcd-ef1234567890"
                  status: fiat_transfer_pending
        '400':
          description: Bad Request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerErrorResponse'
        '401':
          description: Unauthorized — invalid or missing API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerErrorResponse'
        '404':
          description: Order not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerErrorResponse'
              example:
                success: false
                error:
                  code: ORDER_NOT_FOUND
                  message: "Order not found"
        '409':
          description: Order is not in awaiting_fiat_transfer status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerErrorResponse'
              example:
                success: false
                error:
                  code: INVALID_STATUS
                  message: "Order is not in awaiting_fiat_transfer status"
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerErrorResponse'

  /api/v1/partner/orders/{order_id}/bridge-authorization-parameters:
    get:
      tags:
        - On-Ramp
      summary: Get send-out authorization parameters
      description: |
        After an on-ramp order reaches `completed` status (crypto on master wallet), the partner
        can optionally initiate a cross-chain send-out to transfer the USDT to an external network
        (e.g. Arbitrum).

        This endpoint returns the EIP-712 `ForwardRequest` parameters required to authorize the
        transfer. The partner must sign the request locally and submit the signature to
        `POST /api/v1/partner/orders/{order_id}/authorize-bridge`.

        **Prerequisites:**
        - Order status must be `completed` (internal: `escrow_released_to_buyer`)
        - `destination_chain` must be a supported chain slug (e.g. `arbitrum`, `ethereum`)
        - `destination_address` must be a valid EVM address

        **Response includes:**
        - `forward_request` — pre-filled EIP-712 ForwardRequest object to sign
        - `eip712_domain` — domain parameters for EIP-712 signing
        - `quote_id` — Relay quote ID embedded in the transfer calldata
        - `to_amount` — estimated USDT amount to be received on the destination chain (atomic units)
        - `forwarder_nonce` — current on-chain nonce for the master wallet
      operationId: getBridgeAuthorizationParameters
      security:
        - ApiKeyAuth: []
      parameters:
        - name: order_id
          in: path
          required: true
          description: Order UUID
          schema:
            type: string
            format: uuid
          example: "ce3cfd6a-1234-5678-abcd-ef1234567890"
        - name: destination_chain
          in: query
          required: true
          description: Destination chain slug (e.g. `arbitrum`, `ethereum`)
          schema:
            type: string
          example: arbitrum
        - name: destination_address
          in: query
          required: true
          description: Recipient EVM address on the destination chain
          schema:
            type: string
          example: "0x9B61e9b3aed5bbFa66d778854E3dE7AF3241F021"
      responses:
        '200':
          description: Authorization parameters returned successfully
          content:
            application/json:
              example:
                success: true
                data:
                  order_id: "ce3cfd6a-1234-5678-abcd-ef1234567890"
                  chain_id: "660279"
                  rpc_url: "https://xai-chain.net/rpc"
                  token_address: "0xf86Cc81F4E480CF54Eb013FFe6929a0C2Ad5EdCA"
                  forwarder_address: "0x6fFCF38bEc8c733b096958fcd2a8E31A00530EDC"
                  solver_address: "0x00A72adEB6542916F53dc2d78450CD436320868B"
                  sender_address: "0x6b14951552EA9fCE8f58EC52B909C14CB0619Af2"
                  amount_atomic: "10100"
                  amount_human: "0.010100"
                  crypto_currency: "USDT"
                  crypto_decimals: 6
                  recommended_gas: "500000"
                  recommended_ttl: 3600
                  transfer_data: "0xa9059cbb..."
                  forwarder_nonce: "63"
                  quote_id: "0x9b0539b1..."
                  to_amount: "10100"
                  destination_chain: "arbitrum"
                  destination_address: "0x9B61e9b3aed5bbFa66d778854E3dE7AF3241F021"
                  expires_at: "2026-04-03T17:13:12Z"
                  authorization_path: "/api/v1/partner/orders/ce3cfd6a-1234-5678-abcd-ef1234567890/authorize-bridge"
        '400':
          description: Invalid destination_chain or destination_address
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerErrorResponse'
        '401':
          description: Unauthorized — invalid or missing API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerErrorResponse'
        '404':
          description: Order not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerErrorResponse'
        '409':
          description: Order is not in `completed` status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerErrorResponse'
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerErrorResponse'

  /api/v1/partner/orders/{order_id}/authorize-bridge:
    post:
      tags:
        - On-Ramp
      summary: Authorize send-out (cross-chain transfer)
      description: |
        Submit the signed EIP-712 ForwardRequest to initiate a cross-chain send-out.
        The signature is verified on-chain via the XAI forwarder contract.

        Call `GET bridge-authorization-parameters` first to get the parameters to sign.

        After submission the order transitions to `send_out_pending`. A background worker
        monitors the Relay bridge and updates the status to `send_out_completed` or
        `send_out_failed` when the transfer settles. The partner receives a webhook for
        each transition.

        When the send-out completes, the `send_out_tx_hash` field in `GET /orders/:id`
        contains the destination-chain transaction hash.

        **This endpoint is idempotent at the on-chain level** — the EIP-712 nonce prevents
        duplicate submissions. A second call with the same nonce will be rejected.
      operationId: authorizeBridge
      security:
        - ApiKeyAuth: []
      parameters:
        - name: order_id
          in: path
          required: true
          description: Order UUID
          schema:
            type: string
            format: uuid
          example: "ce3cfd6a-1234-5678-abcd-ef1234567890"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [destination_chain, destination_address, forward_request, signature]
              properties:
                destination_chain:
                  type: string
                  description: Destination chain slug (must match the value used in bridge-authorization-parameters)
                  example: arbitrum
                destination_address:
                  type: string
                  description: Recipient EVM address on the destination chain
                  example: "0x9B61e9b3aed5bbFa66d778854E3dE7AF3241F021"
                forward_request:
                  type: object
                  description: EIP-712 ForwardRequest object (values from bridge-authorization-parameters)
                  required: [from, to, value, gas, nonce, deadline, data]
                  properties:
                    from:
                      type: string
                      example: "0x6b14951552EA9fCE8f58EC52B909C14CB0619Af2"
                    to:
                      type: string
                      example: "0xf86Cc81F4E480CF54Eb013FFe6929a0C2Ad5EdCA"
                    value:
                      type: string
                      example: "0"
                    gas:
                      type: string
                      example: "500000"
                    nonce:
                      type: string
                      example: "63"
                    deadline:
                      type: string
                      example: "1775236548"
                    data:
                      type: string
                      example: "0xa9059cbb..."
                signature:
                  type: string
                  description: EIP-712 signature of the ForwardRequest (hex, with 0x prefix)
                  example: "0xc9a69a67..."
            example:
              destination_chain: arbitrum
              destination_address: "0x9B61e9b3aed5bbFa66d778854E3dE7AF3241F021"
              forward_request:
                from: "0x6b14951552EA9fCE8f58EC52B909C14CB0619Af2"
                to: "0xf86Cc81F4E480CF54Eb013FFe6929a0C2Ad5EdCA"
                value: "0"
                gas: "500000"
                nonce: "63"
                deadline: "1775236548"
                data: "0xa9059cbb..."
              signature: "0xc9a69a67..."
      responses:
        '200':
          description: Send-out submitted successfully
          content:
            application/json:
              example:
                success: true
                data:
                  order_id: "ce3cfd6a-1234-5678-abcd-ef1234567890"
                  status: send_out_pending
        '400':
          description: Invalid request — bad forward_request, nonce mismatch, or invalid destination
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerErrorResponse'
        '401':
          description: Unauthorized — invalid or missing API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerErrorResponse'
        '404':
          description: Order not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerErrorResponse'
        '409':
          description: Order is not in `completed` status or send-out already in progress
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerErrorResponse'
        '502':
          description: Transactor service unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerErrorResponse'
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerErrorResponse'

  # ============================================
  # 10. OFF-RAMP OPERATIONS
  # ============================================
  /api/v1/partner/offramp/estimate:
    post:
      tags:
        - Off-Ramp
      summary: Get off-ramp price estimate
      description: |
        Get indicative off-ramp pricing (crypto to fiat) without creating a quote.
        Pricing can be driven from either side: provide `crypto_amount` to compute the fiat payout, or `fiat_amount` to compute the crypto required (exactly one of the two).
        This endpoint is lightweight and does not reserve liquidity.

        Use this when you need quick pricing discovery before calling `/partner/offramp/quote`.
        Optional filters: `payment_method_slug`, `payment_network_slug`, `provider_scope`, `country_code`.
        This endpoint is indicative and does not apply user/payment-details ownership checks; for executable pricing use quote.

        **Note:** EUR, AUD, and GBP off-ramp payouts require the user's physical
        address. Ensure the user's KYC includes `address`, `city`, and `postal_code` before initiating.
        See `PATCH /partner/users/{user_uuid}/kyc` to update.
      operationId: getOffRampEstimate
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PartnerPriceEstimateRequest'
            example:
              crypto_currency: USDT
              fiat_currency: NGN
              crypto_amount: "100.00"
              payment_method_slug: access-bank
              payment_network_slug: nibss
              country_code: NG
              provider_scope: all
      responses:
        '200':
          description: Estimate retrieved successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/APIResponse'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/PartnerPriceEstimateResponse'
              example:
                success: true
                data:
                  rate: "1530.12"
                  crypto_amount: "101.50"
                  fiat_amount: "152880.00"
                  fee_breakdown:
                    platform_fee: "1.50"
                    platform_fee_pct: 1.5
                    partner_fee: "0.00"
                    partner_fee_pct: 0
                    total_fee: "1.50"
                  payment_method_slug: access-bank
                  payment_network_slug: nibss
                  provider_scope: all
                  is_indicative: true
        '400':
          description: |
            Bad request — invalid parameters, missing required fields, 
            unsupported cryptocurrency, or invalid provider_scope.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerErrorResponse'
              examples:
                invalid_amount:
                  summary: Invalid amount configuration
                  value:
                    success: false
                    error:
                      code: INVALID_REQUEST
                      message: "exactly one of crypto_amount or fiat_amount must be provided"
                unsupported_crypto:
                  summary: Unsupported cryptocurrency
                  value:
                    success: false
                    error:
                      code: INVALID_REQUEST
                      message: "unsupported cryptocurrency: BTC"
                invalid_provider_scope:
                  summary: Invalid provider_scope value
                  value:
                    success: false
                    error:
                      code: INVALID_REQUEST
                      message: "invalid provider_scope: must be all, licensed_only, or p2p_only"
        '401':
          description: Unauthorized - missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerErrorResponse'
              example:
                success: false
                error:
                  code: UNAUTHORIZED
                  message: "missing X-API-Key header"
        '409':
          description: No offers available for the requested pair/amount
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerErrorResponse'
              example:
                success: false
                error:
                  code: NO_OFFERS_AVAILABLE
                  message: "no matching offers found for the provided criteria"
        '500':
          description: Internal Server Error

  /api/v1/partner/offramp/quote:
    post:
      tags:
        - Off-Ramp
      summary: Get off-ramp quote
      description: |
        Get a price quote for an off-ramp operation (crypto to fiat). Returns the exchange rate,
        amounts, and a quote ID that can be used to initiate the order. Quotes expire after 60 seconds.

        The `user_uuid` is the public UUID returned by `POST /partner/users` (the `id` field).
        The `payment_details_id` is the ID of a previously created payment profile for the user.

        **Anchor mode — provide exactly one of `crypto_amount` or `fiat_amount`:**

        | Field | Anchor | Guarantee |
        |---|---|---|
        | `crypto_amount` | Crypto-anchored | Exact crypto cost; fiat payout varies by the rate at settlement |
        | `fiat_amount` | Fiat-anchored | Exact fiat delivery to recipient; crypto cost may vary within a 1.20× safety bound |

        The response `anchor_type` field tells you which side is guaranteed: `"crypto"` or `"fiat"`.

        **Fiat-anchored notes:**
        - Useful for exact-payout use cases (e.g. deliver exactly 500,000 NGN).
        - If the crypto cost we quote exceeds 1.20× the expected amount, the order moves to `failed` status
          and the deposit is automatically refunded from escrow back to the partner wallet. Create a new quote
          to retry.
        - `slippage_tolerance.type=amount` in fiat-anchored mode is evaluated as crypto-side delta, not fiat-side
          (fiat is always exact by construction).
        - Integer-minor-unit currencies (NGN, UGX, TZS, KES, GHS): `fiat_amount` must be a whole number.

        **Fee structure:** The `fee_breakdown` object shows all fee components transparently:
        `platform_fee` (our fee in crypto), `partner_fee` (partner's markup, currently 0, configurable
        via dashboard in a future release), and `total_fee` (sum of both).

        **Address requirement for EUR/AUD/GBP:** EUR, AUD, and GBP payouts require the
        user's physical address (street, city, postal code). If the user's
        KYC does not include address data, the quote response will include a `beneficiary_requirements`
        field indicating which fields are needed. You can update the user's KYC with
        `PATCH /partner/users/{user_uuid}/kyc` to provide the address before or after initiating the order.
        If you initiate without address, the order will be created with status `pending_address` until
        the address is provided.
      operationId: getOffRampQuote
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PartnerQuoteRequest'
            examples:
              crypto_anchored:
                summary: Crypto-anchored (exact crypto cost)
                value:
                  user_uuid: "550e8400-e29b-41d4-a716-446655440000"
                  payment_details_id: 987
                  crypto_currency: USDT
                  crypto_amount: "100.00"
                  fiat_currency: NGN
                  provider_scope: all
                  has_fiat_settlement_notification: false
                  slippage_tolerance:
                    type: percent
                    value: 1.5
              fiat_anchored:
                summary: Fiat-anchored (exact fiat delivery)
                value:
                  user_uuid: "550e8400-e29b-41d4-a716-446655440000"
                  payment_details_id: 987
                  crypto_currency: USDT
                  fiat_amount: "500000"
                  fiat_currency: NGN
                  provider_scope: all
                  has_fiat_settlement_notification: false
                  slippage_tolerance:
                    type: percent
                    value: 1.5
      responses:
        '200':
          description: Quote retrieved successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/APIResponse'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/PartnerQuoteResponse'
              examples:
                crypto_anchored:
                  summary: Crypto-anchored quote
                  value:
                    success: true
                    data:
                      quote_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                      anchor_type: "crypto"
                      rate: "1530.12"
                      crypto_amount: "101.50"
                      fiat_amount: "152880"
                      fee_breakdown:
                        platform_fee: "1.50"
                        platform_fee_pct: 1.5
                        partner_fee: "0.00"
                        partner_fee_pct: 0
                        total_fee: "1.50"
                      payment_details_id: 987
                      payment_method_id: 5
                      payment_network_id: 12
                      expires_at: "2026-02-01T12:00:30Z"
                fiat_anchored:
                  summary: Fiat-anchored quote
                  value:
                    success: true
                    data:
                      quote_id: "b2c3d4e5-f6a7-8901-bcde-ef1234567890"
                      anchor_type: "fiat"
                      rate: "1530.12"
                      crypto_amount: "328.10"
                      fiat_amount: "500000"
                      fee_breakdown:
                        platform_fee: "4.92"
                        platform_fee_pct: 1.5
                        partner_fee: "0.00"
                        partner_fee_pct: 0
                        total_fee: "4.92"
                      payment_details_id: 987
                      payment_method_id: 5
                      payment_network_id: 12
                      expires_at: "2026-02-01T12:00:30Z"
        '400':
          description: Bad request — invalid parameters or missing required fields
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerErrorResponse'
              examples:
                missing_currency:
                  summary: Missing required field
                  value:
                    success: false
                    error:
                      code: INVALID_REQUEST
                      message: "Missing required field: crypto_currency"
                ambiguous_amount:
                  summary: Both or neither amount fields provided
                  value:
                    success: false
                    error:
                      code: INVALID_REQUEST
                      message: "exactly one of crypto_amount or fiat_amount must be provided"
                sub_minor_unit:
                  summary: Fractional fiat for integer-minor-unit currency
                  value:
                    success: false
                    error:
                      code: INVALID_REQUEST
                      message: "fiat_amount must be a whole number for integer-minor-unit currencies"
        '401':
          description: Unauthorized — invalid or missing API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerErrorResponse'
              example:
                success: false
                error:
                  code: UNAUTHORIZED
                  message: "Invalid or missing API key"
        '404':
          description: User or payment details not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerErrorResponse'
              example:
                success: false
                error:
                  code: INVALID_PAYMENT_DETAILS
                  message: "Payment details not found or do not belong to user"
        '409':
          description: No offers available for the requested pair
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerErrorResponse'
              example:
                success: false
                error:
                  code: NO_OFFERS_AVAILABLE
                  message: "No offers available for USDT/NGN"
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerErrorResponse'

  /api/v1/partner/offramp/initiate:
    post:
      tags:
        - Off-Ramp
      summary: Initiate off-ramp order
      description: |
        Initiate an off-ramp order using a previously obtained quote. The quote must still be active
        (not expired or already used). The system refreshes the price and checks slippage tolerance
        before creating the order.

        If the price has moved beyond the slippage tolerance, a `PRICE_CHANGED_REQUOTE` error is returned
        with details about the old and new rates, so the partner can request a new quote.

        On success, returns the created order with status `awaiting_liquidity_provider`.

        **Address requirement for EUR/AUD/GBP:** For EUR/AUD/GBP orders, if the
        user's KYC does not include a physical address, the order will be created with status
        `pending_address`. The response will include `required_fields: ["address", "city", "postal_code"]`
        and `next_action: "provide_address"`. Call `PATCH /partner/users/{user_uuid}/kyc` to add the
        address, and the order will automatically advance.

        **Recommended flow for EUR/AUD/GBP:**
        1. Submit KYC with address/city/postal_code (`POST /kyc-submissions`)
        2. Create payment details (`POST /payment-details`) — address auto-populated from KYC
        3. Get quote (`POST /offramp/quote`)
        4. Initiate (`POST /offramp/initiate`) — proceeds without delays
      operationId: initiateOffRamp
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PartnerInitiateRequest'
            example:
              quote_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
      responses:
        '201':
          description: Off-ramp order created successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/APIResponse'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/PartnerInitiateResponse'
              example:
                success: true
                data:
                  order_id: "b2c3d4e5-f6a7-8901-bcde-f12345678901"
                  status: awaiting_liquidity_provider
                  quote_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                  amounts:
                    crypto_amount: "101.50"
                    fiat_amount: "152880.00"
                    crypto_currency: USDT
                    fiat_currency: NGN
                    rate: "1530.12"
                  fee_breakdown:
                    platform_fee: "1.50"
                    platform_fee_pct: 1.5
                    partner_fee: "0.00"
                    partner_fee_pct: 0
                    total_fee: "1.50"
                  timeline:
                    - status: awaiting_liquidity_provider
                      timestamp: "2026-02-01T12:00:05Z"
                      description: "Order created and awaiting liquidity provider"
                  created_at: "2026-02-01T12:00:05Z"
        '400':
          description: Bad request — missing required fields
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerErrorResponse'
        '401':
          description: Unauthorized — invalid or missing API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerErrorResponse'
        '404':
          description: Quote not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerErrorResponse'
              example:
                success: false
                error:
                  code: QUOTE_NOT_FOUND
                  message: "Quote not found"
        '409':
          description: Quote expired, already used, or slippage exceeded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerErrorResponse'
              examples:
                quote_expired:
                  summary: Quote has expired
                  value:
                    success: false
                    error:
                      code: QUOTE_EXPIRED
                      message: "Quote has expired, please request a new quote"
                quote_already_used:
                  summary: Quote already consumed
                  value:
                    success: false
                    error:
                      code: QUOTE_ALREADY_USED
                      message: "Quote has already been used"
                slippage_exceeded:
                  summary: Price changed beyond tolerance
                  value:
                    success: false
                    error:
                      code: PRICE_CHANGED_REQUOTE
                      message: "Price has changed beyond slippage tolerance, please request a new quote"
                      details:
                        quoted_rate: 1530.12
                        current_rate: 1545.00
                        fiat_difference: 1510.38
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerErrorResponse'

  # ============================================
  # 11. ORDER MANAGEMENT
  # ============================================
  /api/v1/partner/orders/{order_id}:
    get:
      tags:
        - Orders
      summary: Order details
      description: |
        Get the current status, amounts, and full timeline for a specific order.
        The `next_action` field indicates what the partner **must** do to advance the order.
        `null` means no action is required from the partner at this stage — the order is waiting
        for the counterparty (e.g. the buyer to submit fiat payment proof).

        The `allowed_actions` array lists all API actions the partner **can** perform at the current
        status. An action can be in `allowed_actions` even when `next_action` is `null` — this means
        the action is optional. For example, `confirm-fiat-received` is available at `crypto_received`
        as a fast-track: if you already know the fiat has arrived (via your own systems), you can
        confirm immediately without waiting for the buyer's payment proof flow to complete.

        **Order flow summary:**

        | Status | next_action | Who acts |
        |---|---|---|
        | `created` | — | Partner (or wait for vendor match) |
        | `awaiting_crypto_transfer_authorization` | `authorize_crypto_transfer` | **Partner** |
        | `crypto_transfer_authorization_pending` | — | System (on-chain confirmation) |
        | `crypto_received` | — | **Buyer** submits fiat payment proof |
        | `fiat_payment_started` | — | System reviews proof |
        | `fiat_payment_review_started` | — | System accepts/rejects proof |
        | `awaiting_fiat_received_confirmation` | `confirm_fiat_received` | **Partner** |
        | `completed` | — | — |

        **Fiat settlement and confirmation:**

        You can call `confirm-fiat-received` at any time after crypto is in escrow.
        You do not need to wait for a system notification. If you know through your
        own systems that the recipient has received the fiat payment, confirm immediately.

        Some liquidity providers also send automatic settlement confirmations when the
        recipient receives the fiat. When the order's `has_fiat_settlement_notification`
        is `true` (see the quote/order response), the order moves to `completed` on its own
        and you receive the `status=completed` webhook — no manual confirm needed. If you
        already confirmed manually, the auto-confirmation is a no-op; if the auto-confirmation
        arrives first, your manual confirm call is also a no-op. Both paths are safe. See the
        Webhooks page → "Settlement notifications" for details.

        **How to implement:**
        - Always implement a `confirm-fiat-received` call in your flow.
        - Listen for webhooks. If you receive `status=completed`, no further action needed.
        - If `next_action=confirm_fiat_received`, call `confirm-fiat-received` when you
          know fiat has been received.
        - Do not wait for `next_action` if you already have confirmation from your
          own banking or payment systems.

        **Possible statuses:** `created`, `awaiting_liquidity_provider`, `awaiting_crypto_transfer_authorization`, `crypto_transfer_authorization_pending`, `crypto_received`, `fiat_payment_started`, `awaiting_fiat_received_confirmation`, `fiat_received_confirmed`, `crypto_release_pending`, `completed`, `cancelled`, `failed`, `price_changed_requote_needed`.
      operationId: getOrder
      security:
        - ApiKeyAuth: []
      parameters:
        - name: order_id
          in: path
          required: true
          description: Order UUID returned by the initiate endpoint
          schema:
            type: string
            format: uuid
          example: "b2c3d4e5-f6a7-8901-bcde-f12345678901"
      responses:
        '200':
          description: Order retrieved successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/APIResponse'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/PartnerOrderResponse'
              example:
                success: true
                data:
                  order_id: "b2c3d4e5-f6a7-8901-bcde-f12345678901"
                  status: awaiting_crypto_transfer_authorization
                  next_action: authorize_crypto_transfer
                  allowed_actions: ["authorize-crypto-transfer", "cancel"]
                  crypto_transfer_authorization_seconds_left: 1720
                  amounts:
                    crypto_amount: "101.50"
                    fiat_amount: "152880.00"
                    crypto_currency: USDT
                    fiat_currency: NGN
                    rate: "1530.12"
                  fee_breakdown:
                    platform_fee: "1.50"
                    platform_fee_pct: 1.5
                    partner_fee: "0.00"
                    partner_fee_pct: 0
                    total_fee: "1.50"
                  timeline:
                    - status: created
                      timestamp: "2026-02-01T12:00:05Z"
                      description: "Order created"
                    - status: awaiting_crypto_transfer_authorization
                      timestamp: "2026-02-01T12:00:06Z"
                      description: "Awaiting crypto transfer authorization"
                  created_at: "2026-02-01T12:00:05Z"
                  updated_at: "2026-02-01T12:00:06Z"
        '401':
          description: Unauthorized — invalid or missing API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerErrorResponse'
        '404':
          description: Order not found or does not belong to this partner
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerErrorResponse'
              example:
                success: false
                error:
                  code: ORDER_NOT_FOUND
                  message: "Order not found"
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerErrorResponse'

  /api/v1/partner/orders/{order_id}/transfer-authorization-parameters:
    get:
      tags:
        - SDK
        - Orders
      summary: Get transfer authorization parameters
      description: |
        Returns pre-filled parameters required to build and sign the EIP-712 forward request.
        Use this endpoint first, then sign locally and send the signature to
        `/api/v1/partner/orders/{order_id}/authorize-crypto-transfer`.
      operationId: getTransferAuthorizationParameters
      security:
        - ApiKeyAuth: []
      parameters:
        - name: order_id
          in: path
          required: true
          description: Order UUID
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Transfer authorization parameters returned
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/APIResponse'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          order_id:
                            type: string
                            format: uuid
                          network_preset:
                            type: string
                          chain_id:
                            type: string
                          rpc_url:
                            type: string
                            description: JSON-RPC URL for the target chain
                          token_address:
                            type: string
                          forwarder_address:
                            type: string
                          recipient_address:
                            type: string
                            description: Escrow address to receive the crypto
                          sender_address:
                            type: string
                            description: Partner wallet address (must match the signing private key)
                          amount_atomic:
                            type: string
                          amount_human:
                            type: string
                          crypto_currency:
                            type: string
                          crypto_decimals:
                            type: integer
                          recommended_gas_limit:
                            type: string
                          recommended_ttl_seconds:
                            type: integer
                          transfer_data:
                            type: string
                            description: Pre-encoded ERC20 transfer(address,uint256) calldata (hex with 0x prefix)
                          forwarder_nonce:
                            type: string
                            description: Current forwarder nonce for the sender address (used in EIP-712 signing)
                          authorization_path:
                            type: string
        '401':
          description: Unauthorized
        '404':
          description: Order not found

  /api/v1/partner/orders/{order_id}/authorize-crypto-transfer:
    post:
      tags:
        - SDK
        - Orders
      summary: Authorize crypto transfer
      description: |
        Submit signed EIP-712 `forward_request` to authorize transfer of order crypto to escrow.

        **Security note:** Your private key is never sent to the API. The SDK uses it locally to
        construct and sign the EIP-712 ForwardRequest on your machine. Only the resulting signature
        and the unsigned forward_request fields are submitted. You can verify this by reading the
        SDK source code.

        Recommended: use SDK helper files (only 4 arguments needed):
        - Python: `https://github.com/Unigox/sdk_python/blob/main/partner_authorize_crypto_transfer_sdk.py`
        - JavaScript: `https://github.com/Unigox/sdk_javascript/blob/main/partner_authorize_crypto_transfer_sdk.js`

        The SDK automatically fetches all required parameters (chain, token, forwarder, escrow
        address, RPC URL) from the `transfer-authorization-parameters` endpoint.

        SDK helper options:
        - End-to-end one call: `authorize_crypto_transfer(...)` / `authorizeCryptoTransfer(...)`
        - Build unsigned request only: `build_forward_request(...)` / `buildForwardRequest(...)`
        - Sign existing request only: `sign_forward_request(...)` / `signForwardRequest(...)`
        - Build full request body `{forward_request, signature}`:
          `build_authorize_crypto_transfer_payload(...)` / `buildAuthorizeCryptoTransferPayload(...)`

        Manual implementation: call `GET /transfer-authorization-parameters` first, then build
        the EIP-712 typed data and sign with your partner wallet private key locally.
      operationId: authorizeCryptoTransfer
      security:
        - ApiKeyAuth: []
      parameters:
        - name: order_id
          in: path
          required: true
          description: Order UUID
          schema:
            type: string
            format: uuid
          example: "b2c3d4e5-f6a7-8901-bcde-f12345678901"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - forward_request
                - signature
              properties:
                forward_request:
                  type: object
                  description: Signed EIP-712 ForwardRequest payload
                signature:
                  type: string
                  description: Hex-encoded signature from partner wallet
            example:
              forward_request:
                from: "0x674b16054290b4f06D7Ba8e3Ac379E3B0a359a16"
                to: "0x2222222222222222222222222222222222222222"
                value: "0"
                gas: "500000"
                nonce: "0"
                deadline: "1760000000"
                data: "0xa9059cbb000000000000000000000000..."
              signature: "0xabc123..."
      responses:
        '200':
          description: Crypto authorization accepted
        '400':
          description: Bad Request
        '401':
          description: Unauthorized
        '404':
          description: Order not found
        '409':
          description: Order is not in the correct status for crypto authorization
        '422':
          description: |
            Insufficient token balance — the partner wallet does not hold enough
            tokens to fund the escrow. Top up the wallet and retry.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerErrorResponse'
              example:
                success: false
                error:
                  code: INSUFFICIENT_BALANCE
                  message: "insufficient USDC balance: wallet 0x... has 1928961 but 3000000 is required"
        '502':
          description: Transactor service error (e.g. on-chain simulation failed)

  /api/v1/partner/orders/{order_id}/confirm-fiat-received:
    post:
      tags:
        - Orders
      summary: Confirm fiat received
      description: |
        Confirm that the recipient has received the fiat payment for an off-ramp order.
        This triggers the crypto release from escrow.

        Call this endpoint as soon as you know fiat has been received. You do not need
        to wait for a system prompt. If you have confirmation from your own banking or
        payment systems, confirm immediately.

        This endpoint is idempotent. If the order already completed (via automatic
        settlement confirmation from the liquidity provider), calling this returns
        the current order status with no side effects.
      operationId: confirmFiatReceived
      security:
        - ApiKeyAuth: []
      parameters:
        - name: order_id
          in: path
          required: true
          description: Order UUID
          schema:
            type: string
            format: uuid
          example: "b2c3d4e5-f6a7-8901-bcde-f12345678901"
      responses:
        '200':
          description: Fiat receipt confirmed successfully
        '400':
          description: Bad Request
        '401':
          description: Unauthorized
        '404':
          description: Order not found

  /api/v1/partner/orders/{order_id}/cancel:
    post:
      tags:
        - Orders
      summary: Cancel order
      description: |
        Cancel an order if cancellation is allowed at the current status.
        Check `allowed_actions` on the order response — `cancel` is available when cancellation is permitted.
      operationId: cancelOrder
      security:
        - ApiKeyAuth: []
      parameters:
        - name: order_id
          in: path
          required: true
          description: Order UUID
          schema:
            type: string
            format: uuid
          example: "b2c3d4e5-f6a7-8901-bcde-f12345678901"
      responses:
        '200':
          description: Order cancelled successfully
        '400':
          description: Bad Request — cancellation not allowed at current status
        '401':
          description: Unauthorized
        '404':
          description: Order not found

  /api/v1/partner/orders:
    get:
      tags:
        - Orders
      summary: List orders
      description: |
        Get a paginated list of orders with optional filtering by status.
        Each order includes `allowed_actions` indicating which API actions are available.
      operationId: listOrders
      security:
        - ApiKeyAuth: []
      parameters:
        - name: status
          in: query
          required: false
          description: Filter by partner order status
          schema:
            type: string
            enum: [created, awaiting_liquidity_provider, awaiting_crypto_transfer_authorization, crypto_transfer_authorization_pending, crypto_received, awaiting_fiat_received_confirmation, fiat_received_confirmed, crypto_release_pending, completed, cancelled, failed, price_changed_requote_needed]
          example: "completed"
        - name: page
          in: query
          required: false
          description: Page number (default 1)
          schema:
            type: integer
            default: 1
          example: 1
        - name: limit
          in: query
          required: false
          description: Results per page (default 20, max 100)
          schema:
            type: integer
            default: 20
            maximum: 100
          example: 20
      responses:
        '200':
          description: Orders retrieved successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/APIResponse'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          orders:
                            type: array
                            items:
                              $ref: '#/components/schemas/PartnerOrderResponse'
                          pagination:
                            type: object
                            properties:
                              page:
                                type: integer
                                example: 1
                              limit:
                                type: integer
                                example: 20
                              total_count:
                                type: integer
                                example: 42
                              total_pages:
                                type: integer
                                example: 3
              example:
                success: true
                data:
                  orders:
                    - order_id: "b2c3d4e5-f6a7-8901-bcde-f12345678901"
                      status: completed
                      next_action: null
                      allowed_actions: []
                      amounts:
                        crypto_amount: "101.50"
                        fiat_amount: "152880.00"
                        crypto_currency: USDT
                        fiat_currency: NGN
                        rate: "1530.12"
                      fee_breakdown:
                        platform_fee: "1.50"
                        platform_fee_pct: 1.5
                        partner_fee: "0.00"
                        partner_fee_pct: 0
                        total_fee: "1.50"
                      timeline:
                        - status: created
                          timestamp: "2026-02-01T12:00:05Z"
                          description: "Order created"
                        - status: completed
                          timestamp: "2026-02-01T12:30:00Z"
                          description: "Order completed"
                      created_at: "2026-02-01T12:00:05Z"
                      updated_at: "2026-02-01T12:30:00Z"
                  pagination:
                    page: 1
                    limit: 20
                    total_count: 42
                    total_pages: 3
        '401':
          description: Unauthorized — invalid or missing API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerErrorResponse'

  # ============================================
  # 12. WEBHOOKS
  # ============================================
  /api/v1/partner/webhooks:
    post:
      tags:
        - Webhooks
      summary: Register webhook URL
      description: |
        Register or update the webhook URL for your partner account. The URL must use HTTPS.
        Once registered, the webhook is automatically enabled and will start receiving order status events.
      operationId: registerWebhook
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RegisterWebhookRequest'
            example:
              url: "https://partner.example.com/webhooks/unigox"
      responses:
        '200':
          description: Webhook registered successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/APIResponse'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/WebhookConfigResponse'
              example:
                success: true
                data:
                  url: "https://partner.example.com/webhooks/unigox"
                  enabled: true
        '400':
          description: Bad request — missing or invalid URL
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                url_required:
                  summary: URL is required
                  value:
                    success: false
                    data:
                      error_key: "webhook URL is required"
                url_not_https:
                  summary: URL must use HTTPS
                  value:
                    success: false
                    data:
                      error_key: "webhook URL must use HTTPS"
        '401':
          description: Unauthorized — invalid or missing API key

    get:
      tags:
        - Webhooks
      summary: Get webhook config
      description: |
        Returns the current webhook configuration for your partner account,
        including the URL and whether the webhook is enabled.
      operationId: getWebhook
      security:
        - ApiKeyAuth: []
      responses:
        '200':
          description: Webhook configuration retrieved
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/APIResponse'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/WebhookConfigResponse'
              example:
                success: true
                data:
                  url: "https://partner.example.com/webhooks/unigox"
                  enabled: true
        '401':
          description: Unauthorized — invalid or missing API key

  /api/v1/partner/webhooks/{id}:
    delete:
      tags:
        - Webhooks
      summary: Delete webhook
      description: |
        Remove the webhook URL and disable webhook delivery for your partner account.
        No further events will be sent until a new URL is registered.
      operationId: deleteWebhook
      security:
        - ApiKeyAuth: []
      parameters:
        - name: id
          in: path
          required: true
          description: Webhook ID (any value accepted — the system uses the authenticated partner context)
          schema:
            type: string
          example: "1"
      responses:
        '200':
          description: Webhook deleted successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/APIResponse'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          message:
                            type: string
                            example: "webhook deleted"
              example:
                success: true
                data:
                  message: "webhook deleted"
        '401':
          description: Unauthorized — invalid or missing API key

  /api/v1/partner/webhooks/test:
    post:
      tags:
        - Webhooks
      summary: Test webhook
      description: |
        Send a signed test event to your configured webhook URL. Uses the same HMAC-SHA256
        signature scheme as production events. Use this to verify your endpoint is reachable
        and correctly validating signatures.
      operationId: testPingWebhook
      security:
        - ApiKeyAuth: []
      responses:
        '200':
          description: Test event delivered successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/APIResponse'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/WebhookTestPingResponse'
              example:
                success: true
                data:
                  delivered: true
                  http_status: 200
        '400':
          description: Webhook URL not configured
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                data:
                  error_key: "webhook URL is not set"
        '401':
          description: Unauthorized — invalid or missing API key
        '502':
          description: Webhook delivery failed — target endpoint returned non-2xx or timed out
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                data:
                  error_key: "webhook delivery failed"

components:
  schemas:
    APIResponse:
      type: object
      required:
        - success
      properties:
        success:
          type: boolean
          description: Indicates if the request was successful
          example: true
        data:
          description: Response data (structure varies by endpoint) or error information if success is false

    ErrorResponse:
      type: object
      required:
        - success
        - data
      properties:
        success:
          type: boolean
          example: false
        data:
          type: object
          required:
            - error_key
          properties:
            error_key:
              type: string
              description: Standardized error key for client error handling
              example: "required_fields_missing"

    CryptoCurrencyCapability:
      type: object
      properties:
        code:
          type: string
          description: Cryptocurrency code (e.g., BTC, ETH)
          example: BTC
        name:
          type: string
          description: Cryptocurrency name
          example: Bitcoin
        contract:
          type: string
          description: Token contract address (for ERC-20 tokens)
          example: "0x..."
        decimals:
          type: integer
          format: int64
          description: Token decimals (number of decimal places)
          example: 8
        blockchains:
          type: array
          items:
            type: string
          description: List of supported blockchain names/slugs
          example: ["ethereum", "polygon"]

    CryptoCurrenciesResponse:
      allOf:
        - $ref: '#/components/schemas/APIResponse'
        - type: object
          properties:
            data:
              type: array
              items:
                $ref: '#/components/schemas/CryptoCurrencyCapability'

    BlockchainCapability:
      type: object
      properties:
        name:
          type: string
          description: Blockchain name
          example: Ethereum
        slug:
          type: string
          description: Blockchain slug identifier
          example: ethereum
        ticker:
          type: string
          description: Blockchain ticker symbol
          example: eth
        crypto_currencies:
          type: array
          items:
            $ref: '#/components/schemas/CryptoCurrencyCapability'
          description: List of cryptocurrencies supported on this blockchain

    BlockchainsResponse:
      allOf:
        - $ref: '#/components/schemas/APIResponse'
        - type: object
          properties:
            data:
              type: array
              items:
                $ref: '#/components/schemas/BlockchainCapability'

    FiatCurrencyCapability:
      type: object
      properties:
        code:
          type: string
          description: Fiat currency code (ISO 4217)
          example: USD
        name:
          type: string
          description: Fiat currency name
          example: United States Dollar
        institution_id:
          type: array
          items:
            type: string
          description: List of payment method slugs that support this currency (retrieved from active payment method options)
          example: ["wise", "revolut", "bank-transfer"]
        countries:
          type: array
          items:
            type: string
          description: List of ISO 2-letter country codes where this currency is supported (retrieved from active payment method options)
          example: ["US", "GB", "EU"]
        onramp:
          type: boolean
          description: |
            Whether onramp (fiat to crypto) is supported. Currently set to true if there are any active payment method options for this currency.
            This will be refined in future updates based on trade types and explicit direction configuration.
          example: true
        offramp:
          type: boolean
          description: |
            Whether offramp (crypto to fiat) is supported. Currently set to true if there are any active payment method options for this currency.
            This will be refined in future updates based on trade types and explicit direction configuration.
          example: true

    FiatCurrenciesResponse:
      allOf:
        - $ref: '#/components/schemas/APIResponse'
        - type: object
          properties:
            data:
              type: array
              items:
                $ref: '#/components/schemas/FiatCurrencyCapability'

    CorridorRateQuote:
      type: object
      description: |
        One partner-type variant of a corridor rate (P2P or Licensed).
      properties:
        rate:
          type: number
          format: double
          nullable: true
          description: All-in fiat-per-USDT rate (platform fee already applied).
          example: 1542.6
        min_notional_fiat:
          type: number
          format: double
          nullable: true
          description: Smallest amount accepted by any active offer in this variant, in the corridor's local currency. Set only when it exceeds the reference notional in dollar terms — i.e. the customer would need to size up to use this variant.
          example: 3465

    CorridorDirectionInfo:
      type: object
      description: |
        Per-direction info for one fiat corridor. The whole object is
        omitted when no active offer exists in that direction. Either
        `p2p` or `licensed` (or both) will be present.
      properties:
        supported:
          type: boolean
          description: Always `true` when the object is present (the object is omitted otherwise). Reserved for future states where a corridor may be configured but temporarily without liquidity.
          example: true
        p2p:
          $ref: '#/components/schemas/CorridorRateQuote'
        licensed:
          $ref: '#/components/schemas/CorridorRateQuote'

    Corridor:
      type: object
      properties:
        code:
          type: string
          example: NGN
        name:
          type: string
          example: Nigerian Naira
        mid_market_rate:
          type: number
          format: double
          nullable: true
          description: Open Exchange Rates reference rate (fiat per USDT) for this currency. Same value applies to both directions.
          example: 1358.97
        mid_market_updated_at:
          type: string
          format: date-time
          nullable: true
          example: '2026-04-27T15:56:11Z'
        unigox_fee_pct:
          type: number
          format: double
          nullable: true
          description: Cheapest active Unigox platform fee for this corridor, in percent. Already baked into every `rate` below.
          example: 0.25
        onramp:
          $ref: '#/components/schemas/CorridorDirectionInfo'
        offramp:
          $ref: '#/components/schemas/CorridorDirectionInfo'

    SupportedCorridorsPayload:
      type: object
      properties:
        notional_usdt:
          type: number
          format: double
          example: 500
        generated_at:
          type: string
          format: date-time
          example: '2026-04-27T15:56:11Z'
        currencies:
          type: array
          items:
            $ref: '#/components/schemas/Corridor'

    SupportedCorridorsResponse:
      allOf:
        - $ref: '#/components/schemas/APIResponse'
        - type: object
          properties:
            data:
              $ref: '#/components/schemas/SupportedCorridorsPayload'

    PaymentField:
      type: object
      properties:
        key:
          type: string
          description: Field identifier/key
          example: vpa
        label:
          type: string
          description: Human-readable field label
          example: UPI ID
        type:
          type: string
          description: Field input type (text, number, select, etc.)
          example: text
        required:
          type: boolean
          description: Whether this field is required
          example: true
        pattern:
          type: string
          description: Regex pattern for validation (optional)
          example: "^[\\w.-]+@[\\w.-]+$"
        placeholder:
          type: string
          description: Placeholder text for input (optional)
          example: "username@upi"
        lookup:
          type: boolean
          description: Whether field can be used for lookup (optional)
          example: true
        options:
          type: array
          description: Options for select fields (optional)
          items:
            type: object
            properties:
              code:
                type: string
                example: CBU
              name:
                type: string
                example: "CBU (Traditional Bank)"

    Rails:
      type: object
      properties:
        slug:
          type: string
          description: Payment rail slug identifier
          example: upi-india
        name:
          type: string
          description: Payment rail name
          example: UPI India
        institution_required:
          type: boolean
          description: Whether institution (bank) selection is required by the rail itself. In the current partner API, omission of `institution_id` is explicitly supported for `iban-sepa`, `nip-nigeria`, and `imps-neft-india` (which returns `false` and is routed by `ifsc_code` instead).
          example: false
        institution_lookup_key:
          type: string
          nullable: true
          description: Lookup key for institution search, e.g., ifsc or sort_code
          example: ifsc
        currencies:
          type: array
          items:
            type: string
          description: List of supported fiat currency codes
          example: ["INR"]
        settlement:
          type: object
          nullable: true
          description: Settlement information
          properties:
            speed:
              type: string
              enum: [INSTANT, SAME_DAY, NEXT_DAY, STANDARD]
              description: Settlement speed
              example: INSTANT
            typical_minutes:
              type: integer
              description: Typical settlement time in minutes
              example: 2
            note:
              type: string
              nullable: true
              description: Optional note about settlement
        limits:
          type: object
          nullable: true
          description: Transaction limits
          properties:
            min_amount:
              type: number
              nullable: true
              description: Minimum transaction amount
            max_amount:
              type: number
              nullable: true
              description: Maximum transaction amount (null = no limit)
            max_per_transaction:
              type: number
              nullable: true
              description: Maximum per transaction (null = no limit)
        fields:
          type: array
          items:
            $ref: '#/components/schemas/PaymentField'
          description: >
            Required fields for payment details input. For a multi-format rail (see
            `formats`) this is a single best-effort field set kept for backward
            compatibility; the authoritative, institution-specific fields are in `formats`.
        formats:
          type: array
          description: >
            Present only when a rail accepts more than one field-set (e.g. GHIPSS and
            EthSwitch accept a mobile-money format keyed on `phone_number` and a bank
            format keyed on `account_number`). The required field set depends on the
            chosen institution's type, so pick an institution from
            `/api/v1/supported/institutions`, match its `institution_type` to a
            format via `institution_types`, and submit that format's `fields`.
          items:
            $ref: '#/components/schemas/RailFormat'

    RailFormat:
      type: object
      properties:
        id:
          type: string
          description: Format identifier
          example: mobile-money
        name:
          type: string
          description: Human-readable format name
          example: Mobile Money
        institution_types:
          type: array
          items:
            type: string
          description: Institution types this format applies to. Match the chosen institution's type to select the format.
          example: ["mobile-money", "mobile-wallets"]
        has_liquidity:
          type: boolean
          description: >
            Whether a vendor/provider can currently settle this format in the corridor.
            Omitted when liquidity could not be determined (treat as unknown, not "no").
            This reflects configured liquidity; live availability is confirmed at quote time.
          example: true
        fields:
          type: array
          items:
            $ref: '#/components/schemas/PaymentField'
          description: Required fields for payment details input when paying out to an institution of this format's type.

    PaymentRailsResponse:
      allOf:
        - $ref: '#/components/schemas/APIResponse'
        - type: object
          properties:
            data:
              type: object
              required:
                - country
                - direction
                - rails
              properties:
                country:
                  type: string
                  description: Country code used in the query
                  example: NG
                currency:
                  type: string
                  description: Currency code (from query parameter or first rail)
                  example: NGN
                direction:
                  type: string
                  enum: [onramp, offramp]
                  description: Transaction direction
                  example: offramp
                rails:
                  type: array
                  items:
                    $ref: '#/components/schemas/Rails'
                  description: List of available payment rails
                links:
                  type: object
                  additionalProperties:
                    type: string
                  description: Related API endpoints
                  example:
                    search_institutions: "/api/v1/supported/institutions"

    InstitutionBranch:
      type: object
      nullable: true
      properties:
        code:
          type: string
          description: Branch code (e.g., IFSC, sort code)
          example: HDFC0001234
        name:
          type: string
          description: Branch name
          example: Mumbai Main Branch
        address:
          type: string
          description: Branch address
          example: "123 Financial Street, Mumbai 400001"

    Institution:
      type: object
      properties:
        id:
          type: string
          description: Institution identifier (slug) — pass this as `institution_id` when creating payment details
          example: hdfc-bank
        name:
          type: string
          description: Institution name
          example: HDFC Bank
        type:
          type: string
          description: Broad institution category (e.g., BANK, WALLET)
          example: BANK
        institution_type:
          type: string
          description: >-
            Fine-grained institution type slug (e.g. `mobile-money`,
            `mobile-wallets`, `traditional-banks`, `digital-banks`). On a
            multi-format rail, match this to the rail's
            `formats[].institution_types` to select the right field set.
          example: mobile-wallets
        code:
          type: string
          description: >-
            Corridor-specific institution code: the 4-letter IFSC bank prefix
            for `imps-neft-india`, the NIBSS code for `nip-nigeria`, etc.
            Omitted when the institution has no code (e.g. SEPA banks).
          example: HDFC
        branch:
          $ref: '#/components/schemas/InstitutionBranch'
        supported_rails:
          type: array
          items:
            type: string
          description: List of payment rail slugs this institution supports
          example: ["imps-neft-india"]

    Pagination:
      type: object
      properties:
        total:
          type: integer
          description: Total number of items
          example: 1
        limit:
          type: integer
          description: Items per page
          example: 20
        offset:
          type: integer
          description: Current offset
          example: 0

    InstitutionsResponse:
      allOf:
        - $ref: '#/components/schemas/APIResponse'
        - type: object
          properties:
            data:
              type: object
              required:
                - institutions
                - pagination
              properties:
                institutions:
                  type: array
                  items:
                    $ref: '#/components/schemas/Institution'
                pagination:
                  $ref: '#/components/schemas/Pagination'

    SupportedPair:
      type: object
      properties:
        crypto_currency_code:
          type: string
          description: Cryptocurrency code (e.g., USDC, BTC)
          example: USDC
        fiat_currency_code:
          type: string
          description: Fiat currency code (e.g., USD, EUR)
          example: USD
        type:
          type: string
          enum: [BUY, SELL]
          description: Trading pair type - BUY (fiat to crypto) or SELL (crypto to fiat)
          example: BUY

    SupportedPairsResponse:
      allOf:
        - $ref: '#/components/schemas/APIResponse'
        - type: object
          properties:
            data:
              type: array
              items:
                $ref: '#/components/schemas/SupportedPair'

    CreateUserResponse:
      allOf:
        - $ref: '#/components/schemas/APIResponse'
        - type: object
          properties:
            data:
              type: object
              properties:
                user_uuid:
                  type: string
                  format: uuid
                  description: Public UUID of the user. Use this value in all subsequent API calls.
                  example: "550e8400-e29b-41d4-a716-446655440000"
                email:
                  type: string
                  format: email
                  example: user@example.com
                kyc_status:
                  type: string
                  enum: [NOT_INITIATED, IN_PROGRESS, VERIFIED, VERIFICATION_REJECTED]
                  example: NOT_INITIATED
                created_at:
                  type: string
                  format: date-time
                  example: "2026-01-06T14:30:00Z"

    GetUserResponse:
      allOf:
        - $ref: '#/components/schemas/APIResponse'
        - type: object
          properties:
            data:
              type: object
              properties:
                user_uuid:
                  type: string
                  format: uuid
                  description: Public UUID of the user. Use this value in all subsequent API calls.
                  example: "550e8400-e29b-41d4-a716-446655440000"
                email:
                  type: string
                  format: email
                  example: user@example.com
                created_at:
                  type: string
                  format: date-time
                  example: "2026-01-06T14:30:00Z"
                kyc:
                  type: object
                  properties:
                    status:
                      type: string
                      enum: [NOT_INITIATED, IN_PROGRESS, VERIFIED, VERIFICATION_REJECTED]
                      example: IN_PROGRESS
                    kyc_method:
                      type: string
                      enum: [direct_data, external_token, handoff]
                      description: KYC submission method used
                      example: direct_data
                    first_name:
                      type: string
                      description: User's first name from KYC data
                      example: John
                    last_name:
                      type: string
                      description: User's last name from KYC data
                      example: Doe
                    country_code:
                      type: string
                      description: User's country code (ISO 2-letter) from KYC data
                      example: NG
                    rejection_reason:
                      type: string
                      nullable: true
                      description: Reason for KYC rejection (if status is VERIFICATION_REJECTED)
                      example: "Verification declined by AiPrise"
                    updated_at:
                      type: string
                      format: date-time
                      description: Last update timestamp for KYC status
                      example: "2026-01-06T14:30:00Z"
                payment_profiles:
                  type: array
                  items:
                    type: object
                    properties:
                      id:
                        type: string
                        example: "987"
                      rail:
                        type: string
                        example: nip-nigeria
                      currency:
                        type: string
                        example: NGN
                      institution_name:
                        type: string
                        nullable: true
                        example: Guaranty Trust Bank
                      institution_id:
                        type: string
                        nullable: true
                        description: Institution identifier (our internal slug)
                        example: guaranty-trust-bank
                      details:
                        type: object
                        additionalProperties: true
                        example:
                          account_number: "0123456789"

    KYCSubmitDirectData:
      type: object
      required: [method, data]
      properties:
        method:
          type: string
          enum: [direct_data]
          example: direct_data
        data:
          type: object
          required: [pii]
          properties:
            pii:
              type: object
              required: [first_name, last_name, country_code]
              properties:
                first_name:
                  type: string
                  example: John
                last_name:
                  type: string
                  example: Doe
                country_code:
                  type: string
                  example: NG
                dob:
                  type: string
                  format: date
                  description: >-
                    Date of birth (YYYY-MM-DD). Optional, but when provided it must be a real
                    date in the past and indicate an age of at least 18. A future, malformed,
                    or under-18 date is rejected with HTTP 400 (see error responses below).
                  example: "1990-01-01"
                middle_name:
                  type: string
                  example: Michael
                phone_number:
                  type: string
                  description: E.164 format
                  example: "+2348012345678"
                id_number:
                  type: string
                  example: "A12345678"
                id_type:
                  type: string
                  enum: [NATIONAL_ID, DRIVER_LICENSE, PASSPORT]
                  example: NATIONAL_ID
                address:
                  type: string
                  description: Street address
                  example: "Svitrigailos g 29"
                city:
                  type: string
                  description: City. Required for EUR/AUD/GBP offramp (auto-populates into payment details).
                  example: "Vilnius"
                postal_code:
                  type: string
                  example: "03229"

    KYCSubmitExternalToken:
      type: object
      required: [method, data]
      properties:
        method:
          type: string
          enum: [external_token]
          example: external_token
        data:
          type: object
          required: [provider]
          description: |
            Provider-specific fields:
            - SumSub: requires `token` (SumSub share token)
            - Persona: requires `inquiry_id` (Persona inquiry ID, e.g. inq_xxx)
          properties:
            provider:
              type: string
              enum: [sumsub, persona]
              description: KYC provider name
              example: persona
            token:
              type: string
              description: SumSub share token (required when provider=sumsub)
              example: "sumsub_external_session_id"
            inquiry_id:
              type: string
              description: Persona inquiry ID (required when provider=persona). This is the inq_xxx identifier returned by Persona when the inquiry was created.
              example: "inq_CWQzc5afzD4dmF66EJqjdPC2T79C"

    KYCSubmitHandoff:
      type: object
      required: [method, data]
      properties:
        method:
          type: string
          enum: [handoff]
          example: handoff
        data:
          type: object
          properties:
            pii:
              type: object
              properties:
                first_name:
                  type: string
                  example: John
                last_name:
                  type: string
                  example: Doe
                country_code:
                  type: string
                  example: NG

    KYCSubmitResponse:
      allOf:
        - $ref: '#/components/schemas/APIResponse'
        - type: object
          properties:
            data:
              type: object
              properties:
                kyc_status:
                  type: string
                  enum: [NOT_INITIATED, IN_PROGRESS, VERIFIED, VERIFICATION_REJECTED]
                  example: IN_PROGRESS
                message:
                  type: string
                  example: "PII data accepted"
                verification_url:
                  type: string
                  format: uri
                  nullable: true
                  description: Verification URL (only returned for handoff method)
                  example: "https://verify.aiprise.com/?verification_session_id=4fd410c9-ba9b-46cf-a4f0-132204a9a564"

    VerificationStatusResponse:
      allOf:
        - $ref: '#/components/schemas/APIResponse'
        - type: object
          properties:
            data:
              type: object
              properties:
                status:
                  type: string
                  enum: [NOT_STARTED, PENDING, IN_PROGRESS, COMPLETED, VERIFIED, FAILED, VERIFICATION_REJECTED]
                  description: |
                    Current verification status.

                    Recommended partner interpretation:
                    - `NOT_STARTED`, `PENDING`, `IN_PROGRESS` — still processing
                    - `VERIFIED` — approved / successful verification
                    - `VERIFICATION_REJECTED` — completed with rejection
                    - `FAILED` — terminal error
                    - `COMPLETED` — provider workflow completed, but partners should not treat this alone as approval; wait for `VERIFIED` to consider the user approved
                  example: "IN_PROGRESS"
                verification_url:
                  type: string
                  format: uri
                  nullable: true
                  description: Verification URL (for handoff method)
                  example: "https://verify-sandbox.aiprise.com/?verification_session_id=abc123"
                verification_seconds_left:
                  type: integer
                  nullable: true
                  description: Seconds remaining for verification session (if applicable)
                  example: 3600
                provider_messages:
                  type: array
                  items:
                    type: string
                  nullable: true
                  description: Provider-specific messages or rejection reasons
                  example: ["Verification in progress"]

    CreatePaymentDetailsRequest:
      description: Conditional request schema for partner payment details. `institution_id` may be omitted for `iban-sepa`, `nip-nigeria`, and `imps-neft-india`.
      oneOf:
        - $ref: '#/components/schemas/CreatePaymentDetailsRequestInstitutionOptional'
        - $ref: '#/components/schemas/CreatePaymentDetailsRequestInstitutionRequired'

    CreatePaymentDetailsRequestBase:
      type: object
      required: [currency, rail, details]
      properties:
        currency:
          type: string
          description: ISO 3-letter fiat currency code (e.g., INR, EUR, USD)
          example: INR
        rail:
          type: string
          description: Payment rail identifier (e.g., "upi-india", "iban-sepa", "imps-neft-india")
          example: iban-sepa
        details:
          type: object
          description: Payment details object with fields specific to the payment network (e.g., account_number, vpa, iban, full_name)
          additionalProperties: true
          example:
            full_name: "John Doe"
            iban: DE89370400440532013000
        country_code:
          type: string
          description: Optional ISO 2-letter country code
          example: DE

    CreatePaymentDetailsRequestInstitutionOptional:
      allOf:
        - $ref: '#/components/schemas/CreatePaymentDetailsRequestBase'
        - type: object
          properties:
            rail:
              type: string
              enum: [iban-sepa, nip-nigeria, imps-neft-india]
              description: Payment rail identifier for rails that support omission of `institution_id`
              example: iban-sepa
            institution_id:
              type: string
              description: |
                Optional for `iban-sepa`, `nip-nigeria`, and `imps-neft-india`.
                For `iban-sepa`/`nip-nigeria`, if omitted the backend assigns the generic `other-bank` payment method automatically.
                For `imps-neft-india` it is ignored entirely (resolves to the generic `imps-or-neft-transfer` method); the bank is taken from `details.bank_name` + `details.ifsc_code`.
              example: other-bank

    CreatePaymentDetailsRequestInstitutionRequired:
      allOf:
        - $ref: '#/components/schemas/CreatePaymentDetailsRequestBase'
        - type: object
          required: [institution_id]
          properties:
            rail:
              type: string
              description: Payment rail identifier for rails that require institution selection
              not:
                enum: [iban-sepa, nip-nigeria, imps-neft-india]
              example: cvu-cbu
            institution_id:
              type: string
              description: |
                Institution (payment method) identifier - our internal slug (e.g., "wise", "hdfc-bank", "revolut", "uala", "banco-santander-argentina").
                Required for all rails except `iban-sepa`, `nip-nigeria`, and `imps-neft-india`.
                If omitted for any other rail, the API returns a validation error.
                For rails with multiple formats (e.g., CVU/CBU), the format is automatically determined based on the payment method type of this institution.
              example: wise

    PaymentProfile:
      type: object
      properties:
        payment_details_id:
          type: string
          description: Payment details ID. Use this value in offramp/onramp quote requests.
          example: "987"
        rail:
          type: string
          description: Payment rail identifier
          example: nip-nigeria
        currency:
          type: string
          description: Fiat currency code
          example: NGN
        institution_name:
          type: string
          nullable: true
          description: Institution (bank) name if applicable
          example: Guaranty Trust Bank
        institution_id:
          type: string
          nullable: true
          description: Institution identifier (our internal slug)
          example: guaranty-trust-bank
        details:
          type: object
          description: Payment details (account number, VPA, IBAN, etc.)
          additionalProperties: true
          example:
            account_number: "0123456789"

    CreatePaymentDetailsResponse:
      allOf:
        - $ref: '#/components/schemas/APIResponse'
        - type: object
          properties:
            data:
              $ref: '#/components/schemas/PaymentProfile'

    GetPaymentDetailResponse:
      allOf:
        - $ref: '#/components/schemas/APIResponse'
        - type: object
          properties:
            data:
              $ref: '#/components/schemas/PaymentProfile'

    GetPaymentDetailsResponse:
      allOf:
        - $ref: '#/components/schemas/APIResponse'
        - type: object
          properties:
            data:
              type: array
              items:
                $ref: '#/components/schemas/PaymentProfile'
              description: List of all payment profiles for the user

    # ============================================
    # Off-Ramp & Order Schemas
    # ============================================
    SlippageTolerance:
      type: object
      properties:
        type:
          type: string
          enum: [percent, amount, fiat]
          description: |
            Tolerance type:
            - `percent` — percentage of the quoted rate (e.g., 1.5 = 1.5%)
            - `amount` — absolute fiat amount (e.g., 50.00 = 50 NGN)
            - `fiat` — alias of `amount`
          example: percent
        value:
          type: number
          format: double
          description: Tolerance value (percentage or fiat amount depending on type)
          example: 1.5

    LiquidityPair:
      type: object
      required:
        - crypto_currency_code
        - fiat_currency_code
        - trade_type
        - min_amount
        - max_amount
        - min_amount_usd
        - max_amount_usd
        - payment_methods
      properties:
        crypto_currency_code:
          type: string
          description: Cryptocurrency code (e.g. USDC, BTC)
          example: USDC
        fiat_currency_code:
          type: string
          description: Fiat currency code (ISO 4217)
          example: EUR
        trade_type:
          type: string
          enum: [BUY, SELL]
          description: |
            Partner-facing trade direction.
            `SELL` — partner sells crypto and receives fiat (off-ramp).
            `BUY` — partner buys crypto by sending fiat (on-ramp).
          example: SELL
        min_amount:
          type: number
          format: double
          description: Minimum fiat amount accepted across all eligible offers, in the pair's native fiat currency
          example: 50.00
        max_amount:
          type: number
          format: double
          description: Maximum fiat amount accepted across all eligible offers, in the pair's native fiat currency
          example: 10000.00
        min_amount_usd:
          type: number
          format: double
          description: Minimum fiat amount converted to USD
          example: 54.35
        max_amount_usd:
          type: number
          format: double
          description: Maximum fiat amount converted to USD
          example: 10870.00
        payment_methods:
          type: array
          items:
            type: string
          description: Deduplicated, alphabetically sorted list of payment method slugs available for this pair
          example:
            - sepa
            - wise

    PartnerPriceEstimateRequest:
      type: object
      required: [crypto_currency, fiat_currency]
      oneOf:
        - required: [crypto_amount]
        - required: [fiat_amount]
      properties:
        crypto_currency:
          type: string
          description: Cryptocurrency code (e.g., USDT, USDC)
          example: USDT
        fiat_currency:
          type: string
          description: Target fiat currency code (e.g., NGN, USD, EUR)
          example: NGN
        crypto_amount:
          type: string
          description: Crypto amount to estimate from. Provide exactly one of crypto_amount or fiat_amount.
          example: "100.00"
        fiat_amount:
          type: string
          description: Fiat amount to estimate from. Provide exactly one of crypto_amount or fiat_amount.
          example: "150000.00"
        payment_method_slug:
          type: string
          description: Optional payment method slug filter to narrow routing
          example: access-bank
        payment_network_slug:
          type: string
          description: Optional payment network slug filter to narrow routing
          example: nibss
        provider_scope:
          type: string
          description: Optional provider scope filter; defaults to all
          enum: [all, licensed_only, p2p_only]
          example: all
        country_code:
          type: string
          description: Optional ISO country code (alpha-2) used for fee/routing context
          example: NG

    PartnerPriceEstimateResponse:
      type: object
      properties:
        rate:
          type: string
          description: Indicative crypto-to-fiat exchange rate
          example: "1530.12"
        crypto_amount:
          type: string
          description: Estimated total crypto amount (includes fees)
          example: "101.50"
        fiat_amount:
          type: string
          description: Estimated fiat amount
          example: "152880.00"
        fee_breakdown:
          $ref: '#/components/schemas/FeeBreakdown'
        payment_method_slug:
          type: string
          description: Matched payment method slug used for estimate
          example: access-bank
        payment_network_slug:
          type: string
          description: Matched payment network slug used for estimate
          example: nibss
        provider_scope:
          type: string
          description: Applied provider scope
          example: all
        is_indicative:
          type: boolean
          description: True when response is an estimate and not a reserving quote
          example: true

    PartnerQuoteRequest:
      type: object
      required: [user_uuid, payment_details_id, crypto_currency, fiat_currency]
      oneOf:
        - required: [crypto_amount]
        - required: [fiat_amount]
      properties:
        user_uuid:
          type: string
          format: uuid
          description: Public UUID of the end-user (returned by `POST /partner/users` as `id`)
          example: "550e8400-e29b-41d4-a716-446655440000"
        payment_details_id:
          type: integer
          format: int64
          description: ID of a previously created payment profile for the user
          example: 987
        crypto_currency:
          type: string
          description: Cryptocurrency code (e.g., USDT, USDC)
          example: USDT
        crypto_amount:
          type: string
          description: |
            Amount of crypto to sell (standard units, e.g., "100.00" for 100 USDT).
            Provide exactly one of `crypto_amount` or `fiat_amount`.
            When provided, the order is **crypto-anchored** — exact crypto cost is guaranteed, fiat payout varies by rate at settlement.
          example: "100.00"
        fiat_amount:
          type: string
          description: |
            Exact fiat amount the recipient will receive (e.g., "500000" for 500,000 NGN).
            Provide exactly one of `crypto_amount` or `fiat_amount`.
            When provided, the order is **fiat-anchored** — exact fiat delivery is guaranteed, crypto cost may vary within a 1.20× safety bound.
            For integer-minor-unit currencies (NGN, UGX, TZS, KES, GHS), must be a whole number.
          example: "500000"
        fiat_currency:
          type: string
          description: Target fiat currency code (e.g., NGN, USD, EUR)
          example: NGN
        rail:
          type: string
          description: Optional rail hint; must be compatible with payment details if provided
          example: Bank Transfer
        route:
          type: string
          description: Optional route hint; must be compatible with payment details if provided
          example: Instant Transfer
        provider_scope:
          type: string
          description: Optional provider scope filter; defaults to all
          enum: [all, licensed_only, p2p_only]
          example: all
        slippage_tolerance:
          allOf:
            - $ref: '#/components/schemas/SlippageTolerance'
          description: |
            Optional. Maximum tolerated price movement between `/quote` and `/initiate`,
            and between `/initiate` and vendor acceptance. When omitted, the default is
            `percent` / `0` — any rate movement returns `PRICE_CHANGED_REQUOTE` and the
            partner must request a fresh quote. Set this to opt in to silent execution
            for small market movements.
        has_fiat_settlement_notification:
          type: boolean
          description: |
            When `true`, only match liquidity providers that send fiat settlement
            notifications. Orders from these providers complete automatically when
            fiat is delivered. When omitted or `false`, all providers are eligible.

    PartnerQuoteResponse:
      type: object
      properties:
        quote_id:
          type: string
          format: uuid
          description: Unique quote identifier — use this to initiate the order
          example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
        anchor_type:
          type: string
          enum: [crypto, fiat]
          description: |
            Indicates which amount is guaranteed at settlement.
            - `crypto` — `crypto_amount` is exact; `fiat_amount` may vary by the rate at settlement.
            - `fiat` — `fiat_amount` is exact; `crypto_amount` may vary within a 1.20× safety bound.
          example: "crypto"
        rate:
          type: string
          description: Crypto-to-fiat exchange rate at quote time
          example: "1530.12"
        crypto_amount:
          type: string
          description: Total crypto amount the user sends (includes all fees)
          example: "101.50"
        fiat_amount:
          type: string
          description: Fiat amount the recipient receives
          example: "152880.00"
        fee_breakdown:
          $ref: '#/components/schemas/FeeBreakdown'
        payment_details_id:
          type: integer
          format: int64
          description: Payment details ID used for this quote
          example: 987
        payment_method_id:
          type: integer
          format: int64
          description: Matched payment method ID
          example: 5
        payment_network_id:
          type: integer
          format: int64
          description: Matched payment network ID
          example: 12
        expires_at:
          type: string
          format: date-time
          description: Quote expiration timestamp (30 seconds from creation)
          example: "2026-02-01T12:00:30Z"
        has_fiat_settlement_notification:
          type: boolean
          description: |
            Whether the matched liquidity provider sends fiat settlement notifications.
            When `true`, the order will complete automatically when fiat is delivered
            to the recipient. When `false`, you must call `confirm-fiat-received`.
            You can call `confirm-fiat-received` at any time regardless of this value.
          example: true

    PartnerInitiateRequest:
      type: object
      required: [quote_id]
      properties:
        quote_id:
          type: string
          format: uuid
          description: Quote ID from the quote endpoint. Payment details are taken from the quote.
          example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
        reference_id:
          type: string
          description: Optional partner-supplied external reference ID (e.g. your internal order ID). Stored on the order and echoed back in responses.
          example: "your-internal-order-id"

    PartnerInitiateResponse:
      type: object
      properties:
        order_id:
          type: string
          format: uuid
          description: Unique order identifier for tracking
          example: "b2c3d4e5-f6a7-8901-bcde-f12345678901"
        status:
          $ref: '#/components/schemas/PartnerOrderStatus'
        quote_id:
          type: string
          format: uuid
          description: Quote ID that was consumed
          example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
        reference_id:
          type: string
          nullable: true
          description: Partner-supplied external reference ID (echoed from initiate request)
          example: "your-internal-order-id"
        order_type:
          type: string
          enum: [offramp, onramp]
          description: Direction of the order
          example: offramp
        amounts:
          $ref: '#/components/schemas/PartnerOrderAmounts'
        fee_breakdown:
          $ref: '#/components/schemas/FeeBreakdown'
        timeline:
          type: array
          items:
            $ref: '#/components/schemas/TimelineEntry'
        created_at:
          type: string
          format: date-time
          example: "2026-02-01T12:00:05Z"

    PartnerOrderResponse:
      type: object
      properties:
        order_id:
          type: string
          format: uuid
          description: Unique order identifier
          example: "b2c3d4e5-f6a7-8901-bcde-f12345678901"
        status:
          $ref: '#/components/schemas/PartnerOrderStatus'
        next_action:
          type: string
          nullable: true
          description: |
            The action the partner **must** perform to advance the order. `null` means
            no action is required from the partner at this stage — the order is waiting
            for the counterparty or for an automated system step.

            Possible values:
            - `authorize_crypto_transfer` — partner must authorize escrow funding
            - `confirm_fiat_received` — partner must confirm they received the fiat payment
            - `request_new_quote` — price changed, partner must request a new quote
            - `update_kyc_address` — address fields are required for EUR/AUD/GBP payouts; see `action_required` for the endpoint and fields to submit
            - `null` — no required action (waiting for buyer or system)
          enum: [authorize_crypto_transfer, confirm_fiat_received, request_new_quote, update_kyc_address, null]
          example: authorize_crypto_transfer
        action_required:
          type: object
          nullable: true
          description: |
            Present when the order needs partner intervention before it can proceed.
            Contains machine-readable type, human-readable message, the API endpoint to call,
            and the fields to provide. Null/omitted when no action is needed.
          required: [type, message]
          properties:
            type:
              type: string
              description: Machine-readable action identifier
              example: update_kyc_address
            message:
              type: string
              description: Human-readable explanation of what is needed and why
              example: "EUR/AUD/GBP bank transfers require the recipient's physical address. Update the user's KYC before the order can proceed."
            endpoint:
              type: string
              description: API endpoint to call to resolve this action
              example: "PATCH /api/v1/partner/users/{user_uuid}/kyc"
            fields:
              type: array
              description: Fields the partner needs to provide
              items:
                type: object
                required: [field, label]
                properties:
                  field:
                    type: string
                    description: API field name
                    example: city
                  label:
                    type: string
                    description: Human-readable description of the field
                    example: "City of the recipient"
        allowed_actions:
          type: array
          items:
            type: string
          description: |
            List of API actions the partner **can** perform at the current status. An action
            may be present here even when `next_action` is `null` — meaning it is optional.

            Notable case: `confirm-fiat-received` appears at both `crypto_received` and
            `awaiting_fiat_received_confirmation`. At `crypto_received` it is a **fast-track**:
            if you already know the fiat has arrived via your own systems, you can confirm
            immediately. Otherwise, wait for the buyer's proof flow to complete — the order
            will reach `awaiting_fiat_received_confirmation` automatically.

            Status-to-actions mapping:
            - `created` → `["cancel"]`
            - `awaiting_liquidity_provider` → `["cancel"]`
            - `awaiting_crypto_transfer_authorization` → `["authorize-crypto-transfer", "cancel"]`
            - `crypto_transfer_authorization_pending` → `["cancel"]`
            - `crypto_received` → `["confirm-fiat-received"]` (optional fast-track)
            - `fiat_payment_started` → `["confirm-fiat-received"]` (optional fast-track after buyer proof submission)
            - `awaiting_fiat_received_confirmation` → `["confirm-fiat-received"]` (required)
            - `fiat_received_confirmed`, `crypto_release_pending`, `completed`, `cancelled`, `failed` → `[]`
          example: ["authorize-crypto-transfer", "cancel"]
        crypto_transfer_authorization_seconds_left:
          type: integer
          format: int64
          nullable: true
          description: |
            Remaining seconds to authorize crypto transfer while order is in
            `awaiting_crypto_transfer_authorization` / `crypto_transfer_authorization_pending`.
            `null` for all other statuses.
          example: 1720
        amounts:
          $ref: '#/components/schemas/PartnerOrderAmounts'
        fee_breakdown:
          $ref: '#/components/schemas/FeeBreakdown'
        timeline:
          type: array
          items:
            $ref: '#/components/schemas/TimelineEntry'
          description: Chronological list of status changes
        has_fiat_settlement_notification:
          type: boolean
          description: |
            Whether the matched liquidity provider sends fiat settlement notifications.
            When `true`, the order will complete automatically when fiat is delivered.
            When `false`, you must call `confirm-fiat-received`. You can call
            `confirm-fiat-received` at any time regardless of this value.
          example: true
        created_at:
          type: string
          format: date-time
          example: "2026-02-01T12:00:05Z"
        updated_at:
          type: string
          format: date-time
          example: "2026-02-01T12:00:06Z"
        order_type:
          type: string
          nullable: true
          description: |
            Order type. Present for on-ramp orders. Absent for off-ramp orders.
            - `onramp` — buy crypto with fiat
          example: onramp
        vendor_payment_details:
          type: object
          nullable: true
          description: |
            Vendor's payment details for the end-user to send fiat to.
            Present only for on-ramp orders at `awaiting_fiat_transfer` status and beyond.
          properties:
            payment_method_name:
              type: string
              example: "Agribank"
            payment_network_name:
              type: string
              example: "Napas Vietnam"
            details:
              type: object
              description: Payment method-specific fields (e.g. account_number, phone)
        payment_request:
          type: boolean
          nullable: true
          description: |
            `true` if the vendor uses payment request flow — the partner must call
            `submit-payer-details` before `confirm-payment-sent`.
          example: false
        send_out_tx_hash:
          type: string
          nullable: true
          description: |
            Destination-chain transaction hash after a send-out completes.
            Present only for on-ramp orders with `send_out_completed` status.
          example: "0xd413bf3b8fe3b1d86b1e6d4c40a82eb8c94824202ff9a010161301400ab1552d"

    PartnerOrderAmounts:
      type: object
      properties:
        crypto_amount:
          type: string
          description: Total crypto amount (standard units)
          example: "101.50"
        fiat_amount:
          type: string
          description: Fiat amount the user receives
          example: "152880.00"
        crypto_currency:
          type: string
          description: Cryptocurrency code
          example: USDT
        fiat_currency:
          type: string
          description: Fiat currency code
          example: NGN
        rate:
          type: string
          description: Crypto-to-fiat exchange rate
          example: "1530.12"

    FeeBreakdown:
      type: object
      description: Transparent breakdown of all fee components (amounts in crypto)
      properties:
        platform_fee:
          type: string
          description: Platform fee in crypto
          example: "1.50"
        platform_fee_pct:
          type: number
          format: double
          description: Platform fee as a percentage
          example: 1.5
        partner_fee:
          type: string
          description: Partner's fee markup in crypto (0 until partner fee dashboard is available)
          example: "0.00"
        partner_fee_pct:
          type: number
          format: double
          description: Partner's fee percentage (0 until partner fee dashboard is available)
          example: 0
        total_fee:
          type: string
          description: Total fee in crypto (platform_fee + partner_fee)
          example: "1.50"

    PartnerOrderStatus:
      type: string
      description: |
        Order status.

        **Off-ramp statuses (crypto → fiat):**
        - `created` — order created, quote locked
        - `awaiting_liquidity_provider` — order initiated, waiting for vendor acceptance
        - `awaiting_crypto_transfer_authorization` — partner must authorize crypto transfer via EIP-712 signature
        - `crypto_transfer_authorization_pending` — authorization submitted, awaiting on-chain confirmation
        - `crypto_received` — crypto locked in escrow
        - `fiat_payment_started` — buyer submitted fiat payment proof
        - `awaiting_fiat_received_confirmation` — waiting for partner to confirm fiat receipt
        - `fiat_received_confirmed` — fiat confirmed, crypto release pending
        - `crypto_release_pending` — escrow release in progress
        - `completed` — order fully completed
        - `cancelled` — order cancelled
        - `failed` — terminal error
        - `price_changed_requote_needed` — slippage exceeded at initiation (not a trade status)

        **On-ramp statuses (fiat → crypto):**
        - `awaiting_liquidity_provider` — order initiated, waiting for vendor acceptance
        - `awaiting_vendor_escrow_funding` — vendor accepted, funding escrow with crypto
        - `awaiting_fiat_transfer` — crypto locked in escrow, partner must confirm payment sent
        - `fiat_transfer_pending` — fiat payment confirmed, awaiting vendor confirmation of receipt
        - `completed` — vendor confirmed fiat received, crypto released to end-user wallet
        - `cancelled` — order cancelled
        - `failed` — escrow error

        **On-ramp send-out statuses (optional post-trade cross-chain transfer):**

        > After an on-ramp order reaches `completed`, the partner may optionally initiate a
        > **send-out** — a cross-chain transfer of the received crypto to a destination network
        > (e.g. XAI → Arbitrum). This is an additional flow and does not affect the core trade lifecycle.
        > The `completed` status reflects that the trade itself is done; send-out statuses track
        > the subsequent cross-chain transfer only.

        - `send_out_pending` — send-out initiated, cross-chain transfer in progress
        - `send_out_completed` — cross-chain transfer successfully delivered to destination
        - `send_out_failed` — cross-chain transfer failed; the on-ramp trade itself remains completed
      enum:
        - created
        - awaiting_liquidity_provider
        - awaiting_crypto_transfer_authorization
        - crypto_transfer_authorization_pending
        - crypto_received
        - fiat_payment_started
        - awaiting_fiat_received_confirmation
        - fiat_received_confirmed
        - crypto_release_pending
        - awaiting_vendor_escrow_funding
        - awaiting_fiat_transfer
        - fiat_transfer_pending
        - completed
        - cancelled
        - failed
        - price_changed_requote_needed
        - send_out_pending
        - send_out_completed
        - send_out_failed
      example: awaiting_crypto_transfer_authorization

    TimelineEntry:
      type: object
      properties:
        status:
          $ref: '#/components/schemas/PartnerOrderStatus'
        timestamp:
          type: string
          format: date-time
          description: When this status was reached
          example: "2026-02-01T12:00:05Z"
        description:
          type: string
          description: Human-readable description of the status change
          example: "Order created"

    RegisterWebhookRequest:
      type: object
      required: [url]
      properties:
        url:
          type: string
          format: uri
          description: HTTPS URL that will receive webhook events
          example: "https://partner.example.com/webhooks/unigox"

    WebhookConfigResponse:
      type: object
      properties:
        url:
          type: string
          nullable: true
          description: Currently configured webhook URL (null if not set)
          example: "https://partner.example.com/webhooks/unigox"
        enabled:
          type: boolean
          description: Whether webhook delivery is active
          example: true

    WebhookTestPingResponse:
      type: object
      properties:
        delivered:
          type: boolean
          description: Whether the test event was delivered successfully
          example: true
        http_status:
          type: integer
          description: HTTP status code returned by the webhook endpoint
          example: 200

    WebhookEventPayload:
      type: object
      description: Payload shape delivered to your webhook URL on each order status change
      properties:
        event_id:
          type: string
          description: Unique event identifier for de-duplication
          example: "evt_a1b2c3d4-e5f6-7890-abcd-ef1234567890"
        event_type:
          type: string
          description: Event type (always `order.status.changed`)
          example: "order.status.changed"
        created_at:
          type: string
          format: date-time
          description: Timestamp when the event was created
          example: "2026-02-01T12:00:05Z"
        data:
          type: object
          properties:
            order_id:
              type: string
              format: uuid
              description: Partner order UUID
              example: "b2c3d4e5-f6a7-8901-bcde-f12345678901"
            status:
              $ref: '#/components/schemas/PartnerOrderStatus'
            user_uuid:
              type: string
              format: uuid
              description: Public UUID of the end-user (from user creation)
              example: "550e8400-e29b-41d4-a716-446655440000"
            crypto_amount:
              type: string
              description: Total crypto amount including fees
              example: "101.500000"
            crypto_currency:
              type: string
              description: Cryptocurrency code
              example: "USDT"
            fiat_amount:
              type: string
              description: Fiat amount
              example: "152880.00"
            fiat_currency:
              type: string
              description: Fiat currency code
              example: "NGN"
            provider:
              type: string
              description: Liquidity provider type
              enum: [p2p, licensed]
              example: "p2p"
            payment_details_id:
              type: string
              description: Payment details ID used for the order
              example: "987"

    PartnerOnRampQuoteRequest:
      type: object
      required: [user_uuid, fiat_currency, crypto_currency]
      oneOf:
        - required: [fiat_amount]
        - required: [crypto_amount]
      properties:
        user_uuid:
          type: string
          format: uuid
          description: Public UUID of the end-user (returned by `POST /partner/users` as `id`)
          example: "550e8400-e29b-41d4-a716-446655440000"
        fiat_amount:
          type: string
          description: Fiat amount the end-user will send. Provide exactly one of fiat_amount or crypto_amount.
          example: "26345"
        crypto_amount:
          type: string
          description: Crypto amount to receive. Provide exactly one of fiat_amount or crypto_amount.
          example: "1.00"
        fiat_currency:
          type: string
          description: Source fiat currency code (ISO 4217)
          example: VND
        crypto_currency:
          type: string
          description: Target cryptocurrency code (e.g., USDT, USDC)
          example: USDT
        payment_method_slug:
          type: string
          description: Optional payment method slug to narrow routing
          example: zalopay
        payment_network_slug:
          type: string
          description: Optional payment network slug to narrow routing
          example: napas-vietnam
        provider_scope:
          type: string
          description: Optional provider scope filter; defaults to all
          enum: [all, licensed_only, p2p_only]
          example: all
        country_code:
          type: string
          description: Optional ISO 2-letter country code for fee/routing context
          example: VN

    PartnerOnRampQuoteResponse:
      type: object
      properties:
        quote_id:
          type: string
          format: uuid
          description: Unique quote identifier — use this to initiate the order
          example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
        rate:
          type: string
          description: Fiat-to-crypto exchange rate (fiat units per 1 crypto unit)
          example: "26345.00"
        crypto_amount:
          type: string
          description: Total crypto amount the end-user will receive (net of fees)
          example: "1.005000"
        fiat_amount:
          type: string
          description: Fiat amount the end-user sends
          example: "26345.00"
        fee_breakdown:
          $ref: '#/components/schemas/FeeBreakdown'
        payment_method_slug:
          type: string
          description: Matched payment method slug used for this quote
          example: zalopay
        payment_network_slug:
          type: string
          description: Matched payment network slug used for this quote
          example: napas-vietnam
        expires_at:
          type: string
          format: date-time
          description: Quote expiration timestamp (60 seconds from creation)
          example: "2026-03-30T19:15:50Z"

    SubmitPayerDetailsRequest:
      type: object
      description: |
        Submit the payer's payment details for a `payment_request` on-ramp order.
        Provide exactly one of `payment_details_id` (saved profile) or `payment_details` (inline object).
      oneOf:
        - required: [payment_details_id]
          properties:
            payment_details_id:
              type: integer
              format: int64
              description: ID of a previously saved payment profile for the end-user
              example: 1444
        - required: [payment_details]
          properties:
            payment_details:
              type: object
              required: [payment_method_id, payment_network_id, details]
              properties:
                payment_method_id:
                  type: integer
                  format: int64
                  description: Payment method ID
                  example: 5
                payment_network_id:
                  type: integer
                  format: int64
                  description: Payment network ID
                  example: 12
                details:
                  type: object
                  description: Payer's account details (fields depend on payment method)
                  additionalProperties: true
                  example:
                    phone: "+254712345678"
                    name: "John Doe"
                country_code:
                  type: string
                  description: Optional ISO 2-letter country code
                  example: KE

    PartnerErrorResponse:
      type: object
      required: [success, error]
      properties:
        success:
          type: boolean
          example: false
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              description: |
                Machine-readable error code. Possible values:
                - `QUOTE_EXPIRED` — quote TTL has passed
                - `QUOTE_NOT_FOUND` — quote ID does not exist
                - `QUOTE_ALREADY_USED` — quote was already consumed by initiate
                - `PRICE_CHANGED_REQUOTE` — price changed beyond slippage tolerance, partner should request a new quote
                - `ORDER_NOT_FOUND` — order ID does not exist or belongs to another partner
                - `NO_OFFERS_AVAILABLE` — no vendor offers for the requested pair
                - `INVALID_REQUEST` — malformed request or missing fields
                - `INVALID_PAYMENT_DETAILS` — payment details not found or invalid
                - `RAIL_ROUTE_MISMATCH` — rail or route does not match payment details
                - `UNAUTHORIZED` — authentication failed
                - `INTERNAL_ERROR` — unexpected server error
                - `INVALID_STATUS` — order is not in the correct status for the requested action
                - `TRANSACTOR_ERROR` — on-chain transaction relay failed (e.g. simulation reverted)
                - `INSUFFICIENT_BALANCE` — partner wallet lacks tokens to fund escrow
                - `OPERATION_NOT_ALLOWED` — action is not permitted for the current order state
              enum:
                - QUOTE_EXPIRED
                - QUOTE_NOT_FOUND
                - QUOTE_ALREADY_USED
                - PRICE_CHANGED_REQUOTE
                - ORDER_NOT_FOUND
                - NO_OFFERS_AVAILABLE
                - INVALID_REQUEST
                - INVALID_PAYMENT_DETAILS
                - RAIL_ROUTE_MISMATCH
                - UNAUTHORIZED
                - INTERNAL_ERROR
                - INVALID_STATUS
                - TRANSACTOR_ERROR
                - INSUFFICIENT_BALANCE
                - OPERATION_NOT_ALLOWED
              example: QUOTE_EXPIRED
            message:
              type: string
              description: Human-readable error message
              example: "Quote has expired, please request a new quote"
            details:
              type: object
              nullable: true
              additionalProperties: true
              description: Additional context (e.g., quoted_rate, current_rate for slippage errors)
              example:
                quoted_rate: 1530.12
                current_rate: 1545.00

  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: Partner API key for authentication. Required for all partner account endpoints.
