openapi: 3.0.1
info:
  title: Spike Exchange API
  version: 1.0.0
  x-date: '2026-01-12'
  description: |
    Partner-facing API for exchange operations, quotes, and reference data.
    Enables bidirectional conversion between cryptocurrencies and fiat currency through an orchestrated exchange platform.

    Use the guide pages for product and integration flows. Use this API reference for exact endpoint behavior, request and response schemas, authentication, and error details.

    **Key Features:**
    - Transaction state management for exchange tracking, retry handling, and reconciliation
    - Indicative quotes for displaying non-binding rates before exchange execution
    - Deposit address management for crypto collection and automatic exchange creation
    - Idempotency support for duplicate-safe write operations

    **Security Requirements:**
    - **HTTPS Required**: All API requests MUST be made over HTTPS. HTTP connections are not supported for security reasons.
    - **Authentication**: All endpoints require partner authentication with OAuth2 JWT tokens and appropriate scopes (exchange:read, exchange:write).
    - **Rate Limiting**: Token validation attempts are rate-limited to prevent brute force attacks.

    **Transaction Lifecycle:**
    Exchange transactions progress through multiple states (WAITING_DEPOSIT, DEPOSIT_PENDING, PROCESSING, COMPLETED, FAILED, CANCELLED, EXPIRED).
    Partners can query transaction status, cancel pending transactions, and receive webhook notifications on state changes.

    **Rate Information:**
    All rates are quoted in canonical pair direction: units of the fiat currency
    per 1 unit of the crypto currency. The same numerical rate value is returned
    for on-ramp (fiat -> crypto) and off-ramp (crypto -> fiat) on the same pair.
    Direction is carried by `amounts.from.currency` / `amounts.to.currency`, not
    by inverting the rate. Indicative rates are non-binding; fixed rates are
    guaranteed for execution within the validity window. Rates are derived from
    cached orderbook data updated at sub-second cadence to balance accuracy and
    performance.
servers:
  - url: https://one.spikeapp.cool/exchange-gateway-api
    description: Sandbox server
security:
  - BearerAuth: []
tags:
  - name: Authentication
    description: |
      The Exchange API uses OAuth 2.0 Client Credentials flow (RFC 6749) for machine-to-machine authentication.
      Partners authenticate using `client_id` and `client_secret` credentials provided during onboarding.

      ### Obtaining Access Tokens

      To obtain an access token, make a POST request to the token endpoint:

      **Endpoint:** `POST /oauth/token`

      **Content-Type:** `application/x-www-form-urlencoded`

      **Request Parameters:**
      - `grant_type` (required): Must be `client_credentials`
      - `client_id` (required): Your OAuth client identifier
      - `client_secret` (required): Your OAuth client secret
      - `scope` (optional): Space-separated list of requested scopes (defaults to all granted scopes)

      **Example Request:**
      ```
      POST /oauth/token
      Content-Type: application/x-www-form-urlencoded

      grant_type=client_credentials&client_id=your-client-id&client_secret=your-client-secret&scope=exchange:read exchange:write
      ```

      **Success Response (200 OK):**
      ```json
      {
        "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
        "token_type": "Bearer",
        "expires_in": 300,
        "refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
        "refresh_expires_in": 2592000,
        "scope": "exchange:read exchange:write"
      }
      ```

      **Error Response (401 Unauthorized):**
      ```json
      {
        "error": "invalid_client",
        "error_description": "Client authentication failed"
      }
      ```

      ### Using Access Tokens

      Include the access token in the `Authorization` header of all API requests:

      ```
      Authorization: Bearer {access_token}
      ```

      **Example:**
      ```
      GET /v1/exchanges
      Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
      ```

      ### Refreshing Access Tokens

      Access tokens expire after a short period. Use refresh tokens to obtain new access tokens without re-authenticating with client credentials.

      **Token Rotation:**
      - Each refresh token use issues a new refresh token and invalidates the old one
      - Old refresh tokens cannot be reused after rotation
      - Store the new refresh token securely after each refresh

      To refresh an access token, make a POST request to the token endpoint:

      **Endpoint:** `POST /oauth/token`

      **Content-Type:** `application/x-www-form-urlencoded`

      **Request Parameters:**
      - `grant_type` (required): Must be `refresh_token`
      - `refresh_token` (required): Refresh token from previous token response
      - `scope` (optional): Requested scopes (cannot expand beyond original token scopes)

      **Example Request:**
      ```
      POST /oauth/token
      Content-Type: application/x-www-form-urlencoded

      grant_type=refresh_token&refresh_token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
      ```

      **Success Response (200 OK):**
      ```json
      {
        "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
        "token_type": "Bearer",
        "expires_in": 300,
        "refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
        "refresh_expires_in": 2592000,
        "scope": "exchange:read exchange:write"
      }
      ```

      **Error Response (400 Bad Request) - Invalid Refresh Token:**
      ```json
      {
        "error": "invalid_grant",
        "error_description": "Invalid or expired refresh token"
      }
      ```

      **Refresh Token Lifetime:**
      - Refresh tokens expire after 30 days (configurable per environment)
      - When a refresh token expires, use `client_credentials` flow to obtain new tokens
      - Implement proactive token refresh (refresh access tokens before expiration)

      ### Token Lifetime

      Access tokens have a limited lifetime and must be refreshed:
      - **Production:** 5 minutes
      - **Staging:** 60 minutes
      - **Development:** 24 hours

      When a token expires, you will receive a `401 Unauthorized` response. Use the refresh token to obtain a new access token, or use `client_credentials` flow to obtain new tokens.

      **Refresh Token Lifetime:**
      - **Production:** 30 days
      - **Staging:** 30 days
      - **Development:** 30 days

      ### OAuth Scopes

      Scopes control which API endpoints your partner application can access:

      | Scope | Description | Required For |
      |-------|-------------|---------------|
      | `exchange:read` | Read exchange data and reference information | `GET /v1/exchanges`, `GET /v1/exchanges/by-id`, `GET /v1/quotes`, `GET /v1/quotes/fixed`, `POST /v1/addresses/validate`, `GET /v1/currencies`, `GET /v1/currency-pairs`, `GET /v1/deposit-addresses`, `GET /v1/deposit-addresses/{depositAddressId}`, `GET /v1/webhooks`, `GET /v1/webhooks/{webhookId}`, `GET /v1/webhooks/{webhookId}/failed-events` |
      | `exchange:write` | Create and manage exchanges | `POST /v1/exchanges`, `POST /v1/exchanges/cancel`, `POST /v1/exchanges/refund`, `POST /v1/exchanges/{exchangeId}/authorization/approve`, `POST /v1/exchanges/{exchangeId}/authorization/reject`, `POST /v1/deposit-addresses`, `PATCH /v1/deposit-addresses/{depositAddressId}`, `DELETE /v1/deposit-addresses/{depositAddressId}`, `POST /v1/webhooks`, `DELETE /v1/webhooks/{webhookId}`, `POST /v1/webhooks/{webhookId}/activate`, `POST /v1/webhooks/{webhookId}/events/{eventId}/retry` |

      Scopes are granted during partner onboarding. If you request a scope that hasn't been granted, only your allowed scopes will be returned in the token response.

      ### Error Responses

      All authentication errors follow RFC 6749 format:

      | Error Code | HTTP Status | Description |
      |------------|-------------|-------------|
      | `invalid_request` | 400 | Missing or malformed parameters |
      | `invalid_client` | 401 | Client authentication failed (invalid credentials) |
      | `invalid_grant` | 400 | Invalid grant type or invalid/expired refresh token |
      | `invalid_scope` | 400 | Requested scope exceeds allowed scopes |
      | `unauthorized_client` | 403 | Client not authorized for grant type |
      | `server_error` | 500 | Internal server error |

      **Example Error Response:**
      ```json
      {
        "error": "invalid_client",
        "error_description": "Client authentication failed",
        "error_uri": "https://docs.spike.com/errors/invalid_client"
      }
      ```

      ### Security Best Practices

      1. **Store credentials securely:** Never commit `client_id` or `client_secret` to version control
      2. **Use HTTPS only:** All API requests must use HTTPS (TLS 1.2 or higher)
      3. **Implement token caching:** Cache access tokens and refresh tokens securely (encrypted at rest)
      4. **Proactive token refresh:** Refresh access tokens before expiration using refresh tokens to avoid 401 errors
      5. **Handle refresh token expiration:** When refresh token expires, fallback to `client_credentials` flow
      6. **Token rotation:** Always store the new refresh token after each refresh (old token is invalidated)
      7. **Rotate credentials:** Regularly rotate `client_secret` through the partner portal
      8. **Monitor usage:** Review authentication logs for suspicious activity

      ### Rate Limiting

      Token endpoint requests are rate-limited to prevent abuse:
      - **Standard tier:** 10 requests/second, 100 requests/minute

      If the standard tier limits are not sufficient for your use case, please contact the Spike Team to discuss increasing your rate limits.

      Rate limit violations return `429 Too Many Requests` with `Retry-After` header indicating when to retry.
  - name: Exchanges
    description: Exchange transaction operations for converting between cryptocurrencies and fiat currency. Supports both on-ramp (fiat→crypto) and off-ramp (crypto→fiat) flows.
  - name: Quotes
    description: Indicative exchange rates calculated from orderbook data. Rates are non-binding and suitable for display purposes. Final conversion rates are determined at execution time.
  - name: Reference Data
    description: Reference data endpoints for supported currencies, currency pairs, and network information. Used to discover available exchange options.
  - name: Deposit Addresses
    description: Reusable deposit address management for automatic exchange creation on deposits.
  - name: Webhooks
    description: |
      Partner-facing API for webhook management and event notifications.
      Enables partners to receive real-time notifications about exchange transaction status changes and lifecycle events.

      **Core Capabilities:**
      - **Webhook Registration**: Partners can register webhook endpoints to receive event notifications
      - **Event Notifications**: Automatic delivery of exchange transaction status changes and lifecycle events
      - **Webhook Management**: List, update, and delete registered webhooks

      **Event Types:**

      **Deposit Events:**
      - `deposit.initiated` - Funding deposit detected but not yet confirmed
      - `deposit.progressed` - Deposit confirmation progress update
      - `deposit.received` - Funding deposit confirmed

      **Withdraw Events:**
      - `withdraw.initiated` - Withdrawal started
      - `withdraw.completed` - Withdrawal confirmed
      - `withdraw.failed` - Withdrawal failed

      **On-Ramp Events (aggregate terminal — fired once per on-ramp):**
      - `onramp.completed` - On-ramp completed successfully (terminal)
      - `onramp.failed` - On-ramp failed (terminal; carries failureContext)

      **Off-Ramp Events (aggregate terminal — fired once per off-ramp):**
      - `offramp.completed` - Off-ramp completed successfully (terminal)
      - `offramp.failed` - Off-ramp failed (terminal; carries failureContext)

      **Exchange Events (trading-leg — provider order lifecycle, NOT the ramp aggregate):**
      - `exchange.completed` - Trading leg completed (provider order filled)
      - `exchange.failed` - Trading leg failed (provider order rejected)
      - `exchange.cancelled` - Trading leg cancelled (async cancellation request)
      - `exchange.authorization_required` - Withdrawal authorization required from partner

      **Refund Events:**
      - `refund.required` - Refund is required (system detected condition requiring refund)
      - `refund.initiated` - Refund process started
      - `refund.completed` - Refund completed successfully (funds returned to refund address)
      - `refund.failed` - Refund failed (manual intervention may be required)

      **Common Use Cases:**
      - **Deposit notifications**: Subscribe to `deposit.initiated`, `deposit.progressed`, and `deposit.received` for on-ramp exchanges to track deposit lifecycle
      - **Conversion completion**: Subscribe to `exchange.completed` for successful conversions
      - **Failure handling**: Subscribe to `exchange.failed` for conversion errors
      - **Authorization handling**: Subscribe to `exchange.authorization_required` and approve/reject via `POST /v1/exchanges/{exchangeId}/authorization/approve` or `POST /v1/exchanges/{exchangeId}/authorization/reject`
      - **Refund tracking**: Subscribe to `refund.required`, `refund.initiated`, `refund.completed`, and `refund.failed` for refund lifecycle

      **Deposit Event Flow:**
      1. Deposit detected → `deposit.initiated` (confirmations=0)
      2. Additional confirmations received → `deposit.progressed` (confirmations=N, where N < required)
      3. All required confirmations received → `deposit.received` (confirmations >= required, status=CONFIRMED)

      **Webhook Delivery:**
      When an exchange event occurs, Spike sends an HTTP POST request to registered webhook URLs.

      **HTTP Headers:**
      - `Content-Type: application/json`
      - `X-Webhook-Signature: <base64_rsa_signature>` - RSA-SHA256 signature for payload verification
      - `X-Webhook-Timestamp: <unix_timestamp_seconds>` - Unix timestamp in seconds for replay attack prevention
      - `X-Webhook-Nonce: <uuid>` - Unique per-delivery nonce for replay protection (UUID v4)
      - `X-Webhook-Key-Id: <uuid>` - Key identifier (use to download the correct public key)
      - `X-Webhook-Event-Type: <event_type>` - Event type (e.g., "deposit.received", "exchange.completed")
      - `X-Webhook-Event-Id: <event_id>` - Unique event identifier for idempotency

      **Idempotency:**
      Use `eventId` field (in payload) or `X-Webhook-Event-Id` header for deduplication. Each event delivery has a unique `eventId` that remains constant across retry attempts. Partners should track `eventId` to prevent duplicate processing of the same event.

      **Request Body:**
      JSON payload containing event data (see WebhookPayload schema). The signature is calculated over `keyId + ":" + timestamp + ":" + nonce + ":" + raw_json_body`.

      **Example Webhook Payload:**
      ```json
      {
        "eventId": "evt-123",
        "eventType": "exchange.completed",
        "occurredAt": "2023-11-04T10:35:56.789Z",
        "exchange": {
          "exchangeId": "550e8400-e29b-41d4-a716-446655440000",
          "externalReference": { "externalId": "order-123", "externalUserId": "user-456" },
          "status": "COMPLETED",
          "currencyPair": { "fromCurrency": "BTC", "toCurrency": "USD" },
          "providerExecutionInfo": {
            "providerSymbol": "tBTCUSD",
            "providerOrderIds": ["12345678", "12345679"]
          },
          "amounts": { "from": {...}, "to": {...}, "rate": "45000.00" },
          "depositAddress": { "address": "1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2", "network": "bitcoin" },
          "depositAddressId": "550e8400-e29b-41d4-a716-446655440000",
          "cryptoDeposit": {...},
          "cryptoRefunds": [...]
        }
      }
      ```

      **Deposit Address Fields:**
      - `depositAddress`: Crypto deposit address where user sends cryptocurrency (off-ramp only, null for on-ramp)
      - `depositAddressId`: Unique identifier for the reusable deposit address linked to this exchange (off-ramp only, null for on-ramp or when not using reusable address). This field allows partners to correlate exchanges with reusable deposit addresses created via `POST /v1/deposit-addresses`.

      **Signature Verification (RSA-SHA256):**

      1. **Download your public key** via `GET /v1/webhooks/public-key` (do this once and cache it)
      2. **Reconstruct the signed string**: `keyId + ":" + timestamp + ":" + nonce + ":" + raw_json_body`
         - `keyId` is the value from `X-Webhook-Key-Id` header
         - `timestamp` is the value from `X-Webhook-Timestamp` header
         - `nonce` is the value from `X-Webhook-Nonce` header
         - `raw_json_body` is the exact raw JSON body bytes (before parsing)
      3. **Verify signature**: Use RSA-SHA256 to verify `X-Webhook-Signature` (Base64-encoded) against the signed string

      **Example verification (Java):**
      ```java
      String signedString = keyId + ":" + timestamp + ":" + nonce + ":" + rawBody;
      Signature sig = Signature.getInstance("SHA256withRSA");
      sig.initVerify(publicKey);
      sig.update(signedString.getBytes(StandardCharsets.UTF_8));
      boolean valid = sig.verify(Base64.getDecoder().decode(signature));
      ```

      **Replay Protection:**
      Partners SHOULD implement both timestamp and nonce validation:
      - Reject webhooks with timestamps older than 5 minutes
      - Track seen nonces within the 5-minute window to reject duplicates

      **Timestamp Validation:**
      Partners SHOULD reject webhooks with timestamps older than 5 minutes to prevent replay attacks. Example validation:
      ```java
      long webhookTimestamp = Long.parseLong(request.getHeader("X-Webhook-Timestamp"));
      long currentTimestamp = System.currentTimeMillis() / 1000;
      if (currentTimestamp - webhookTimestamp > 300) { // 5 minutes
          throw new SecurityException("Webhook timestamp too old");
      }
      ```

      **Refund Handling:**
      Refunds occur in various scenarios including technical errors, deposit timeouts (fixed-rate only), below minimum amounts, wrong network deposits, and trade failures. Refund processing is asynchronous:
      - Refunds use the `refundAddress` provided during exchange creation
      - Refunds are processed to the original cryptocurrency and network (no conversion)
      - Refund completion triggers refund.completed webhook event

      **Security Requirements:**
      - HTTPS required for all webhook endpoints
      - Verify `X-Webhook-Signature` using RSA-SHA256 with your public key
      - Validate `X-Webhook-Timestamp` (reject if > 5 minutes old)
      - Failed deliveries retried with exponential backoff

      **Authentication:**
      All management endpoints require OAuth2 JWT tokens with appropriate scopes (exchange:read, exchange:write).
paths:
  /oauth/token:
    post:
      tags:
        - Authentication
      summary: OAuth 2.0 token endpoint
      description: |
        OAuth 2.0 token endpoint supporting client credentials flow and refresh token flow.
        Proxies requests to auth-service for partner authentication.
        Returns JWT access tokens and refresh tokens for authenticated partners.

        **Grant Types:**
        - `client_credentials`: Initial token issuance using client_id and client_secret
        - `refresh_token`: Token refresh using a previously issued refresh token

        **Token Refresh Flow:**
        - Access tokens expire after a short period
        - Refresh tokens are long-lived and can be used to obtain new access tokens
        - Each refresh token use issues a new refresh token (token rotation) for enhanced security
        - Partners should store refresh tokens securely and use them to refresh access tokens before expiration
      operationId: issueToken
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required:
                - grant_type
              properties:
                grant_type:
                  type: string
                  enum:
                    - client_credentials
                    - refresh_token
                  description: |
                    OAuth 2.0 grant type.
                    - `client_credentials`: Initial token issuance (requires client_id and client_secret)
                    - `refresh_token`: Token refresh (requires refresh_token)
                client_id:
                  type: string
                  description: |
                    Partner OAuth client ID.
                    Required for `client_credentials` grant type.
                    Not required for `refresh_token` grant type.
                client_secret:
                  type: string
                  description: |
                    Partner OAuth client secret.
                    Required for `client_credentials` grant type.
                    Not required for `refresh_token` grant type.
                refresh_token:
                  type: string
                  description: |
                    Refresh token from previous token response.
                    Required for `refresh_token` grant type.
                    Not required for `client_credentials` grant type.
                scope:
                  type: string
                  description: |
                    Requested scopes (space-separated, e.g., "exchange:read exchange:write").
                    Optional for both grant types. If not provided, all partner scopes are granted.
                    For refresh_token grant type, scopes cannot be expanded beyond original token scopes.
      responses:
        '200':
          description: Token issued successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TokenResponse'
        '400':
          description: Bad request (invalid parameters)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '401':
          description: Unauthorized (invalid client credentials)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '429':
          description: Too Many Requests (rate limit exceeded)
          headers:
            Retry-After:
              description: Number of seconds to wait before retrying
              schema:
                type: integer
                example: 60
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        default:
          description: Unexpected error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
  /v1/exchanges:
    post:
      tags:
        - Exchanges
      summary: Create exchange transaction
      description: |
        Creates a new exchange transaction for converting between cryptocurrencies and fiat currency.
        Supports both Fixed Rate Mode and Non-Fixed Rate Mode exchanges.

        **Fixed Rate Mode (quoteId required):**
        - `quoteId` is required and must be provided in the request body
        - Quote must be obtained from `GET /v1/quotes/fixed` and must be valid (not expired and not already used)
        - Exchange executes using the locked rate and amounts from the quote
        - Supports both on-ramp (fiat→crypto) and off-ramp (crypto→fiat) flows

        **Non-Fixed Rate Mode (quoteId not provided):**
        - `quoteId` must not be provided in the request body
        - For off-ramp flows (crypto→fiat): Use `POST /v1/deposit-addresses` instead to create reusable deposit addresses
        - For on-ramp flows (fiat→crypto): Create exchange via this endpoint without `quoteId`
        - Exchange executes using market rate at execution time

        **On-Ramp Flow (Fiat → Crypto):**
        - Fiat funds are secured before market execution
        - Cryptocurrency is withdrawn to an external address provided by the end user
        - External addresses format validated on exchange creation

        **On-Ramp Settlement Patterns:** how the fiat is delivered to the exchange depends on the partner's integration with Spike — the request shape differs between the two:
        - **Partner-managed funds** (default): the partner moves the fiat onto the exchange platform themselves, then submits `depositReference` (with `fromAmount` or `toAmount`) to assert the movement. A fresh exchange normally returns `WAITING_DEPOSIT` while Spike accepts and processes that assertion asynchronously, then advances to `PROCESSING`. This is the pattern documented in the public schema and the one a new integrator should expect.
        - **Spike-managed funds** (enabled per partner): for partners on integrations where Spike has operational control of the fiat accounts, Spike executes the fiat movement itself based on a richer settlement payload submitted with the exchange. This pattern is provisioned per-partner and uses fields beyond the public schema — contact the Spike team to enable it.

        **Off-Ramp Flow (Crypto → Fiat):**
        - Deposit address is assigned from a pre-generated pool for low latency
        - Conversion executes after all required blockchain confirmations are received
        - Fiat proceeds are credited to the end user's payment account

        **Transaction State:**
        Spike maintains transaction state as the single source of truth, enabling retry logic and reconciliation.
        Transactions can be queried, cancelled (if in pending states), and tracked through their lifecycle.

        **Business Identifiers:**
        - `externalId` (required): Partner's transaction identifier for business tracking and reconciliation. Must be unique per partner. Used for reconciliation, cancel operations, webhook correlation, and exchange lookup.

        **Idempotency:**
        - `Idempotency-Key` (optional): Client-generated UUID for idempotency. Prevents duplicate exchanges on repeated requests. If not provided, server generates a UUID automatically. Must be unique per partner per exchange intent.
        - Duplicate `Idempotency-Key` → 409 Conflict, returns existing exchange

        Requires partner authentication with 'exchange:write' scope.
      operationId: createExchange
      security:
        - BearerAuth: []
      parameters:
        - in: header
          name: Authorization
          required: true
          description: Bearer token for partner authentication (JWT)
          schema:
            type: string
            example: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
        - in: header
          name: Idempotency-Key
          required: false
          description: |
            Optional UUID for retry protection. Prevents duplicate exchanges on repeated requests.
            Must be unique per partner. Use same Idempotency-Key for each retry attempt.
            If not provided, server generates UUID automatically.
          schema:
            type: string
            format: uuid
            example: 550e8400-e29b-41d4-a716-446655440000
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateExchangeRequest'
      responses:
        '200':
          description: Exchange created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExchangeResponse'
        '400':
          description: Bad request (invalid parameters, validation errors)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '403':
          description: Forbidden (partner not active, insufficient permissions, currency/pair not allowed)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '404':
          description: Not found (quote not found or already used if quoteId provided)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '409':
          description: |
            Conflict: Duplicate externalId or Idempotency-Key detected.
            Returns existing exchange in response body (same as 200 OK response).
            - Duplicate externalId: Partner's transaction ID already exists
            - Duplicate Idempotency-Key: Retry key already used
            Both fields are checked independently for duplicates.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExchangeResponse'
        '429':
          description: Too Many Requests (rate limit exceeded)
          headers:
            Retry-After:
              description: Number of seconds to wait before retrying
              schema:
                type: integer
                example: 60
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '503':
          description: Service unavailable (exchange service dependencies unavailable)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        default:
          description: Unexpected error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
    get:
      tags:
        - Exchanges
      summary: List exchange transactions
      description: |
        Retrieves a paginated list of exchange transactions for the authenticated partner.
        Supports filtering by status, currency, date range, and external identifiers.
        Uses cursor-based pagination for efficient handling of large result sets.
        Requires partner authentication with 'exchange:read' scope.
        Partners can only access their own exchanges.

        **Pagination:**
        - First request: Omit `cursor` parameter to get the first page
        - Subsequent requests: Use `nextCursor` value from previous response's pagination object
        - The `hasMore` field indicates if more results are available
        - Cursor is an opaque token - do not modify or decode it
      operationId: listExchanges
      security:
        - BearerAuth: []
      parameters:
        - in: header
          name: Authorization
          required: true
          description: Bearer token for partner authentication (JWT)
          schema:
            type: string
            example: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
        - in: query
          name: status
          required: false
          description: Filter by exchange status
          schema:
            $ref: '#/components/schemas/ExchangeStatus'
        - in: query
          name: fromCurrency
          required: false
          description: Filter by source currency code
          schema:
            type: string
            minLength: 3
            maxLength: 10
            example: BTC
        - in: query
          name: toCurrency
          required: false
          description: Filter by target currency code
          schema:
            type: string
            minLength: 3
            maxLength: 10
            example: USD
        - in: query
          name: fromDateTime
          required: false
          description: Start date-time filter (ISO 8601 format, includes time component)
          schema:
            type: string
            format: date-time
            example: '2023-11-04T10:30:56.789Z'
        - in: query
          name: toDateTime
          required: false
          description: End date-time filter (ISO 8601 format, includes time component)
          schema:
            type: string
            format: date-time
            example: '2023-11-04T10:35:56.789Z'
        - in: query
          name: externalId
          required: false
          description: Filter by partner's external transaction ID
          schema:
            type: string
            maxLength: 255
            example: partner-tx-456
        - in: query
          name: externalUserId
          required: false
          description: Filter by partner's user identifier
          schema:
            type: string
            maxLength: 255
            example: user-123
        - in: query
          name: depositAddressId
          required: false
          description: Filter by reusable deposit address identifier
          schema:
            type: string
            example: addr-xyz789
        - in: query
          name: type
          required: false
          description: |
            Exchange type filter. on-ramp for fiat-to-crypto, off-ramp for crypto-to-fiat.
            Defaults to off-ramp for backward compatibility.
          schema:
            type: string
            enum:
              - on-ramp
              - off-ramp
            default: off-ramp
        - in: query
          name: limit
          required: false
          description: Results per page (default 50, max 100)
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
            example: 50
        - in: query
          name: cursor
          required: false
          description: Cursor for pagination (opaque token from previous response's nextCursor). Omit for first page.
          schema:
            type: string
            example: eyJleGNoYW5nZUlkIjoiNTUwZTg0MDAtZTI5Yi00MWQ0LWE3MTYtNDQ2NjU1NDQwMDAwIiwidGltZXN0YW1wIjoxNjk5MTIzNDU2Nzg5fQ
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExchangeListResponse'
        '400':
          description: Bad request (invalid query parameters)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '403':
          description: Forbidden (partner not active or insufficient permissions)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '429':
          description: Too Many Requests (rate limit exceeded)
          headers:
            Retry-After:
              description: Number of seconds to wait before retrying
              schema:
                type: integer
                example: 60
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '503':
          description: Service unavailable (exchange service dependencies unavailable)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        default:
          description: Unexpected error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
  /v1/exchanges/by-id:
    get:
      tags:
        - Exchanges
      summary: Get exchange transaction
      description: |
        Retrieves details of a specific exchange transaction by ID.
        Accepts either exchangeId (internal UUID) or externalId (partner's transaction ID) as query parameters.
        At least one identifier must be provided.
        Note: externalId can only be used if it was provided during exchange creation.
        Requires partner authentication with 'exchange:read' scope.
        Partners can only access their own exchanges.
      operationId: getExchange
      security:
        - BearerAuth: []
      parameters:
        - in: header
          name: Authorization
          required: true
          description: Bearer token for partner authentication (JWT)
          schema:
            type: string
            example: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
        - in: query
          name: exchangeId
          required: false
          description: Internal exchange transaction ID (UUID)
          schema:
            type: string
            format: uuid
            example: 550e8400-e29b-41d4-a716-446655440000
        - in: query
          name: externalId
          required: false
          description: Partner's external transaction ID. Only available if externalId was provided during exchange creation.
          schema:
            type: string
            minLength: 1
            maxLength: 255
            example: partner-tx-456
      responses:
        '200':
          description: Exchange found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExchangeResponse'
        '400':
          description: Bad request (invalid query parameters, missing or invalid exchangeId/externalId)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '403':
          description: Forbidden (exchange belongs to different partner)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '404':
          description: Exchange not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '429':
          description: Too Many Requests (rate limit exceeded)
          headers:
            Retry-After:
              description: Number of seconds to wait before retrying
              schema:
                type: integer
                example: 60
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '503':
          description: Service unavailable (exchange service dependencies unavailable)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        default:
          description: Unexpected error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
  /v1/exchanges/cancel:
    post:
      tags:
        - Exchanges
      summary: Request exchange cancellation (async)
      description: |
        Requests cancellation of a pending exchange transaction.

        **Important: This operation is asynchronous.**
        - The endpoint records the intention to cancel and returns immediately
        - Response confirms the cancel request was accepted
        - Check webhooks or poll GET /exchanges/{id} for final CANCELLED status

        **Parameters:**
        - Accepts either exchangeId (internal UUID) or externalId (partner's transaction ID) as query parameters
        - At least one identifier must be provided
        - Note: externalId can only be used if it was provided during exchange creation

        **Cancellable States:**
        Only exchanges in WAITING_DEPOSIT or DEPOSIT_PENDING states can be cancelled.
        Exchanges in PROCESSING, COMPLETED, FAILED, or already CANCELLED states cannot be cancelled.

        Requires partner authentication with 'exchange:write' scope.
        Partners can only cancel their own exchanges.
      operationId: cancelExchange
      security:
        - BearerAuth: []
      parameters:
        - in: header
          name: Authorization
          required: true
          description: Bearer token for partner authentication (JWT)
          schema:
            type: string
            example: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
        - in: query
          name: exchangeId
          required: false
          description: Internal exchange transaction ID (UUID)
          schema:
            type: string
            format: uuid
            example: 550e8400-e29b-41d4-a716-446655440000
        - in: query
          name: externalId
          required: false
          description: Partner's external transaction ID. Only available if externalId was provided during exchange creation.
          schema:
            type: string
            minLength: 1
            maxLength: 255
            example: partner-tx-456
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CancelExchangeRequest'
      responses:
        '202':
          description: |
            Cancel request accepted. Cancellation will be processed asynchronously.
            Check webhooks or poll GET /exchanges/{id} for final CANCELLED status.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CancelExchangeResponse'
        '400':
          description: Bad request (exchange cannot be cancelled - already processing/completed/failed, invalid query parameters)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '403':
          description: Forbidden (exchange belongs to different partner)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '404':
          description: Exchange not found or not accessible by partner
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '429':
          description: Too Many Requests (rate limit exceeded)
          headers:
            Retry-After:
              description: Number of seconds to wait before retrying
              schema:
                type: integer
                example: 60
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '503':
          description: Service unavailable (exchange service dependencies unavailable)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        default:
          description: Unexpected error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
  /v1/exchanges/refund:
    post:
      tags:
        - Exchanges
      summary: Request refund for exchange (async)
      description: |
        Requests a refund for an exchange transaction. Refunds can be requested for exchanges that have received deposits.
        Refund processing is asynchronous - the endpoint returns 202 Accepted immediately and processes the refund in the background.

        **Important: This operation is asynchronous.**
        - The endpoint creates a refund request and returns immediately
        - Check webhooks or poll GET /v1/exchanges/{exchangeId} for refund status in refunds array

        **Parameters:**
        - `Idempotency-Key` (optional): Client-generated UUID for retry protection. Prevents duplicate refunds on repeated requests. If not provided, server generates a deterministic key based on exchangeId, amount, and refundAddress.
        - Duplicate `Idempotency-Key` → 409 Conflict, returns existing refund
        - Accepts either exchangeId (internal UUID) or externalId (partner's transaction ID) as query parameters
        - At least one identifier must be provided
        - Note: externalId can only be used if it was provided during exchange creation

        **Refund Eligibility:**
        - Exchange must have at least one confirmed deposit
        - Refund amount must not exceed available deposit amount (total deposits minus already exchanged amounts and already refunded amounts)
        - Refund currency must match exchange fromCurrency
        - Refund amount must meet minimum threshold (dust protection)

        **Refund Status:**
        After requesting a refund, partners can check refund status via:
        - `GET /v1/exchanges/{exchangeId}` - Returns refunds array with current status
        - Webhook events: `refund.initiated`, `refund.completed`, `refund.failed`

        **Multiple Refunds:**
        Multiple refunds can be requested per exchange, each with its own lifecycle and tracking.

        Requires partner authentication with 'exchange:write' scope.
      operationId: requestRefund
      security:
        - BearerAuth: []
      parameters:
        - in: header
          name: Authorization
          required: true
          description: Bearer token for partner authentication (JWT)
          schema:
            type: string
            example: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
        - in: header
          name: Idempotency-Key
          required: false
          description: |
            Optional UUID for retry protection. Prevents duplicate refunds on repeated requests.
            Must be unique per partner per refund intent. Use same Idempotency-Key for each retry attempt.
            If not provided, server generates a deterministic key based on exchangeId, amount, and refundAddress.
          schema:
            type: string
            format: uuid
            example: 550e8400-e29b-41d4-a716-446655440000
        - in: query
          name: exchangeId
          required: false
          description: Internal exchange transaction ID (UUID)
          schema:
            type: string
            format: uuid
            example: 550e8400-e29b-41d4-a716-446655440000
        - in: query
          name: externalId
          required: false
          description: Partner's external transaction ID. Only available if externalId was provided during exchange creation.
          schema:
            type: string
            minLength: 1
            maxLength: 255
            example: partner-tx-456
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RequestRefundRequest'
      responses:
        '202':
          description: |
            Refund request accepted. Refund will be processed asynchronously.
            Check webhooks or poll GET /exchanges/{id} for refund status in refunds array.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RequestRefundResponse'
        '400':
          description: Bad request (invalid parameters, refund not eligible, validation errors)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '403':
          description: Forbidden (partner not active, insufficient permissions, exchange belongs to different partner)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '404':
          description: Exchange not found or not accessible by partner
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '409':
          description: |
            Conflict: Duplicate Idempotency-Key or refund request detected.
            - Duplicate Idempotency-Key: Returns the refund created with the same key
            - Duplicate refund: Same amount and address for same exchange
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '429':
          description: Too Many Requests (rate limit exceeded)
          headers:
            Retry-After:
              description: Number of seconds to wait before retrying
              schema:
                type: integer
                example: 60
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '503':
          description: Service unavailable (exchange service dependencies unavailable)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        default:
          description: Unexpected error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
  /v1/exchanges/{exchangeId}/authorization/approve:
    post:
      tags:
        - Exchanges
      summary: Approve exchange authorization
      description: |
        Approves partner authorization for an exchange that is awaiting approval.
        Called by partner after receiving `exchange.authorization_required` webhook.

        **Prerequisite:** Exchange must be in AWAITING_AUTHORIZATION status.
        On approval: exchange creation and order submission proceed automatically.

        Requires partner authentication with 'exchange:write' scope.
      operationId: approveExchangeAuthorization
      security:
        - BearerAuth: []
      parameters:
        - in: path
          name: exchangeId
          required: true
          description: Exchange transaction ID (from webhook payload)
          schema:
            type: string
            format: uuid
            example: 550e8400-e29b-41d4-a716-446655440000
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ApproveExchangeAuthorizationRequest'
      responses:
        '200':
          description: Authorization approved, exchange processing started
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExchangeResponse'
        '400':
          description: |
            Bad request. Error codes:
            - `exchange_wrong_state` — exchange not in AWAITING_AUTHORIZATION
            - `missing_approval_material` — required approval fields missing for on-ramp
            - `commitment_mismatch` — commitment hash does not match transaction parameters
            - `commitment_conflict` — retry with a different commitment hash than the one previously accepted
            - `authorization_expired` — approval past validUntil
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '403':
          description: Forbidden (exchange belongs to different partner)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '404':
          description: Exchange not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '429':
          description: Too Many Requests (rate limit exceeded)
          headers:
            Retry-After:
              description: Number of seconds to wait before retrying
              schema:
                type: integer
                example: 60
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        default:
          description: Unexpected error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
  /v1/exchanges/{exchangeId}/authorization/reject:
    post:
      tags:
        - Exchanges
      summary: Reject exchange authorization
      description: |
        Rejects partner authorization for an exchange that is awaiting approval.
        Called by partner after receiving `exchange.authorization_required` webhook.

        **Prerequisite:** Exchange must be in AWAITING_AUTHORIZATION status.
        On rejection: exchange is marked as FAILED and refund webhook is triggered.

        Requires partner authentication with 'exchange:write' scope.
      operationId: rejectExchangeAuthorization
      security:
        - BearerAuth: []
      parameters:
        - in: path
          name: exchangeId
          required: true
          description: Exchange transaction ID (from webhook payload)
          schema:
            type: string
            format: uuid
            example: 550e8400-e29b-41d4-a716-446655440000
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RejectExchangeAuthorizationRequest'
      responses:
        '200':
          description: Authorization rejected, exchange marked as failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExchangeResponse'
        '400':
          description: |
            Bad request. Error codes (`code` field carries HTTP status `400`; the specific reason is in `message`):
            - `exchange_wrong_state` — exchange not in AWAITING_AUTHORIZATION (already terminal, processing, or never reached authorization)
            - `authorization_expired` — rejection submitted after the authorization window closed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '403':
          description: Forbidden (exchange belongs to different partner)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '404':
          description: Exchange not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '429':
          description: Too Many Requests (rate limit exceeded)
          headers:
            Retry-After:
              description: Number of seconds to wait before retrying
              schema:
                type: integer
                example: 60
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        default:
          description: Unexpected error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
  /v1/quotes:
    get:
      tags:
        - Quotes
      summary: Get indicative exchange rate
      description: |
        Returns an indicative (non-binding) exchange rate for a currency pair calculated from liquidity provider orderbook data.
        The rate is suitable for display purposes only. Final conversion rates are determined at execution time based on market conditions.
        Rates are calculated from the best available price in the orderbook (bid for sells, ask for buys) without volume adjustments.

        **Parameters:**
        - `fromCurrency` and `toCurrency` are required
        - Optional `fromAmount` or `toAmount` (mutually exclusive) - if provided, response includes calculated amounts

        Requires partner authentication with 'exchange:read' scope.
      operationId: getQuote
      security:
        - BearerAuth: []
      parameters:
        - in: header
          name: Authorization
          required: true
          description: Bearer token for partner authentication (JWT)
          schema:
            type: string
            example: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
        - in: query
          name: fromCurrency
          required: true
          description: Source currency code (e.g., "BTC")
          schema:
            type: string
            minLength: 3
            maxLength: 10
            example: BTC
        - in: query
          name: toCurrency
          required: true
          description: Target currency code (e.g., "USD")
          schema:
            type: string
            minLength: 3
            maxLength: 10
            example: USD
        - in: query
          name: fromAmount
          required: false
          description: |
            Amount in source currency (fromCurrency). Mutually exclusive with toAmount.
            If provided, response includes calculated amounts.
          schema:
            type: string
            pattern: ^[0-9]+(\.[0-9]+)?$
            example: '1000.00'
        - in: query
          name: toAmount
          required: false
          description: |
            Amount in target currency (toCurrency). Mutually exclusive with fromAmount.
            If provided, response includes calculated amounts.
          schema:
            type: string
            pattern: ^[0-9]+(\.[0-9]+)?$
            example: '0.01234'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IndicativeQuoteResponse'
              examples:
                indicativeQuote:
                  summary: Indicative quote example
                  value:
                    rate: '45000.00'
                    amounts:
                      from:
                        amount: '0.1'
                        currency: BTC
                      to:
                        amount: '4500.00'
                        currency: USD
        '400':
          description: Bad request (invalid parameters)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '403':
          description: Forbidden (partner not active or insufficient permissions)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '404':
          description: Not found (currency pair not supported or not allowed for partner)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '429':
          description: Too Many Requests (rate limit exceeded)
          headers:
            Retry-After:
              description: Number of seconds to wait before retrying
              schema:
                type: integer
                example: 60
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '503':
          description: Service unavailable (market data provider unavailable)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        default:
          description: Unexpected error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
  /v1/quotes/fixed:
    get:
      tags:
        - Quotes
      summary: Get fixed-rate quote
      description: |
        Returns a fixed-rate (binding) exchange quote for a currency pair with guaranteed execution.
        The rate is locked for a specified validity period and guaranteed for execution
        if the exchange is created within the validity window using the returned `quoteId`.

        **Key Differences from Indicative Quotes:**
        - Rate is binding and guaranteed for execution within validity window
        - Exactly one of `fromAmount` or `toAmount` is required
        - Returns `quoteId` that must be used when creating the exchange

        **Quote Reuse:**
        Each quote can only be used once. After an exchange is created with a quoteId,
        that quoteId cannot be reused. Partners must request a new quote for subsequent exchanges.

        **Parameters:**
        - `fromCurrency` and `toCurrency` are required
        - Exactly one of `fromAmount` or `toAmount` is required (mutually exclusive):
          - `fromAmount`: amount in source currency (e.g., "I want to exchange 0.1 BTC, how much USD will I receive?")
          - `toAmount`: amount in target currency (e.g., "I want to receive 4500 USD, how much BTC do I need to send?")

        **Note:** Routing information (source/destination) should be specified when creating the exchange, not in the quote request.

        Requires partner authentication with 'exchange:read' scope.
      operationId: getFixedQuote
      security:
        - BearerAuth: []
      parameters:
        - in: header
          name: Authorization
          required: true
          description: Bearer token for partner authentication (JWT)
          schema:
            type: string
            example: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
        - in: query
          name: fromCurrency
          required: true
          description: Source currency code (e.g., "BTC")
          schema:
            type: string
            minLength: 3
            maxLength: 10
            example: BTC
        - in: query
          name: toCurrency
          required: true
          description: Target currency code (e.g., "USD")
          schema:
            type: string
            minLength: 3
            maxLength: 10
            example: USD
        - in: query
          name: fromAmount
          required: false
          description: |
            Amount in source currency (fromCurrency). Mutually exclusive with toAmount.
            Exactly one of fromAmount or toAmount is required.
          schema:
            type: string
            pattern: ^[0-9]+(\.[0-9]+)?$
            example: '1000.00'
        - in: query
          name: toAmount
          required: false
          description: |
            Amount in target currency (toCurrency). Mutually exclusive with fromAmount.
            Exactly one of fromAmount or toAmount is required.
          schema:
            type: string
            pattern: ^[0-9]+(\.[0-9]+)?$
            example: '0.01234'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FixedQuoteResponse'
              examples:
                fixedQuote:
                  summary: Fixed-rate quote example
                  value:
                    quoteId: quote-abc123
                    rate: '45000.00'
                    amounts:
                      from:
                        amount: '0.1'
                        currency: BTC
                      to:
                        amount: '4500.00'
                        currency: USD
                    validity:
                      validUntil: '2023-11-04T10:35:16.789Z'
        '400':
          description: Bad request (invalid parameters)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '403':
          description: Forbidden (partner not active or insufficient permissions)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '404':
          description: Not found (currency pair not supported or not allowed for partner)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '429':
          description: Too Many Requests (rate limit exceeded)
          headers:
            Retry-After:
              description: Number of seconds to wait before retrying
              schema:
                type: integer
                example: 60
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '503':
          description: Service unavailable (market data provider unavailable)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        default:
          description: Unexpected error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
  /v1/addresses/validate:
    post:
      tags:
        - Reference Data
      summary: Validate a crypto deposit address
      description: |
        Validates a cryptocurrency address for a given currency and network without creating any exchange
        or deposit-address resource. Intended for partner UIs to give users immediate feedback before
        submitting an on-ramp exchange.

        **Domain answer vs request error:**
        - A syntactically or semantically invalid address for a known currency/network is a successful
          domain answer, not an error. The endpoint returns `200 OK` with `valid: false` and a machine-readable
          `reason` (e.g. `INVALID_FORMAT`, `INVALID_CHECKSUM`, `NETWORK_MISMATCH`).
        - An unknown or unsupported currency, network, or a malformed request body is a client error and
          returns `400 Bad Request`.
        - If address validation for the requested currency/network is temporarily disabled (kill switch or
          validator dependency unavailable), the endpoint returns `503 Service Unavailable`. Partners should
          retry after the indicated `Retry-After` interval.

        **MISSING_TAG behavior:**
        For currencies that require a destination tag / memo (e.g. XRP, XLM), omitting the tag yields
        `200 OK` with `valid: false` and `reason: MISSING_TAG`.

        Requires partner authentication with 'exchange:read' scope.
      operationId: validateAddress
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ValidateAddressRequest'
      responses:
        '200':
          description: OK (domain answer — check `valid` and `reason` for invalid addresses)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidateAddressResponse'
        '400':
          description: Bad request (malformed body, unknown currency, or unsupported network)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '403':
          description: Forbidden (partner not active or insufficient permissions)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '429':
          description: Too Many Requests (rate limit exceeded)
          headers:
            Retry-After:
              description: Number of seconds to wait before retrying
              schema:
                type: integer
                example: 60
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '503':
          description: Service unavailable (address validation disabled for this currency/network or validator dependency unavailable)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        default:
          description: Unexpected error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
  /v1/currencies:
    get:
      tags:
        - Reference Data
      summary: Get supported currencies
      description: |
        Returns list of supported currencies with optional filtering by type, network, and partner enablement.

        **enabledOnly:** When `enabledOnly=true`, returns only currencies that are globally enabled and enabled for the authenticated partner (based on partner exchange configuration). When omitted or `false`, returns currencies based solely on `type`, `network`, and `currency` filters.

        Requires partner authentication with 'exchange:read' scope.
      operationId: getCurrencies
      security:
        - BearerAuth: []
      parameters:
        - in: query
          name: type
          description: Filter by currency type
          schema:
            $ref: '#/components/schemas/CurrencyType'
        - in: query
          name: network
          description: Filter by network (e.g., "ethereum", "bitcoin")
          schema:
            type: string
        - in: query
          name: currency
          description: Filter by currency code (e.g., "USDT", "BTC")
          schema:
            type: string
            minLength: 3
            maxLength: 10
        - in: query
          name: enabledOnly
          required: false
          description: |
            When true, returns only currencies that are globally enabled and enabled for the authenticated partner.
            When omitted or false, returns currencies based solely on type, network, and currency filters.
          schema:
            type: boolean
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CurrencyListResponse'
        '400':
          description: Bad request (invalid type parameter or query parameters)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '403':
          description: Forbidden (partner not active or insufficient permissions)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '404':
          description: Not found (no currencies match the specified filters)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '429':
          description: Too Many Requests (rate limit exceeded)
          headers:
            Retry-After:
              description: Number of seconds to wait before retrying
              schema:
                type: integer
                example: 60
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '503':
          description: Service unavailable (reference data service unavailable)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        default:
          description: Unexpected error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
  /v1/currency-pairs:
    get:
      tags:
        - Reference Data
      summary: Get supported currency pairs (canonical market pairs)
      description: |
        Returns the list of canonical market pairs (one row per crypto-fiat
        pair, e.g., BTC/USD).

        **New integrations should use `/v1/conversion-routes` instead.** That
        endpoint expresses each direction as a separate row using the same
        directional `fromCurrency` / `toCurrency` semantics as `/v1/quotes`
        and `POST /v1/exchanges`. This endpoint is retained for back-compat
        and treats the pair canonically (one BTC/USD row carries both
        on-ramp and off-ramp via the `directions[]` field).

        **Currency Pair Enablement:**
        - When `enabledOnly=true`, returns only currency pairs that are globally enabled and enabled for the authenticated partner (based on partner exchange configuration).
        - When omitted or `false`, returns currency pairs based solely on `fromCurrency` and `toCurrency` filters.

        **Currency Pair Directionality:**
        - Filtering works canonically: `fromCurrency=BTC` returns pairs where BTC is the canonical source (e.g., BTC/USD). It does NOT return USD->BTC on-ramps. For directional discovery, use `/v1/conversion-routes`.
        - Partners can restrict directions for specific pairs using exchange configuration (contact Spike team for more information).

        Requires partner authentication with 'exchange:read' scope.
      operationId: getCurrencyPairs
      security:
        - BearerAuth: []
      parameters:
        - in: header
          name: Authorization
          required: true
          description: Bearer token for partner authentication (JWT)
          schema:
            type: string
            example: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
        - in: query
          name: fromCurrency
          description: Filter by source currency
          schema:
            type: string
            minLength: 1
            maxLength: 10
        - in: query
          name: toCurrency
          description: Filter by target currency
          schema:
            type: string
            minLength: 1
            maxLength: 10
        - in: query
          name: enabledOnly
          required: false
          description: |
            When true, returns only currency pairs that are globally enabled and enabled for the authenticated partner.
            When omitted or false, returns currency pairs based solely on fromCurrency and toCurrency filters.
          schema:
            type: boolean
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CurrencyPairListResponse'
        '400':
          description: Bad request (invalid query parameters)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '403':
          description: Forbidden (partner not active or insufficient permissions)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '404':
          description: Not found (no currency pairs match the specified filters)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '429':
          description: Too Many Requests (rate limit exceeded)
          headers:
            Retry-After:
              description: Number of seconds to wait before retrying
              schema:
                type: integer
                example: 60
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '503':
          description: Service unavailable (market data provider unavailable)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        default:
          description: Unexpected error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
  /v1/conversion-routes:
    get:
      tags:
        - Reference Data
      summary: Get supported conversion routes
      x-spike-empty-result-semantics: |
        When the supplied filter combination matches no routes, the endpoint
        returns 200 with an empty `routes` array. 404 is reserved for path-not-
        found, not for empty filter results.
      description: |
        Returns the list of directional conversion routes the authenticated
        partner can execute. Each row is one direction (ON_RAMP or OFF_RAMP)
        of a fiat<->crypto pair, expressed using the same fromCurrency /
        toCurrency semantics as `/v1/quotes` and `POST /v1/exchanges`.

        **Recommended discovery flow for partners.** Use this endpoint to
        decide whether a specific direction is supported. For an on-ramp
        USD -> BTC, query with `fromCurrency=USD&toCurrency=BTC&type=ON_RAMP`
        and verify a row is returned with `enabled=true` — no need to read a
        canonical pair and decode a `directions[]` array.

        **Filters.** All filters are optional and are AND-ed together.
        - `fromCurrency` / `toCurrency` filter on the directional source and
          target codes.
        - `type` filters on direction (ON_RAMP or OFF_RAMP). When omitted,
          both directions are returned.
        - `enabledOnly=true` returns only routes that are globally enabled
          and enabled for the authenticated partner. When omitted or false,
          disabled routes are also returned with `enabled=false`.

        Requires partner authentication with 'exchange:read' scope.

        **Relationship to `/v1/currency-pairs`.** `/v1/currency-pairs`
        returns canonical market pairs (one row per crypto-fiat pair, e.g.,
        BTC/USD) and is intended for internal/legacy use. New integrations
        should use `/v1/conversion-routes`, which expresses each direction
        as a separate row using the same direction-aware terminology as the
        rest of the public API.
      operationId: getConversionRoutes
      security:
        - BearerAuth: []
      parameters:
        - in: query
          name: fromCurrency
          description: Filter by source currency code (e.g., "USD" for on-ramp, "BTC" for off-ramp).
          schema:
            type: string
            minLength: 1
            maxLength: 10
        - in: query
          name: toCurrency
          description: Filter by target currency code (e.g., "BTC" for on-ramp, "USD" for off-ramp).
          schema:
            type: string
            minLength: 1
            maxLength: 10
        - in: query
          name: type
          description: Filter by route direction. When omitted, both directions are returned.
          schema:
            $ref: '#/components/schemas/ConversionRouteType'
        - in: query
          name: enabledOnly
          required: false
          description: |
            When true, returns only routes that are globally enabled and
            enabled for the authenticated partner. When omitted or false,
            disabled routes are also returned with enabled=false.
          schema:
            type: boolean
      responses:
        '200':
          description: |
            OK. Returned with an empty `routes` array when the supplied filter
            combination matches no routes (this is not a 404 case).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConversionRouteListResponse'
        '400':
          description: Bad request (invalid query parameters)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '403':
          description: Forbidden (partner not active or insufficient permissions)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '429':
          description: Too Many Requests (rate limit exceeded)
          headers:
            Retry-After:
              description: Number of seconds to wait before retrying
              schema:
                type: integer
                example: 60
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '503':
          description: Service unavailable (market data provider unavailable)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        default:
          description: Unexpected error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
  /v1/deposit-addresses:
    post:
      tags:
        - Deposit Addresses
      summary: Create reusable deposit address
      description: |
        Creates a reusable deposit address for cryptocurrency deposits.
        Each deposit to this address will automatically create a new exchange transaction.
        Addresses can be reused for multiple deposits from the same user/currency pair.
        Requires partner authentication with 'exchange:write' scope.

        **Idempotency:**
        - Use `Idempotency-Key` header for retry protection.
      operationId: createDepositAddress
      security:
        - BearerAuth: []
      parameters:
        - in: header
          name: Authorization
          required: true
          description: Bearer token for partner authentication (JWT)
          schema:
            type: string
            example: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
        - in: header
          name: Idempotency-Key
          required: false
          description: |
            Optional UUID for retry protection. Prevents duplicate address creation on network retries.
            If not provided, server generates UUID automatically.
          schema:
            type: string
            format: uuid
            example: 550e8400-e29b-41d4-a716-446655440000
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateDepositAddressRequest'
      responses:
        '200':
          description: Deposit address created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DepositAddressResponse'
        '400':
          description: Bad request (invalid parameters, validation errors)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '403':
          description: Forbidden (partner not active, insufficient permissions, currency/pair not allowed)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '404':
          description: Not found (required resource not found for address creation)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '409':
          description: |
            Conflict: Duplicate Idempotency-Key detected.
            Returns the address created with the same Idempotency-Key in response body (same as 200 OK response).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DepositAddressResponse'
        '429':
          description: Too Many Requests (rate limit exceeded)
          headers:
            Retry-After:
              description: Number of seconds to wait before retrying
              schema:
                type: integer
                example: 60
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '503':
          description: Service unavailable (deposit address service dependencies unavailable)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        default:
          description: Unexpected error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
    get:
      tags:
        - Deposit Addresses
      summary: List reusable deposit addresses
      description: |
        Retrieves a paginated list of reusable deposit addresses for the authenticated partner.
        Supports filtering by external user ID, currency pair, and pagination.
        Uses cursor-based pagination for efficient handling of large result sets.
        Requires partner authentication with 'exchange:read' scope.

        **Pagination:**
        - First request: Omit `cursor` parameter to get the first page
        - Subsequent requests: Use `nextCursor` value from previous response's pagination object
        - The `hasMore` field indicates if more results are available
        - Cursor is an opaque token - do not modify or decode it
      operationId: listDepositAddresses
      security:
        - BearerAuth: []
      parameters:
        - in: header
          name: Authorization
          required: true
          description: Bearer token for partner authentication (JWT)
          schema:
            type: string
            example: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
        - in: query
          name: status
          required: false
          description: Filter by deposit address status
          schema:
            $ref: '#/components/schemas/DepositAddressStatus'
        - in: query
          name: externalUserId
          required: false
          description: Filter by external user ID
          schema:
            type: string
            maxLength: 255
            example: user-123
        - in: query
          name: fromCurrency
          required: false
          description: Filter by source currency code
          schema:
            type: string
            minLength: 3
            maxLength: 10
            example: BTC
        - in: query
          name: toCurrency
          required: false
          description: Filter by target currency code
          schema:
            type: string
            minLength: 3
            maxLength: 10
            example: USD
        - in: query
          name: limit
          required: false
          description: Results per page (default 20, max 100)
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
            example: 20
        - in: query
          name: cursor
          required: false
          description: Cursor for pagination (opaque token from previous response's nextCursor). Omit for first page.
          schema:
            type: string
            example: eyJkZXBvc2l0QWRkcmVzc0lkIjoiYWRkci14eXo3ODkiLCJ0aW1lc3RhbXAiOjE2OTkxMjM0NTY3ODl9
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DepositAddressListResponse'
        '400':
          description: Bad request (invalid query parameters)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '403':
          description: Forbidden (partner not active or insufficient permissions)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '404':
          description: Not found (no deposit addresses match the specified filters)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '429':
          description: Too Many Requests (rate limit exceeded)
          headers:
            Retry-After:
              description: Number of seconds to wait before retrying
              schema:
                type: integer
                example: 60
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '503':
          description: Service unavailable (deposit address service dependencies unavailable)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        default:
          description: Unexpected error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
  /v1/deposit-addresses/{depositAddressId}:
    get:
      tags:
        - Deposit Addresses
      summary: Get reusable deposit address details
      description: |
        Retrieves details of a specific reusable deposit address by ID.
        Requires partner authentication with 'exchange:read' scope.
      operationId: getDepositAddress
      security:
        - BearerAuth: []
      parameters:
        - in: header
          name: Authorization
          required: true
          description: Bearer token for partner authentication (JWT)
          schema:
            type: string
            example: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
        - in: path
          name: depositAddressId
          required: true
          description: Deposit address identifier
          schema:
            type: string
            maxLength: 255
            example: addr-xyz789
      responses:
        '200':
          description: Deposit address found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DepositAddressResponse'
        '400':
          description: Bad request (invalid depositAddressId format)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '403':
          description: Forbidden (deposit address belongs to different partner)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '404':
          description: Deposit address not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '429':
          description: Too Many Requests (rate limit exceeded)
          headers:
            Retry-After:
              description: Number of seconds to wait before retrying
              schema:
                type: integer
                example: 60
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '503':
          description: Service unavailable (deposit address service dependencies unavailable)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        default:
          description: Unexpected error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
    delete:
      tags:
        - Deposit Addresses
      summary: Disable reusable deposit address
      description: |
        Disables a reusable deposit address, preventing new deposits from creating exchanges.
        Existing exchanges linked to this address continue processing normally.
        Requires partner authentication with 'exchange:write' scope.

        **Behavior:**
        - Sets address status to DISABLED
        - New deposits to disabled address are rejected (no exchange creation)
        - Refund can be requested for deposits to disabled address
        - Existing exchanges continue processing normally
        - Address can be reactivated by creating a new address for the same combination

        **Idempotency:**
        - Multiple DELETE requests for the same address return the same result
        - If address already disabled, returns 409 Conflict with current status
      operationId: cancelDepositAddress
      security:
        - BearerAuth: []
      parameters:
        - in: header
          name: Authorization
          required: true
          description: Bearer token for partner authentication (JWT)
          schema:
            type: string
            example: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
        - in: path
          name: depositAddressId
          required: true
          description: Deposit address identifier
          schema:
            type: string
            maxLength: 255
            example: addr-xyz789
      responses:
        '200':
          description: Deposit address disabled successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DepositAddressResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '403':
          description: Forbidden (deposit address belongs to different partner)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '404':
          description: Deposit address not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '409':
          description: Conflict (deposit address already disabled)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DepositAddressResponse'
        '429':
          description: Too Many Requests (rate limit exceeded)
          headers:
            Retry-After:
              description: Number of seconds to wait before retrying
              schema:
                type: integer
                example: 60
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '503':
          description: Service unavailable (deposit address service dependencies unavailable)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        default:
          description: Unexpected error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
    patch:
      tags:
        - Deposit Addresses
      summary: Update reusable deposit address
      description: |
        Updates a reusable deposit address properties, such as the refund address.
        Requires partner authentication with 'exchange:write' scope.

        **Updatable Fields:**
        - `refundAddress`: Crypto address for refunds. Set to null to remove.

        **Validation:**
        - Refund address network must match deposit address network (same blockchain)
        - Address format must be valid for the specified network
        - Tag required for XRP/XLM and similar networks

        **Effect on Existing Exchanges:**
        - Updates apply to future exchanges created from deposits to this address
        - Existing exchanges retain their original refund address (if any)
      operationId: updateDepositAddress
      security:
        - BearerAuth: []
      parameters:
        - in: header
          name: Authorization
          required: true
          description: Bearer token for partner authentication (JWT)
          schema:
            type: string
            example: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
        - in: path
          name: depositAddressId
          required: true
          description: Deposit address identifier
          schema:
            type: string
            maxLength: 255
            example: addr-xyz789
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateDepositAddressRequest'
      responses:
        '200':
          description: Deposit address updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DepositAddressResponse'
        '400':
          description: Bad request (invalid parameters, network mismatch, invalid address format)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '403':
          description: Forbidden (deposit address belongs to different partner)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '404':
          description: Deposit address not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '429':
          description: Too Many Requests (rate limit exceeded)
          headers:
            Retry-After:
              description: Number of seconds to wait before retrying
              schema:
                type: integer
                example: 60
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '503':
          description: Service unavailable (deposit address service dependencies unavailable)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        default:
          description: Unexpected error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
  /v1/webhooks/public-key:
    get:
      tags:
        - Webhooks
      summary: Get webhook public key
      description: |
        Returns the RSA public key for verifying webhook signatures.

        **Usage:**
        1. Call this endpoint once and cache the public key
        2. Use the public key to verify `X-Webhook-Signature` on incoming webhooks
        3. Re-fetch if you receive a webhook with a different `X-Webhook-Key-Id`

        **Key Details:**
        - Algorithm: RSA-SHA256 (4096-bit RSA key)
        - Format: Base64-encoded X.509/SPKI public key
        - One key per partner (not per webhook)
      operationId: getPublicKey
      security:
        - BearerAuth: []
      parameters:
        - in: header
          name: Authorization
          required: true
          description: Bearer token for partner authentication (JWT)
          schema:
            type: string
            example: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
      responses:
        '200':
          description: Public key for signature verification
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicKeyResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '403':
          description: Forbidden (partner not active or insufficient permissions)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '404':
          description: No key found (register a webhook first to generate a key)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '429':
          description: Too Many Requests (rate limit exceeded)
          headers:
            Retry-After:
              description: Number of seconds to wait before retrying
              schema:
                type: integer
                example: 60
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '503':
          description: Service unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        default:
          description: Unexpected error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
  /v1/webhooks:
    post:
      tags:
        - Webhooks
      summary: Register webhook
      description: |
        Registers a webhook endpoint for exchange event notifications.

        **Note:** No secret is required. Webhooks are signed using asymmetric RSA-SHA256
        signatures. Use `GET /v1/webhooks/public-key` to download your public key for
        verification.

        **Example:**
        ```json
        {
          "url": "https://partner.com/webhooks/exchange",
          "events": ["deposit.received", "withdraw.completed", "exchange.completed", "exchange.failed", "exchange.cancelled"],
          "description": "Production webhook"
        }
        ```
      operationId: registerWebhook
      security:
        - BearerAuth: []
      parameters:
        - in: header
          name: Authorization
          required: true
          description: Bearer token for partner authentication (JWT)
          schema:
            type: string
            example: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RegisterWebhookRequest'
      responses:
        '200':
          description: Webhook registered
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookResponse'
        '400':
          description: Bad request (invalid parameters, validation errors)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '403':
          description: Forbidden (partner not active or insufficient permissions)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '404':
          description: Not found (required resource not found for webhook registration)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '429':
          description: Too Many Requests (rate limit exceeded)
          headers:
            Retry-After:
              description: Number of seconds to wait before retrying
              schema:
                type: integer
                example: 60
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '503':
          description: Service unavailable (webhook service dependencies unavailable)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        default:
          description: Unexpected error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
    get:
      tags:
        - Webhooks
      summary: List webhooks
      description: |
        Retrieves a paginated list of webhooks for the authenticated partner.
        Uses cursor-based pagination for efficient handling of large result sets.
        Requires partner authentication with 'exchange:read' scope.
        Partners can only access their own webhooks.

        **Pagination:**
        - First request: Omit `cursor` parameter to get the first page
        - Subsequent requests: Use `nextCursor` value from previous response's pagination object
        - The `hasMore` field indicates if more results are available
        - Cursor is an opaque token - do not modify or decode it
      operationId: listWebhooks
      security:
        - BearerAuth: []
      parameters:
        - in: header
          name: Authorization
          required: true
          description: Bearer token for partner authentication (JWT)
          schema:
            type: string
            example: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
        - in: query
          name: limit
          required: false
          description: Results per page (default 50, max 100)
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
            example: 50
        - in: query
          name: cursor
          required: false
          description: Cursor for pagination (opaque token from previous response's nextCursor). Omit for first page.
          schema:
            type: string
            example: eyJ3ZWJob29rSWQiOiJ3aC0xMjMifQ
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookListResponse'
        '400':
          description: Bad request (invalid query parameters)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '403':
          description: Forbidden (partner not active or insufficient permissions)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '404':
          description: Not found (no webhooks match the specified filters)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '429':
          description: Too Many Requests (rate limit exceeded)
          headers:
            Retry-After:
              description: Number of seconds to wait before retrying
              schema:
                type: integer
                example: 60
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '503':
          description: Service unavailable (webhook service dependencies unavailable)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        default:
          description: Unexpected error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
  /v1/webhooks/{webhookId}:
    get:
      tags:
        - Webhooks
      summary: Get webhook
      description: |
        Retrieves a specific webhook by ID.
        Requires partner authentication with 'exchange:read' scope.
        Partners can only access their own webhooks.
      operationId: getWebhook
      security:
        - BearerAuth: []
      parameters:
        - in: header
          name: Authorization
          required: true
          description: Bearer token for partner authentication (JWT)
          schema:
            type: string
            example: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
        - in: path
          name: webhookId
          required: true
          description: Unique identifier of the webhook
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Webhook details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookResponse'
        '400':
          description: Bad request (invalid webhookId format)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '404':
          description: Webhook not found or not accessible
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '429':
          description: Too Many Requests (rate limit exceeded)
          headers:
            Retry-After:
              description: Number of seconds to wait before retrying
              schema:
                type: integer
                example: 60
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '503':
          description: Service unavailable (webhook service dependencies unavailable)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        default:
          description: Unexpected error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
    delete:
      tags:
        - Webhooks
      summary: Delete webhook
      operationId: deleteWebhook
      security:
        - BearerAuth: []
      parameters:
        - in: header
          name: Authorization
          required: true
          description: Bearer token for partner authentication (JWT)
          schema:
            type: string
            example: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
        - in: path
          name: webhookId
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Webhook deleted successfully
        '400':
          description: Bad request (invalid webhookId format)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '403':
          description: Forbidden (webhook belongs to different partner)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '404':
          description: Webhook not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '429':
          description: Too Many Requests (rate limit exceeded)
          headers:
            Retry-After:
              description: Number of seconds to wait before retrying
              schema:
                type: integer
                example: 60
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '503':
          description: Service unavailable (webhook service dependencies unavailable)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        default:
          description: Unexpected error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
  /v1/webhooks/{webhookId}/activate:
    post:
      tags:
        - Webhooks
      summary: Activate webhook
      description: |
        Reactivates a SUSPENDED webhook. Resets the failure counter to zero.
        Only webhooks in SUSPENDED status can be activated.
        The webhook will resume receiving event deliveries after activation.
      operationId: activateWebhook
      security:
        - BearerAuth: []
      parameters:
        - in: path
          name: webhookId
          required: true
          description: Unique identifier of the webhook to activate
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Webhook activated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookResponse'
        '400':
          description: Bad request (webhook is not in SUSPENDED status)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '404':
          description: Webhook not found or not accessible
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '429':
          description: Too Many Requests (rate limit exceeded)
          headers:
            Retry-After:
              description: Number of seconds to wait before retrying
              schema:
                type: integer
                example: 60
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        default:
          description: Unexpected error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
  /v1/webhooks/{webhookId}/failed-events:
    get:
      tags:
        - Webhooks
      summary: List failed webhook events
      description: |
        Retrieves a paginated list of events that failed delivery to this webhook.
        Uses cursor-based pagination for efficient handling of large result sets.
        Partners can use this endpoint to identify missed events and retry them individually.
      operationId: listFailedEvents
      security:
        - BearerAuth: []
      parameters:
        - in: path
          name: webhookId
          required: true
          description: Unique identifier of the webhook
          schema:
            type: string
            format: uuid
        - in: query
          name: limit
          required: false
          description: Results per page (default 50, max 100)
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
            example: 50
        - in: query
          name: cursor
          required: false
          description: Cursor for pagination (opaque token from previous response's nextCursor). Omit for first page.
          schema:
            type: string
      responses:
        '200':
          description: List of failed webhook events
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FailedWebhookEventListResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '404':
          description: Webhook not found or not accessible
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '429':
          description: Too Many Requests (rate limit exceeded)
          headers:
            Retry-After:
              description: Number of seconds to wait before retrying
              schema:
                type: integer
                example: 60
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        default:
          description: Unexpected error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
  /v1/webhooks/{webhookId}/events/{eventId}/retry:
    post:
      tags:
        - Webhooks
      summary: Retry failed webhook event
      description: |
        Retries delivery of a specific failed event to the webhook.
        The webhook must be in ACTIVE status for retry to succeed.
        The event must be in FAILED status.
      operationId: retryFailedEvent
      security:
        - BearerAuth: []
      parameters:
        - in: path
          name: webhookId
          required: true
          description: Unique identifier of the webhook
          schema:
            type: string
            format: uuid
        - in: path
          name: eventId
          required: true
          description: Unique identifier of the event to retry
          schema:
            type: string
      responses:
        '200':
          description: Event retry initiated successfully
        '400':
          description: Bad request (webhook not active or event not in failed status)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '404':
          description: Webhook or event not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        '429':
          description: Too Many Requests (rate limit exceeded)
          headers:
            Retry-After:
              description: Number of seconds to wait before retrying
              schema:
                type: integer
                example: 60
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
        default:
          description: Unexpected error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientError'
components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      in: header
      name: Authorization
      description: OAuth 2.0 JWT Bearer token for partner authentication. Enter your bearer token in the format **Bearer &lt;token>**
  schemas:
    ApiError:
      title: ApiError
      type: object
      properties:
        internalErrorCode:
          type: integer
          format: int64
        internalErrorDescription:
          type: string
        internalErrorDetails:
          type: object
        clientErrorDescription:
          type: string
        clientErrorDetails:
          type: object
      required:
        - internalErrorCode
    ClientError:
      title: ClientError
      type: object
      properties:
        code:
          type: integer
          format: int64
        message:
          type: string
        traceId:
          type: string
        details:
          type: object
      required:
        - code
    WebhookPayload:
      type: object
      description: |
        Webhook payload delivered to partner endpoints.
        Uses nested structure with exchange data under `exchange` object for scalability.
      required:
        - eventId
        - eventType
        - occurredAt
        - exchange
      properties:
        eventId:
          type: string
          description: Unique event identifier for idempotency
          example: evt-123
        eventType:
          $ref: '#/components/schemas/WebhookEventType'
        occurredAt:
          type: string
          format: date-time
          description: Event occurrence timestamp (ISO 8601 format)
          example: '2023-11-04T10:35:56.789Z'
        approvalToken:
          type: string
          nullable: true
          description: |
            Signed dual-party-control approval token, present **only** on `withdraw.completed`
            events for on-ramp exchanges. Binds the original TOTP authorization to the exact
            transaction parameters (amount, currency, destination address, bitfinex_trx_id)
            via a commitment hash, enabling the partner to mark its server-side token as
            consumed and reject any subsequent out-of-band reuse attempts. Opaque to the
            partner for routing purposes — correlate the lifecycle by `exchange.exchangeId`.
          example: eyJhbGciOiJSUzI1NiJ9.eyJjb21taXRtZW50X2hhc2giOiIuLi4ifQ.sig
        authorizationContext:
          nullable: true
          description: |
            Present **only** on `exchange.authorization_required` events. Contains the
            parameters the partner needs to compute a commitment hash and issue an
            approval token. After receiving this webhook the partner calls
            `POST /v1/exchanges/{exchangeId}/authorization/approve` with the approval
            material, or `/reject` to decline.
          $ref: '#/components/schemas/AuthorizationContext'
        exchange:
          type: object
          description: Exchange transaction data
          required:
            - exchangeId
            - externalReference
            - status
            - currencyPair
          properties:
            exchangeId:
              type: string
              format: uuid
              description: Exchange transaction ID
              example: 550e8400-e29b-41d4-a716-446655440000
            externalReference:
              $ref: '#/components/schemas/ExternalReference'
              description: Partner-provided external identifiers
            status:
              $ref: '#/components/schemas/ExchangeStatus'
              description: Current exchange status
            failReason:
              $ref: '#/components/schemas/ExchangeFailReason'
              nullable: true
              description: |
                Reason for exchange failure. Present only when status is FAILED.
                Partners should use this to determine whether to surface the failure to end users.
            failDescription:
              type: string
              nullable: true
              description: |
                Human-readable failure description with context (e.g., "Dust deposit: 0.01 USDT on sol network").
                Present only when status is FAILED.
            currencyPair:
              $ref: '#/components/schemas/CurrencyPair'
              description: Source and target currencies
            providerExecutionInfo:
              $ref: '#/components/schemas/ProviderExecutionInfo'
              nullable: true
              description: Provider execution information (provider order IDs and symbol)
            amounts:
              $ref: '#/components/schemas/ExchangeAmounts'
              nullable: true
              description: Exchange amounts and execution rate (null until exchange executed)
            flow:
              $ref: '#/components/schemas/ExchangeFlow'
              nullable: true
              description: Exchange flow configuration (source and destination types)
            depositAddress:
              $ref: '#/components/schemas/CryptoAddress'
              nullable: true
              description: Crypto deposit address where user sends cryptocurrency (off-ramp only, null for on-ramp)
            cryptoDeposit:
              $ref: '#/components/schemas/CryptoDeposit'
              nullable: true
              description: |
                Cryptocurrency deposit transaction (off-ramp only, null for on-ramp).
                Represents the blockchain transaction sent to the depositAddress.
            fiatDeposit:
              $ref: '#/components/schemas/FiatDeposit'
              nullable: true
              description: |
                Fiat deposit/funding transaction (on-ramp only, null for off-ramp).
                Represents partner-confirmed fiat funding for the exchange.
            authorizationContext:
              $ref: '#/components/schemas/AuthorizationContext'
              nullable: true
              description: |
                Authorization context for on-ramp crypto withdrawal authorization.
                Present only when eventType == exchange.authorization_required and the flow is on-ramp.
            cryptoRefunds:
              type: array
              nullable: true
              description: |
                Array of cryptocurrency refund transactions for this exchange. Refunds are independent of exchange status
                and can occur multiple times, including after exchange completion.
              items:
                $ref: '#/components/schemas/CryptoRefund'
            cryptoWithdrawals:
              type: array
              nullable: true
              description: |
                Array of cryptocurrency withdrawal transactions for this exchange (on-ramp only).
                Multiple withdrawals can exist due to retries after failure.
              items:
                $ref: '#/components/schemas/CryptoWithdrawal'
            fiatWithdrawals:
              type: array
              nullable: true
              description: |
                Array of fiat withdrawal/payout transactions for this exchange (off-ramp only).
                Multiple withdrawals can exist due to retries after failure.
              items:
                $ref: '#/components/schemas/FiatWithdrawal'
            timestamps:
              $ref: '#/components/schemas/ExchangeTimestamps'
              nullable: true
              description: Exchange lifecycle timestamps
    TokenResponse:
      title: TokenResponse
      type: object
      required:
        - access_token
        - token_type
        - expires_in
      properties:
        access_token:
          type: string
          description: JWT access token for API authentication
        token_type:
          type: string
          description: Token type (always "Bearer")
          example: Bearer
        expires_in:
          type: integer
          description: Access token expiration time in seconds
          example: 300
        refresh_token:
          type: string
          description: |
            Refresh token for obtaining new access tokens without re-authentication.
            Present in initial token response (client_credentials grant).
            Present in refresh response (refresh_token grant) with new refresh token (token rotation).
            Partners should store refresh tokens securely and use them to refresh access tokens before expiration.
          example: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
        refresh_expires_in:
          type: integer
          description: |
            Refresh token expiration time in seconds (e.g., 2592000 for 30 days).
            Present when refresh_token is included in response.
          example: 2592000
        scope:
          type: string
          description: Granted scopes (space-separated, e.g., "exchange:read exchange:write")
    ExchangeStatus:
      type: string
      enum:
        - WAITING_DEPOSIT
        - DEPOSIT_PENDING
        - AWAITING_AUTHORIZATION
        - PROCESSING
        - COMPLETED
        - FAILED
        - CANCELLATION_REQUESTED
        - CANCELLED
      description: |
        Exchange transaction status. Represents the main exchange lifecycle.

        **Note:** An exchange has one deposit and can have multiple refunds, each with their own status.
        - deposit.status: PENDING → COMPLETED (for crypto deposit)
        - refunds[].status: PENDING → INITIATED → COMPLETED (per refund)
        Refunds are independent of exchange status and can occur even after COMPLETED.

        **Status Values:**
        - WAITING_DEPOSIT: Deposit address assigned, waiting for deposit
        - DEPOSIT_PENDING: At least one deposit detected, awaiting confirmations
        - AWAITING_AUTHORIZATION: Deposit confirmed, waiting for partner authorization (approve/reject via API)
        - PROCESSING: Trade execution in progress on exchange
        - COMPLETED: Main exchange completed (refunds may still occur)
        - FAILED: Exchange failed
        - CANCELLATION_REQUESTED: Cancel request accepted, awaiting completion
        - CANCELLED: Exchange cancelled (final status)
    ExternalReference:
      title: ExternalReference
      type: object
      required:
        - externalId
        - externalUserId
      description: Partner-provided external identifiers for transaction and user tracking
      properties:
        externalId:
          type: string
          description: |
            Partner's transaction identifier for business tracking and reconciliation.
            **Required** - Used for reconciliation, cancel operations, webhook correlation, and exchange lookup.
            Must be unique per partner. Duplicate externalId returns 409 Conflict with existing exchange.
          minLength: 1
          maxLength: 255
          example: partner-tx-456
        externalUserId:
          type: string
          description: Partner's user identifier (not a Spike user)
          minLength: 1
          maxLength: 255
          example: user-123
    CurrencyPair:
      title: CurrencyPair
      type: object
      required:
        - fromCurrency
        - toCurrency
      description: Currency pair representing source and target currencies for exchange
      properties:
        fromCurrency:
          type: string
          description: Source currency code (e.g., "BTC", "USD")
          minLength: 3
          maxLength: 10
          example: BTC
        toCurrency:
          type: string
          description: Target currency code (e.g., "USD", "BTC")
          minLength: 3
          maxLength: 10
          example: USD
    ProviderExecutionInfo:
      title: ProviderExecutionInfo
      type: object
      description: |
        Information about execution on the underlying liquidity provider.
        Contains provider-specific identifiers required for independent execution validation.
      properties:
        providerOrderIds:
          type: array
          nullable: true
          description: |
            Array of order IDs submitted to the provider during exchange execution.
            Append-only: includes all orders ever used for this exchange.

            **Timing note:** This field is populated *eventually* during the PROCESSING phase,
            not synchronously with the `PROCESSING` status transition. The underlying liquidity
            provider may accept orders via an asynchronous submission pipeline, so there is a
            brief window (typically a few seconds) during which `status=PROCESSING` but
            `providerOrderIds` is still empty/absent. Integrations that need to act on the
            provider order ID (e.g. for reconciliation, monitoring, or cancellation) should
            poll on the presence of `providerOrderIds` directly rather than treating
            `status=PROCESSING` as a guarantee that the field is populated.
          items:
            type: string
          example:
            - '12345678'
        providerSymbol:
          type: string
          nullable: true
          description: |
            Trading symbol used on the liquidity provider for this exchange.
            Follows provider-specific naming conventions (e.g., Bitfinex: "tBTCUSD", "tBCHN:USD").
            Partners can use this to cross-reference transactions directly with the provider.
          example: tBTCUSD
    CurrencyAmount:
      title: CurrencyAmount
      type: object
      required:
        - amount
        - currency
      description: Amount with associated currency code. Uses string for precision in financial calculations.
      properties:
        amount:
          type: string
          description: Amount value (decimal string for precision)
          pattern: ^[0-9]+(\.[0-9]+)?$
          example: '0.1'
        currency:
          type: string
          description: Currency code (e.g., "BTC", "USD")
          minLength: 3
          maxLength: 10
          example: BTC
    QuoteSourceType:
      type: string
      enum:
        - ACCOUNT
        - CRYPTOWALLET
      default: ACCOUNT
      description: Source type for the exchange
    QuoteDestinationType:
      type: string
      enum:
        - ACCOUNT
        - CRYPTOWALLET
      default: ACCOUNT
      description: Destination type for the exchange
    ExchangeFlow:
      title: ExchangeFlow
      type: object
      required:
        - source
        - destination
      description: Exchange flow configuration specifying source and destination types
      properties:
        source:
          $ref: '#/components/schemas/QuoteSourceType'
          description: Source type for exchange
        destination:
          $ref: '#/components/schemas/QuoteDestinationType'
          description: Destination type for exchange
    ExchangeTimestamps:
      title: ExchangeTimestamps
      type: object
      required:
        - createdAt
      description: Temporal information for exchange transaction lifecycle
      properties:
        createdAt:
          type: string
          format: date-time
          description: Exchange creation timestamp (ISO 8601 format)
          example: '2023-11-04T10:30:56.789Z'
        completedAt:
          type: string
          format: date-time
          nullable: true
          description: Exchange completion timestamp (ISO 8601 format, null if not completed)
          example: '2023-11-04T10:35:56.789Z'
    CryptoAddress:
      title: CryptoAddress
      type: object
      description: Cryptocurrency address with network specification
      required:
        - address
        - network
      properties:
        address:
          type: string
          description: Cryptocurrency address
          minLength: 1
          maxLength: 255
          example: 1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2
        network:
          type: string
          description: Blockchain network (e.g., "bitcoin", "ethereum", "tron")
          minLength: 1
          maxLength: 50
          example: bitcoin
        tag:
          type: string
          description: Deposit tag/memo (required for Ripple/XRP and similar cryptocurrencies). Must be included with address for XRP deposits.
          maxLength: 255
          nullable: true
          example: '1234567890'
        addressId:
          type: string
          nullable: true
          description: |
            Identifier of a reusable deposit address (off-ramp only, null otherwise).
            Multiple exchanges can share the same addressId. Use exchangeId to track a specific exchange.
          maxLength: 255
          example: 550e8400-e29b-41d4-a716-446655440000
    RefundStatus:
      type: string
      enum:
        - PENDING
        - INITIATED
        - COMPLETED
        - FAILED
      description: |
        Refund transaction status. Each refund in the refunds[] array has independent status.
        - PENDING: Refund required but not yet initiated
        - INITIATED: Refund transaction submitted to blockchain
        - COMPLETED: Refund confirmed on blockchain
        - FAILED: Refund failed (manual intervention may be required)
    CryptoRefund:
      title: CryptoRefund
      type: object
      description: Individual cryptocurrency refund transaction information
      required:
        - refundId
        - amount
        - network
        - refundAddress
        - status
        - initiatedAt
      properties:
        refundId:
          type: string
          format: uuid
          description: Unique refund identifier (Payment ID)
          example: 550e8400-e29b-41d4-a716-446655440001
        transactionHash:
          type: string
          nullable: true
          description: Blockchain transaction hash (null until refund completed)
          example: abc123def456...
        amount:
          $ref: '#/components/schemas/CurrencyAmount'
          description: Cryptocurrency refund amount and currency
        network:
          type: string
          description: Blockchain network
          example: bitcoin
        refundAddress:
          $ref: '#/components/schemas/CryptoAddress'
          description: Address refund was sent to
        status:
          $ref: '#/components/schemas/RefundStatus'
          description: Refund status
          example: COMPLETED
        initiatedAt:
          type: string
          format: date-time
          description: Refund initiation timestamp (ISO 8601 format)
          example: '2023-11-04T10:30:56.789Z'
        completedAt:
          type: string
          format: date-time
          nullable: true
          description: Refund completion timestamp (ISO 8601 format, null until completed)
          example: '2023-11-04T10:35:56.789Z'
        reason:
          type: string
          nullable: true
          description: Refund reason
          example: Deposit timeout - exchange expired before deposit received
        networkFee:
          $ref: '#/components/schemas/CurrencyAmount'
          nullable: true
          description: |
            On-chain network fee deducted from `amount` before the refund transaction is
            broadcast. Denominated in the same currency as `amount`. Null until the refund has
            been broadcast and the fee is known.
        netAmount:
          $ref: '#/components/schemas/CurrencyAmount'
          nullable: true
          description: |
            Net amount actually delivered to `refundAddress`, equal to `amount - networkFee`.
            Null until the refund has been broadcast and the fee is known.
    ExchangeSummary:
      type: object
      required:
        - exchangeId
        - externalReference
        - status
        - currencyPair
        - timestamps
      properties:
        exchangeId:
          type: string
          format: uuid
          description: Unique exchange identifier
          example: 550e8400-e29b-41d4-a716-446655440000
        externalReference:
          $ref: '#/components/schemas/ExternalReference'
          description: Partner-provided external identifiers
        status:
          $ref: '#/components/schemas/ExchangeStatus'
          description: Exchange transaction status
        currencyPair:
          $ref: '#/components/schemas/CurrencyPair'
          description: Source and target currencies
        providerExecutionInfo:
          $ref: '#/components/schemas/ProviderExecutionInfo'
          nullable: true
          description: Provider execution information (provider order IDs and symbol)
        fromAmount:
          $ref: '#/components/schemas/CurrencyAmount'
          nullable: true
          description: Exchange amount in source currency
        toAmount:
          $ref: '#/components/schemas/CurrencyAmount'
          nullable: true
          description: Exchange amount in target currency
        flow:
          $ref: '#/components/schemas/ExchangeFlow'
          nullable: true
          description: Exchange flow (source/destination endpoints) as persisted on the ramp
        timestamps:
          $ref: '#/components/schemas/ExchangeTimestamps'
          description: Transaction lifecycle timestamps
        depositSummary:
          type: object
          nullable: true
          description: Summary of deposits for this exchange (null if no deposits yet)
          properties:
            totalDeposits:
              type: integer
              description: Total number of deposits received
              example: 2
            totalAmount:
              type: string
              description: Total amount deposited across all deposits
              example: '0.15'
            currency:
              type: string
              description: Deposit currency
              example: BTC
            network:
              type: string
              description: Deposit network
              example: bitcoin
        cryptoRefunds:
          type: array
          nullable: true
          description: |
            Array of cryptocurrency refund transactions for this exchange. Refunds are independent of exchange status
            and can occur multiple times, including after exchange completion.
          items:
            $ref: '#/components/schemas/CryptoRefund'
    PaginationInfo:
      type: object
      required:
        - limit
        - hasMore
      properties:
        limit:
          type: integer
          description: Results per page
          example: 50
        hasMore:
          type: boolean
          description: Whether more results are available
          example: true
        nextCursor:
          type: string
          nullable: true
          description: Cursor for next page (null if no more results). Use this value in the cursor query parameter for the next request.
          example: eyJleGNoYW5nZUlkIjoiNTUwZTg0MDAtZTI5Yi00MWQ0LWE3MTYtNDQ2NjU1NDQwMDAwIiwidGltZXN0YW1wIjoxNjk5MTIzNDU2Nzg5fQ
    ExchangeListResponse:
      type: object
      required:
        - exchanges
        - pagination
      properties:
        exchanges:
          type: array
          items:
            $ref: '#/components/schemas/ExchangeSummary'
          description: List of exchange transactions
        pagination:
          $ref: '#/components/schemas/PaginationInfo'
          description: Pagination metadata
    CreateExchangeRequest:
      type: object
      required:
        - currencyPair
        - externalReference
      properties:
        currencyPair:
          $ref: '#/components/schemas/CurrencyPair'
          description: Source and target currencies for exchange
        fromAmount:
          type: string
          nullable: true
          pattern: ^[0-9]+(\.[0-9]+)?$
          description: |
            Amount in source currency (currencyPair.fromCurrency).
            Mutually exclusive with toAmount.
            Required for fixed-rate flows. At least one required when depositReference is provided.
          example: '1000.00'
        toAmount:
          type: string
          nullable: true
          pattern: ^[0-9]+(\.[0-9]+)?$
          description: |
            Amount in target currency (currencyPair.toCurrency).
            Mutually exclusive with fromAmount.
            Required for fixed-rate flows. At least one required when depositReference is provided.
          example: '0.01234'
        flow:
          $ref: '#/components/schemas/ExchangeFlow'
          nullable: true
          description: |
            Exchange flow configuration. Optional - defaults to ACCOUNT for both source and destination.
            Specify only when using CRYPTOWALLET as source or destination.
        externalReference:
          $ref: '#/components/schemas/ExternalReference'
          description: Partner-provided external identifiers
        destinationAddress:
          $ref: '#/components/schemas/CryptoAddress'
          nullable: true
          description: |
            Crypto withdrawal address.
            Required if flow.destination is CRYPTOWALLET.
        depositAddressId:
          type: string
          nullable: true
          maxLength: 255
          description: |
            Reusable deposit address identifier obtained from POST /v1/deposit-addresses.
            Required for off-ramps (crypto -> fiat) so the partner knows where to send crypto.
            Ignored for on-ramps.
          example: addr-xyz789
        quoteId:
          type: string
          nullable: true
          description: |
            Quote ID from GET /v1/quotes/fixed. Required for Fixed Rate Mode exchanges.
            When provided, the quote must be valid (not expired).
          maxLength: 255
          example: quote-abc123
        depositReference:
          type: string
          nullable: true
          maxLength: 255
          description: |
            External deposit reference (e.g., payment system movement ID) for the
            **partner-managed funds** on-ramp pattern: the partner has already
            transferred the fiat onto the exchange platform themselves and is
            asserting that movement here. A fresh exchange normally starts in
            WAITING_DEPOSIT while Spike accepts and processes that assertion
            asynchronously, then advances to PROCESSING.

            Requires `fromAmount` or `toAmount` to also be provided.

            Partners on integrations where Spike controls the fiat accounts and
            executes the movement itself receive a richer settlement object instead
            of (or in addition to) `depositReference`. That richer flow is enabled
            per partner configuration — contact the Spike team for details.
          example: '12345678'
    ExchangeFailReason:
      type: string
      enum:
        - DEPOSIT_BELOW_DUST_THRESHOLD
        - PROVIDER_ORDER_CANCELLED
      description: |
        Reason for exchange failure. Present only when status is FAILED.
        - DEPOSIT_BELOW_DUST_THRESHOLD: The crypto deposit amount was below the configured dust threshold for this currency. Partners should suppress these from end-user views.
        - PROVIDER_ORDER_CANCELLED: The liquidity provider cancelled the exchange order before execution, for example because a fill-or-kill order could not be filled at the requested price.
    ExchangeAmounts:
      title: ExchangeAmounts
      type: object
      description: Exchange amount information including source, target amounts and execution rate
      properties:
        from:
          $ref: '#/components/schemas/CurrencyAmount'
          nullable: true
          description: Source amount (null at creation, populated after deposit/trade)
        to:
          $ref: '#/components/schemas/CurrencyAmount'
          nullable: true
          description: Target amount (null at creation, populated after deposit/trade)
        rate:
          type: string
          nullable: true
          description: |
            Execution rate in canonical pair direction: units of the fiat currency per 1
            unit of the crypto currency. Null at exchange creation, populated after the
            trade executes. Same numerical value regardless of trade direction (see
            `BaseQuoteResponse.rate` for the convention).
          example: '76452.60'
    CryptoDeposit:
      title: CryptoDeposit
      type: object
      description: Individual cryptocurrency deposit transaction information
      required:
        - transactionHash
        - amount
        - network
        - status
        - detectedAt
      properties:
        transactionHash:
          type: string
          description: Blockchain transaction hash
          example: abc123def456...
        amount:
          $ref: '#/components/schemas/CurrencyAmount'
          description: Cryptocurrency amount and currency
        network:
          type: string
          description: Blockchain network
          example: bitcoin
        confirmations:
          type: integer
          nullable: true
          description: Current confirmation count (null if not yet confirmed)
          example: 6
        requiredConfirmations:
          type: integer
          nullable: true
          description: Required confirmations for completion
          example: 3
        status:
          type: string
          enum:
            - PENDING
            - COMPLETED
            - FAILED
          description: |
            Deposit status for the cryptocurrency deposit transaction.
            - PENDING: Deposit detected, awaiting blockchain confirmations
            - COMPLETED: Deposit has blockchain confirmations (ready for trading/refund)
            - FAILED: Deposit transaction failed (e.g. blockchain reorg)
          example: COMPLETED
        detectedAt:
          type: string
          format: date-time
          description: When the deposit transaction was first detected (ISO 8601 format)
          example: '2023-11-04T10:30:56.789Z'
        confirmedAt:
          type: string
          format: date-time
          nullable: true
          description: When the deposit reached required confirmations (null while PENDING or FAILED)
          example: '2023-11-04T10:35:56.789Z'
    FiatDeposit:
      type: object
      description: |
        Fiat deposit/funding transaction for on-ramp exchanges.
        Represents partner-confirmed fiat funding (e.g., debit from user's fiat account).
      required:
        - depositId
        - amount
        - status
        - confirmedAt
      properties:
        depositId:
          type: string
          format: uuid
          description: Unique identifier for this fiat deposit
          example: 550e8400-e29b-41d4-a716-446655440001
        amount:
          $ref: '#/components/schemas/CurrencyAmount'
          description: Fiat amount and currency
        status:
          type: string
          enum:
            - PENDING
            - CONFIRMED
            - FAILED
            - CANCELLED
          description: |
            Fiat deposit status:
            - PENDING: Deposit initiated, awaiting confirmation
            - CONFIRMED: Deposit confirmed by partner
            - FAILED: Deposit failed or rejected
            - CANCELLED: Exchange was cancelled by partner
          example: CONFIRMED
        externalReference:
          type: string
          nullable: true
          description: Partner's reference for this fiat deposit (e.g., internal transfer ID)
          example: partner-fiat-ref-123
        confirmedAt:
          type: string
          format: date-time
          description: Timestamp when fiat deposit was confirmed (ISO 8601)
          example: '2023-11-04T10:30:56.789Z'
    AuthorizationContext:
      type: object
      description: |
        Authorization-required context for on-ramp crypto withdrawal authorization.
        Carries the inputs the partner needs to independently compute and verify
        the commitment hash before approving or rejecting the
        `exchange.authorization_required` event. Present only when
        `eventType == EXCHANGE_AUTHORIZATION_REQUIRED` and the ramp direction is
        ON_RAMP. Null for all other event types and for off-ramp flows.
      required:
        - amount
        - currency
        - destinationAddress
        - bitfinexTrxId
      properties:
        amount:
          type: string
          description: |
            Gross crypto withdrawal amount (before network fee) as a decimal string.
            Currency is given by the `currency` field.
          example: '0.01500000'
        currency:
          type: string
          description: |
            Crypto currency code for the withdrawal (matches onRamp.toCurrency).
          example: BTC
        destinationAddress:
          type: string
          description: |
            Blockchain address the crypto will be sent to on approval.
          example: bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh
        bitfinexTrxId:
          type: string
          description: |
            Bitfinex provider order ID captured when the trade filled.
            First entry from `exchange.providerOrderIds`. The partner uses this to
            verify that the commitment hash matches the filled trade.
          example: '123456789'
    WithdrawalStatus:
      type: string
      enum:
        - PENDING
        - INITIATED
        - COMPLETED
        - FAILED
      description: |
        Withdrawal transaction status. Each withdrawal in the cryptoWithdrawals[] array has independent status.
        - PENDING: Withdrawal required but not yet initiated
        - INITIATED: Withdrawal transaction submitted to blockchain
        - COMPLETED: Withdrawal confirmed on blockchain
        - FAILED: Withdrawal failed (manual intervention may be required)
    CryptoWithdrawal:
      title: CryptoWithdrawal
      type: object
      description: Individual cryptocurrency withdrawal transaction
      required:
        - withdrawalId
        - amount
        - destinationAddress
        - status
        - initiatedAt
      properties:
        withdrawalId:
          type: string
          format: uuid
          description: Unique withdrawal identifier (Payment ID)
          example: 550e8400-e29b-41d4-a716-446655440001
        transactionHash:
          type: string
          nullable: true
          description: Blockchain transaction hash (null until withdrawal completed)
          example: abc123def456...
        amount:
          $ref: '#/components/schemas/CurrencyAmount'
          description: |
            Gross withdrawal amount, equal to the exchange's purchased amount. This is NOT the amount that
            lands at the destination address — the on-chain network fee is deducted from this value before
            the transaction is broadcast. Use `netAmount` for the amount actually delivered to the End User.
        networkFee:
          $ref: '#/components/schemas/CurrencyAmount'
          nullable: true
          description: |
            On-chain network fee deducted from `amount` before the transaction is broadcast.
            Denominated in the same currency as `amount`. Null until the withdrawal has been broadcast and
            the fee is known.
        netAmount:
          $ref: '#/components/schemas/CurrencyAmount'
          nullable: true
          description: |
            Net amount actually delivered to `destinationAddress`, equal to `amount - networkFee`.
            Null until the withdrawal has been broadcast and the fee is known.
        destinationAddress:
          $ref: '#/components/schemas/CryptoAddress'
          description: Address withdrawal was sent to
        status:
          $ref: '#/components/schemas/WithdrawalStatus'
          description: Withdrawal status
          example: COMPLETED
        initiatedAt:
          type: string
          format: date-time
          description: Withdrawal initiation timestamp (ISO 8601 format)
          example: '2023-11-04T10:30:56.789Z'
        completedAt:
          type: string
          format: date-time
          nullable: true
          description: Withdrawal completion timestamp (ISO 8601 format, null until completed)
          example: '2023-11-04T10:35:56.789Z'
    FiatWithdrawal:
      title: FiatWithdrawal
      type: object
      description: Individual fiat withdrawal/payout transaction (off-ramp only)
      required:
        - withdrawalId
        - amount
        - status
        - initiatedAt
      properties:
        withdrawalId:
          type: string
          format: uuid
          description: Unique withdrawal identifier (Payment ID)
          example: 550e8400-e29b-41d4-a716-446655440001
        amount:
          $ref: '#/components/schemas/CurrencyAmount'
          description: Fiat withdrawal amount and currency
        status:
          $ref: '#/components/schemas/WithdrawalStatus'
          description: Withdrawal status
          example: COMPLETED
        paymentMethod:
          type: string
          nullable: true
          description: Payment method used for the fiat payout (e.g., "bank_transfer", "sepa", "swift")
          example: bank_transfer
        paymentReference:
          type: string
          nullable: true
          description: Bank or payment provider confirmation/reference number (null until withdrawal completed)
          example: REF-2024-ABC123
        initiatedAt:
          type: string
          format: date-time
          description: Withdrawal initiation timestamp (ISO 8601 format)
          example: '2023-11-04T10:30:56.789Z'
        completedAt:
          type: string
          format: date-time
          nullable: true
          description: Withdrawal completion timestamp (ISO 8601 format, null until completed)
          example: '2023-11-04T10:35:56.789Z'
    ExchangeResponse:
      type: object
      required:
        - exchangeId
        - externalReference
        - status
        - currencyPair
        - flow
        - timestamps
      properties:
        exchangeId:
          type: string
          format: uuid
          description: Unique exchange identifier
          example: 550e8400-e29b-41d4-a716-446655440000
        externalReference:
          $ref: '#/components/schemas/ExternalReference'
          description: Partner-provided external identifiers (echoed from request)
        status:
          $ref: '#/components/schemas/ExchangeStatus'
          description: Exchange transaction status
        failReason:
          $ref: '#/components/schemas/ExchangeFailReason'
          nullable: true
          description: |
            Reason for exchange failure. Present only when status is FAILED.
            Partners should use this field to determine appropriate end-user messaging
            (e.g., suppress DEPOSIT_BELOW_DUST_THRESHOLD from customer-facing views).
        failDescription:
          type: string
          nullable: true
          description: |
            Human-readable failure description with context (e.g., "Dust deposit: 0.01 USDT on sol network").
            Present only when status is FAILED.
        currencyPair:
          $ref: '#/components/schemas/CurrencyPair'
          description: Source and target currencies
        providerExecutionInfo:
          $ref: '#/components/schemas/ProviderExecutionInfo'
          nullable: true
          description: Provider execution information (provider order IDs and symbol)
        amounts:
          $ref: '#/components/schemas/ExchangeAmounts'
          description: Exchange amounts and execution rate (null at creation, populated after deposit/trade)
        depositAddress:
          $ref: '#/components/schemas/CryptoAddress'
          nullable: true
          description: |
            Crypto deposit address where user sends cryptocurrency (off-ramp only, null for on-ramp).
        destinationAddress:
          $ref: '#/components/schemas/CryptoAddress'
          nullable: true
          description: |
            Crypto withdrawal destination (on-ramp only, null for off-ramp).
            Echoed from the original CreateExchangeRequest so the partner can verify
            the address before approving authorization.
        depositAddressId:
          type: string
          nullable: true
          description: Unique identifier for the reusable deposit address linked to this exchange
          example: addr-xyz789
        cryptoDeposit:
          $ref: '#/components/schemas/CryptoDeposit'
          nullable: true
          description: |
            Cryptocurrency deposit transaction (off-ramp only, null for on-ramp).
            Represents the blockchain transaction sent to the depositAddress.
          example:
            transactionHash: abc123def456...
            amount:
              amount: '0.1'
              currency: BTC
            network: bitcoin
            confirmations: 6
            requiredConfirmations: 3
            status: CONFIRMED
            receivedAt: '2023-11-04T10:30:56.789Z'
        fiatDeposit:
          $ref: '#/components/schemas/FiatDeposit'
          nullable: true
          description: |
            Fiat deposit/funding transaction (on-ramp only, null for off-ramp).
            Represents partner-confirmed fiat funding for the exchange.
          example:
            depositId: 550e8400-e29b-41d4-a716-446655440002
            amount:
              amount: '1000.00'
              currency: USD
            status: CONFIRMED
            externalReference: partner-fiat-ref-123
            confirmedAt: '2023-11-04T10:30:56.789Z'
        flow:
          $ref: '#/components/schemas/ExchangeFlow'
          description: Exchange flow configuration
        authorizationContext:
          $ref: '#/components/schemas/AuthorizationContext'
          nullable: true
          description: |
            Authorization context for on-ramp crypto withdrawal.
            Present only when status == AWAITING_AUTHORIZATION and flow direction is on-ramp.
            Null for all other statuses and for off-ramp flows.
        cryptoRefunds:
          type: array
          nullable: true
          description: |
            Array of cryptocurrency refund transactions for this exchange. Refunds are independent of exchange status
            and can occur multiple times, including after exchange completion.
          items:
            $ref: '#/components/schemas/CryptoRefund'
          example:
            - refundId: 550e8400-e29b-41d4-a716-446655440001
              transactionHash: abc123def456...
              amount:
                amount: '0.1'
                currency: BTC
              network: bitcoin
              refundAddress:
                address: 3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy
                network: bitcoin
              status: COMPLETED
              initiatedAt: '2023-11-04T10:30:56.789Z'
              completedAt: '2023-11-04T10:35:56.789Z'
              reason: Deposit timeout - exchange expired before deposit received
        cryptoWithdrawals:
          type: array
          nullable: true
          description: |
            Array of cryptocurrency withdrawal transactions for this exchange (on-ramp only).
            Multiple withdrawals can exist due to retries after failure.
          items:
            $ref: '#/components/schemas/CryptoWithdrawal'
        fiatWithdrawals:
          type: array
          nullable: true
          description: |
            Array of fiat withdrawal/payout transactions for this exchange (off-ramp only).
            Multiple withdrawals can exist due to retries after failure.
          items:
            $ref: '#/components/schemas/FiatWithdrawal'
        timestamps:
          $ref: '#/components/schemas/ExchangeTimestamps'
          description: Transaction lifecycle timestamps
    CancelExchangeRequest:
      type: object
      description: Request body for cancelling an exchange transaction
      properties:
        reason:
          type: string
          maxLength: 500
          description: Optional cancellation reason for audit trail
          example: User requested cancellation
      required: []
    CancelExchangeResponse:
      type: object
      description: |
        Response for cancel request. Note: Cancel is asynchronous - this response confirms
        the cancel request was accepted, not that cancellation is complete.
        Check webhooks or poll GET /exchanges/{id} for final CANCELLED status.
      required:
        - exchangeId
        - status
        - message
      properties:
        exchangeId:
          type: string
          format: uuid
          description: Exchange identifier
          example: 550e8400-e29b-41d4-a716-446655440000
        status:
          type: string
          description: |
            Current status after cancel request accepted.
            Will be CANCELLATION_REQUESTED until cancellation completes.
            Final status will be CANCELLED (check via webhook or polling).
          example: CANCELLATION_REQUESTED
        message:
          type: string
          description: Human-readable message about the cancel request
          example: Cancel request accepted. Check webhooks or poll for final status.
    RequestRefundRequest:
      type: object
      description: Request body for requesting a refund for an exchange transaction
      required:
        - amount
      properties:
        amount:
          $ref: '#/components/schemas/CurrencyAmount'
          description: Refund amount and currency (must match exchange fromCurrency)
        refundAddress:
          $ref: '#/components/schemas/CryptoAddress'
          nullable: true
          description: |
            Cryptocurrency refund address with network (format validated on request).
            Optional - if not provided, will use refund address from exchange creation if available.
            If neither is available, refund request will fail.
        reason:
          type: string
          maxLength: 500
          description: Optional refund reason for audit trail
          example: Partner requested refund for excess deposit
    RequestRefundResponse:
      type: object
      description: Response for refund request
      required:
        - refundId
        - exchangeId
        - status
        - amount
        - refundAddress
        - initiatedAt
      properties:
        refundId:
          type: string
          format: uuid
          description: Unique refund identifier (Payment ID)
          example: 550e8400-e29b-41d4-a716-446655440001
        exchangeId:
          type: string
          format: uuid
          description: Exchange identifier
          example: 550e8400-e29b-41d4-a716-446655440000
        status:
          $ref: '#/components/schemas/RefundStatus'
          description: Refund status (INITIATED after request)
          example: INITIATED
        amount:
          $ref: '#/components/schemas/CurrencyAmount'
          description: Refund amount and currency
        refundAddress:
          $ref: '#/components/schemas/CryptoAddress'
          description: Refund address with network
        reason:
          type: string
          nullable: true
          description: Refund reason (if provided)
          example: Partner requested refund for excess deposit
        initiatedAt:
          type: string
          format: date-time
          description: Refund initiation timestamp (ISO 8601 format)
          example: '2023-11-04T10:31:40.000Z'
    ApproveExchangeAuthorizationRequest:
      type: object
      description: |
        Authorization approval payload submitted after the partner receives the
        `exchange.authorization_required` webhook. On-ramp exchanges require all
        three fields (`approvalToken`, `commitmentHash`, `validUntil`); off-ramp
        exchanges ignore them — the approval signal alone is sufficient.

        **Trust model.** The partner (or whichever component holds the partner's
        signing key) issues `approvalToken`, computes `commitmentHash`, and chooses
        `validUntil`. Spike treats `approvalToken` as opaque, archives it as
        non-repudiation evidence, and verifies that `commitmentHash` was produced
        from the canonical inputs below — that binding is what protects against
        parameter tampering between the webhook and this call.

        **Canonical hash input** (UTF-8, single-line, `|` separator, no escaping):

        ```
        amount + "|" + currency + "|" + address + "|" + bitfinex_trx_id + "|" + approval_token + "|" + valid_until
        ```

        - `amount`, `currency`, `address`, `bitfinex_trx_id` come from the
          `exchange.authorization_required` webhook payload — the exact values
          Spike will execute against.
        - `approval_token` is the same string sent in `approvalToken`.
        - `valid_until` is the same ISO-8601 instant sent in `validUntil`. The
          string serialisation (including precision) must match byte-for-byte.

        `commitmentHash` is the lowercase hex-encoded SHA-256 of that string.

        **Worked example:**

        ```text
        amount          = "0.0065"
        currency        = "BTC"
        address         = "bc1qc42xdngqvtqxqsfgx3jpuczgruwj762drtaju4"
        bitfinex_trx_id = "btfx-5d9c20a59c3a4061"
        approval_token  = "PARTNER_SIGNED_TOKEN_BASE64URL"
        valid_until     = "2026-04-28T12:46:06.401973Z"

        canonical = "0.0065|BTC|bc1qc42xdngqvtqxqsfgx3jpuczgruwj762drtaju4|btfx-5d9c20a59c3a4061|PARTNER_SIGNED_TOKEN_BASE64URL|2026-04-28T12:46:06.401973Z"
        commitmentHash = SHA256_hex(canonical)
        ```

        Returns 400 `missing_approval_material` if an on-ramp request omits any
        of the three fields, and 400 `commitment_mismatch` if the hash does not
        bind the presented `approvalToken`.
      properties:
        approvalToken:
          type: string
          description: |
            Signed artifact issued by the partner (or the authorising party
            operating on the partner's behalf). Treated as opaque by Spike;
            archived as non-repudiation proof. Required for on-ramp.
        commitmentHash:
          type: string
          pattern: ^[a-fA-F0-9]{64}$
          description: |
            Lowercase hex-encoded SHA-256 of the canonical input string described
            above. Required for on-ramp.
        validUntil:
          type: string
          format: date-time
          description: |
            Approval expiration (ISO-8601). Provides time-binding and replay
            protection. The string sent here must match byte-for-byte the value
            hashed into `commitmentHash`. Required for on-ramp.
    RejectExchangeAuthorizationRequest:
      type: object
      description: Request body for rejecting exchange authorization
      properties:
        reason:
          type: string
          maxLength: 500
          description: Optional reason for rejecting the authorization
          example: AML screening failed
      required: []
    QuoteAmounts:
      title: QuoteAmounts
      type: object
      description: Quote amount information including source and target amounts (without rate, as rate is provided at top level)
      properties:
        from:
          $ref: '#/components/schemas/CurrencyAmount'
          nullable: true
          description: Source amount
        to:
          $ref: '#/components/schemas/CurrencyAmount'
          nullable: true
          description: Target amount
    BaseQuoteResponse:
      type: object
      required:
        - rate
      description: Base quote response with shared fields for both indicative and fixed-rate quotes
      properties:
        rate:
          type: string
          description: |
            Exchange rate in canonical pair direction: units of the fiat currency per 1 unit
            of the crypto currency.

            The rate value is identical for on-ramp and off-ramp on the same currency pair.
            Direction is conveyed by `amounts.from.currency` and `amounts.to.currency`, never
            by inverting the rate.

            To compute amounts from the rate:
              - on-ramp  (fiat -> crypto): cryptoAmount = fiatAmount / rate
              - off-ramp (crypto -> fiat): fiatAmount   = cryptoAmount * rate

            Or read `amounts.from.amount` and `amounts.to.amount` directly — they are
            always populated for fixed quotes and for indicative quotes when an amount
            parameter was provided.

            Example: For BTC<->USD trades at a 76,452.60 USD/BTC market price:
              - on-ramp  (USD -> BTC): rate = "76452.60", from 500 USD -> to 0.00654 BTC
              - off-ramp (BTC -> USD): rate = "76452.60", from 0.00654 BTC -> to 500 USD
          example: '76452.60'
        amounts:
          $ref: '#/components/schemas/QuoteAmounts'
          description: |
            Calculated amounts.
            For indicative quotes: only present if fromAmount or toAmount parameter was provided.
            For fixed-rate quotes: always present (see FixedQuoteResponse for required fields).
    IndicativeQuoteResponse:
      allOf:
        - $ref: '#/components/schemas/BaseQuoteResponse'
        - type: object
          description: Indicative (non-binding) quote response. Rate is for display purposes only.
    FixedQuoteResponse:
      allOf:
        - $ref: '#/components/schemas/BaseQuoteResponse'
        - type: object
          required:
            - quoteId
            - amounts
            - validity
          description: Fixed-rate (binding) quote response. Rate is guaranteed for execution within validity window.
          properties:
            quoteId:
              type: string
              description: |
                Unique identifier for this quote. 
                Must be included in CreateExchangeRequest.quoteId when creating the exchange.
              maxLength: 255
              pattern: ^[a-zA-Z0-9_-]+$
              example: quote-abc123
            amounts:
              $ref: '#/components/schemas/QuoteAmounts'
              description: |
                Calculated amounts (always present for fixed-rate quotes).
                The `from` and `to` fields are always present for fixed-rate quotes.
            validity:
              type: object
              required:
                - validUntil
              description: Quote validity and timing information
              properties:
                validUntil:
                  type: string
                  format: date-time
                  description: Quote expiration timestamp (ISO 8601 format)
                  example: '2023-11-04T10:30:56.789Z'
    ValidateAddressRequest:
      title: ValidateAddressRequest
      type: object
      description: Request to validate a destination crypto address for a given currency and network
      required:
        - currency
        - network
        - address
      properties:
        currency:
          type: string
          description: Currency code (e.g., "BTC", "ETH", "USDT")
          minLength: 3
          maxLength: 10
          example: XRP
        network:
          type: string
          description: Network identifier (e.g., "BITCOIN", "ETHEREUM", "RIPPLE")
          minLength: 2
          maxLength: 32
          example: RIPPLE
        address:
          type: string
          description: Destination address to validate. Whitespace around the address is ignored.
          minLength: 1
          maxLength: 256
          example: rEb8TK3gBgk5auZkwc6sHnwrGVJH8DuaLh
        tag:
          type: string
          description: |
            Optional destination tag/memo. Required for networks like RIPPLE. When
            the network requires a tag and this field is omitted, the response is
            `valid: false` with `reason: MISSING_TAG`.
          minLength: 1
          maxLength: 64
          example: '12345'
    AddressValidationReason:
      type: string
      enum:
        - INVALID_FORMAT
        - INVALID_LENGTH
        - INVALID_PREFIX
        - INVALID_ENCODING
        - INVALID_CHECKSUM
        - NETWORK_MISMATCH
        - MISSING_TAG
        - INVALID_TAG
        - TESTNET_ADDRESS
      description: |
        Machine-readable reason for an invalid address. Open enum — new values
        may be added without a breaking version bump. Clients must treat unknown
        values as a generic "invalid" signal.
        - INVALID_FORMAT: Non-ASCII characters, wrong general shape, or decoding
          failed at the format layer. Usually a copy-paste mistake or a
          non-address string.
        - INVALID_LENGTH: Address length is outside the range expected by the
          network. Typically a truncated or padded paste.
        - INVALID_PREFIX: Address does not start with a recognised prefix for the
          network (e.g., `bc1`, `0x`, `r`). Commonly indicates the user picked
          the wrong network.
        - INVALID_ENCODING: Address characters are valid in shape but the
          underlying encoding (Base58, Bech32, Hex) could not be decoded.
        - INVALID_CHECKSUM: Embedded checksum bytes do not match the computed
          checksum. Usually a single-character typo.
        - NETWORK_MISMATCH: Address is well-formed but belongs to a different
          network than the one declared (e.g., an Ethereum address declared as
          Avalanche).
        - MISSING_TAG: The selected network requires a destination tag/memo
          (e.g., RIPPLE) and none was supplied in the request.
        - INVALID_TAG: A destination tag/memo was supplied but is not valid for
          the network (wrong range, wrong format).
        - TESTNET_ADDRESS: The address is a valid testnet address, but Spike only
          accepts mainnet addresses for partner flows.
    ValidateAddressResponse:
      title: ValidateAddressResponse
      type: object
      description: Result of validating a destination crypto address
      required:
        - valid
      properties:
        valid:
          type: boolean
          description: Whether the address is valid for the declared currency and network
          example: true
        reason:
          $ref: '#/components/schemas/AddressValidationReason'
          nullable: true
        normalizedAddress:
          type: string
          nullable: true
          description: |
            Canonical form of the address, present only when `valid` is true.
            Clients should prefer this form for storage and display to avoid
            casing-drift bugs — most notably for EVM addresses, where
            normalization applies EIP-55 mixed-case checksumming. For networks
            whose addresses have a single canonical form (e.g., Bitcoin Bech32,
            Ripple r-addresses), this field returns the input unchanged after
            trimming.
          example: rEb8TK3gBgk5auZkwc6sHnwrGVJH8DuaLh
        tagRequired:
          type: boolean
          nullable: true
          description: |
            Whether this network requires a destination tag/memo. Present only when
            `valid` is true. Clients should use this to drive UI prompts.
          example: true
    CurrencyType:
      title: CurrencyType
      type: string
      enum:
        - CRYPTO
        - FIAT
        - ALL
      description: Currency type filter. ALL returns both CRYPTO and FIAT currencies.
    CurrencyNetworkInfo:
      title: CurrencyNetworkInfo
      type: object
      required:
        - network
        - name
      description: Network information for a currency
      properties:
        network:
          type: string
          description: Network identifier (e.g., "ethereum", "bitcoin")
          example: ethereum
        name:
          type: string
          description: Network display name
          example: Ethereum (ERC20)
        depositEnabled:
          type: boolean
          description: Whether deposits are enabled for this network
        withdrawalEnabled:
          type: boolean
          description: Whether withdrawals are enabled for this network
        minDeposit:
          type: string
          description: Minimum deposit amount
          example: '10.0'
        minWithdrawal:
          type: string
          description: Minimum withdrawal amount
          example: '20.0'
        withdrawalFee:
          type: string
          description: Withdrawal fee
          example: '5.0'
        confirmations:
          type: integer
          description: Number of confirmations required
          example: 12
    CurrencyResponse:
      title: CurrencyResponse
      type: object
      required:
        - code
        - name
        - type
        - decimals
        - enabled
      description: Currency information
      properties:
        code:
          type: string
          description: Currency code (ISO 4217 for fiat, standard code for crypto)
          example: BTC
        name:
          type: string
          description: Currency display name
          example: Bitcoin
        type:
          type: string
          enum:
            - CRYPTO
            - FIAT
          description: Currency type
        networks:
          type: array
          description: Available networks (for crypto currencies)
          items:
            $ref: '#/components/schemas/CurrencyNetworkInfo'
        decimals:
          type: integer
          description: Decimal precision
          example: 8
        enabled:
          type: boolean
          description: Whether this currency is enabled for the authenticated partner
          example: true
        dustThreshold:
          type: string
          pattern: ^\d+(\.\d+)?$
          nullable: true
          description: |
            Minimum crypto deposit amount for off-ramp processing, as configured by the partner.
            Deposits below this threshold are immediately failed with DEPOSIT_BELOW_DUST_THRESHOLD.
            Only present for enabled crypto currencies with a configured threshold. Null or absent means no dust filtering.
          example: '1.00'
    CurrencyListResponse:
      title: CurrencyListResponse
      type: object
      required:
        - currencies
      description: Response containing list of currencies
      properties:
        currencies:
          type: array
          items:
            $ref: '#/components/schemas/CurrencyResponse'
    CurrencyPairResponse:
      title: CurrencyPairResponse
      type: object
      required:
        - fromCurrency
        - toCurrency
        - enabled
      description: Currency pair information
      properties:
        fromCurrency:
          type: string
          description: Source currency code
          example: BTC
        toCurrency:
          type: string
          description: Target currency code
          example: USD
        minAmount:
          type: string
          description: Minimum exchange amount
          example: '0.001'
          nullable: true
        maxAmount:
          type: string
          description: Maximum exchange amount
          example: '10.0'
          nullable: true
        enabled:
          type: boolean
          description: Whether this pair is enabled for the authenticated partner
          example: true
        directions:
          type: array
          description: |
            Allowed exchange directions for this pair for the authenticated partner.

            For directional discovery, prefer `/v1/conversion-routes`, which emits
            one row per direction using the same `fromCurrency` / `toCurrency`
            semantics as the rest of the public API.
          nullable: true
          items:
            type: string
            enum:
              - ON_RAMP
              - OFF_RAMP
          example:
            - ON_RAMP
            - OFF_RAMP
    CurrencyPairListResponse:
      title: CurrencyPairListResponse
      type: object
      required:
        - pairs
      description: Response containing list of currency pairs
      properties:
        pairs:
          type: array
          items:
            $ref: '#/components/schemas/CurrencyPairResponse'
    ConversionRouteType:
      title: ConversionRouteType
      type: string
      enum:
        - ON_RAMP
        - OFF_RAMP
      description: |
        Direction of a conversion route.
        - ON_RAMP: fiat -> crypto (e.g., USD -> BTC).
        - OFF_RAMP: crypto -> fiat (e.g., BTC -> USD).
    ConversionRouteResponse:
      title: ConversionRouteResponse
      type: object
      required:
        - type
        - fromCurrency
        - toCurrency
        - enabled
      description: |
        A directional conversion route the authenticated partner can execute through
        the Spike Exchange API. Each row represents one direction (ON_RAMP or
        OFF_RAMP) of a single fiat<->crypto pair, expressed using the same
        fromCurrency / toCurrency semantics as /v1/quotes and POST /v1/exchanges.
      properties:
        type:
          $ref: '#/components/schemas/ConversionRouteType'
        fromCurrency:
          type: string
          description: |
            Source currency code. For ON_RAMP this is the fiat code; for OFF_RAMP
            this is the crypto code.
          example: USD
        toCurrency:
          type: string
          description: |
            Target currency code. For ON_RAMP this is the crypto code; for OFF_RAMP
            this is the fiat code.
          example: BTC
        fromNetworks:
          type: array
          nullable: true
          description: |
            Networks supported on the source side. Populated only for OFF_RAMP
            (the customer deposits crypto on one of these networks). Null or absent
            for ON_RAMP.
          items:
            type: string
          example:
            - bitcoin
        toNetworks:
          type: array
          nullable: true
          description: |
            Networks supported on the destination side. Populated only for ON_RAMP
            (Spike withdraws crypto to one of these networks). Null or absent for
            OFF_RAMP.
          items:
            type: string
          example:
            - bitcoin
        minAmount:
          type: string
          description: |
            Minimum amount the partner can quote/exchange on this route, expressed
            in fromCurrency units. Null when no per-route limit is configured.
          example: '10.00'
          nullable: true
        maxAmount:
          type: string
          description: |
            Maximum amount the partner can quote/exchange on this route, expressed
            in fromCurrency units. Null when no per-route limit is configured.
          example: '10000.00'
          nullable: true
        enabled:
          type: boolean
          description: |
            Whether this route is enabled for the authenticated partner. When
            enabledOnly=true is set on the request, only enabled routes are
            returned and this field is always true.
          example: true
    ConversionRouteListResponse:
      title: ConversionRouteListResponse
      type: object
      required:
        - routes
      description: Response containing list of conversion routes.
      properties:
        routes:
          type: array
          items:
            $ref: '#/components/schemas/ConversionRouteResponse'
    DepositAddressStatus:
      type: string
      enum:
        - ACTIVE
        - DISABLED
      description: |
        Reusable deposit address status.

        **Status Values:**
        - ACTIVE: Address is active and can receive deposits that trigger exchanges
        - DISABLED: Address is disabled, new deposits will not create exchanges
    DepositAddressResponse:
      title: DepositAddressResponse
      type: object
      description: |
        Reusable deposit address for receiving crypto deposits.
        The same address accepts multiple deposits over time. Each deposit creates an independent exchange with its own exchangeId.
      required:
        - depositAddressId
        - depositAddress
        - currencyPair
        - externalUserId
        - status
        - createdAt
      properties:
        depositAddressId:
          type: string
          description: Unique identifier for the reusable deposit address
          example: addr-xyz789
        depositAddress:
          $ref: '#/components/schemas/CryptoAddress'
          description: Crypto deposit address (reusable). Each deposit to this address creates a separate exchange.
        currencyPair:
          $ref: '#/components/schemas/CurrencyPair'
          description: Source and target currencies
        externalUserId:
          type: string
          description: Partner's user identifier
          example: user-123
        status:
          $ref: '#/components/schemas/DepositAddressStatus'
          description: Address status
          example: ACTIVE
        createdAt:
          type: string
          format: date-time
          description: Address creation timestamp (ISO 8601 format)
          example: '2023-11-04T10:30:56.789Z'
        refundAddress:
          $ref: '#/components/schemas/CryptoAddress'
          nullable: true
          description: Crypto address for refunds
    DepositAddressListResponse:
      title: DepositAddressListResponse
      type: object
      description: List of reusable deposit addresses with pagination
      required:
        - depositAddresses
        - pagination
      properties:
        depositAddresses:
          type: array
          description: List of reusable deposit addresses
          items:
            $ref: '#/components/schemas/DepositAddressResponse'
        pagination:
          $ref: '#/components/schemas/PaginationInfo'
          description: Pagination metadata
    CreateDepositAddressRequest:
      title: CreateDepositAddressRequest
      type: object
      description: Request to create a reusable deposit address
      required:
        - fromCurrency
        - toCurrency
        - externalUserId
        - network
      properties:
        fromCurrency:
          type: string
          description: Source cryptocurrency code
          minLength: 3
          maxLength: 10
          example: BTC
        toCurrency:
          type: string
          description: Target currency code (fiat or crypto)
          minLength: 3
          maxLength: 10
          example: USD
        externalUserId:
          type: string
          description: Partner's user identifier
          maxLength: 255
          example: user-123
        network:
          type: string
          description: Blockchain network for the deposit address (e.g., "bitcoin", "ethereum", "tron")
          maxLength: 50
          example: bitcoin
        refundAddress:
          $ref: '#/components/schemas/CryptoAddress'
          nullable: true
          description: |
            Crypto address for refunds.
            Optional - can be provided at deposit address creation or later when requesting refund.
            If not provided at creation, must be provided in refund request.
    UpdateDepositAddressRequest:
      title: UpdateDepositAddressRequest
      type: object
      description: Request body for updating a reusable deposit address
      properties:
        refundAddress:
          $ref: '#/components/schemas/CryptoAddress'
          nullable: true
          description: |
            Crypto refund address for future exchanges using this deposit address.
            Set to null to remove existing refund address.
            Network must match deposit address network.
    PublicKeyResponse:
      type: object
      description: RSA public key for webhook signature verification
      required:
        - keyId
        - publicKey
        - algorithm
        - createdAt
      properties:
        keyId:
          type: string
          format: uuid
          description: Unique identifier for this key pair. Included in X-Webhook-Key-Id header on webhook deliveries.
          example: 550e8400-e29b-41d4-a716-446655440000
        publicKey:
          type: string
          description: |
            Base64-encoded X.509 RSA public key (4096-bit).
            Use this key to verify webhook signatures using RSA-SHA256.
          example: MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA...
        algorithm:
          type: string
          description: Signature algorithm used for signing webhooks
          enum:
            - RSA-SHA256
          example: RSA-SHA256
        createdAt:
          type: string
          format: date-time
          description: Timestamp when this key was generated
          example: '2024-01-15T10:30:00Z'
    WebhookResponse:
      type: object
      required:
        - webhookId
        - url
        - events
        - status
        - createdAt
      properties:
        webhookId:
          type: string
          format: uuid
          description: Webhook identifier
          example: 550e8400-e29b-41d4-a716-446655440000
        url:
          type: string
          format: uri
          description: Webhook URL
          example: https://partner.com/webhooks/exchange
        events:
          type: array
          description: Array of subscribed event types
          items:
            type: string
          example:
            - exchange.completed
            - exchange.failed
        status:
          type: string
          enum:
            - ACTIVE
            - INACTIVE
            - SUSPENDED
          description: Webhook status
          example: ACTIVE
        createdAt:
          type: string
          format: date-time
          description: Creation timestamp (ISO 8601 format)
          example: '2023-11-04T10:30:56.789Z'
        lastDeliveryAt:
          type: string
          format: date-time
          nullable: true
          description: Last successful delivery timestamp (ISO 8601 format)
          example: '2023-11-04T10:35:56.789Z'
        failureCount:
          type: integer
          description: Number of consecutive delivery failures
          example: 0
        description:
          type: string
          nullable: true
          description: Webhook description
          example: Production webhook endpoint
    WebhookListResponse:
      type: object
      required:
        - webhooks
        - pagination
      properties:
        webhooks:
          type: array
          description: List of webhooks
          items:
            $ref: '#/components/schemas/WebhookResponse'
        pagination:
          $ref: '#/components/schemas/PaginationInfo'
          description: Pagination metadata
    WebhookEventType:
      type: string
      enum:
        - deposit.initiated
        - deposit.progressed
        - deposit.received
        - withdraw.initiated
        - withdraw.completed
        - withdraw.failed
        - onramp.completed
        - onramp.failed
        - offramp.completed
        - offramp.failed
        - exchange.completed
        - exchange.failed
        - exchange.cancelled
        - exchange.authorization_required
        - refund.required
        - refund.initiated
        - refund.completed
        - refund.failed
      description: |
        Webhook event type. Indicates which lifecycle event triggered the notification.

        **Deposit Events:**
        - deposit.initiated: Funding deposit detected but not yet confirmed
        - deposit.progressed: Deposit confirmation progress update
        - deposit.received: Funding deposit confirmed

        **Withdraw Events:**
        - withdraw.initiated: Crypto withdrawal submitted
        - withdraw.completed: Withdrawal confirmed on chain
        - withdraw.failed: Withdrawal failed

        **On-Ramp Events (aggregate terminal — fired once per on-ramp):**
        - onramp.completed: On-ramp completed successfully (terminal)
        - onramp.failed: On-ramp failed (terminal; carries failureContext)

        **Off-Ramp Events (aggregate terminal — fired once per off-ramp):**
        - offramp.completed: Off-ramp completed successfully (terminal)
        - offramp.failed: Off-ramp failed (terminal; carries failureContext)

        **Exchange Events (trading-leg — provider order lifecycle, NOT the ramp aggregate):**
        - exchange.completed: Trading leg completed (Bitfinex order filled)
        - exchange.failed: Trading leg failed (Bitfinex order rejected)
        - exchange.cancelled: Trading leg cancelled (async cancellation request)
        - exchange.authorization_required: Withdrawal authorization required from partner

        **Refund Events:**
        - refund.required: Refund needed (e.g., exchange failed after deposit)
        - refund.initiated: Refund transaction submitted
        - refund.completed: Refund confirmed
        - refund.failed: Refund failed
    RegisterWebhookRequest:
      type: object
      required:
        - url
        - events
      properties:
        url:
          type: string
          format: uri
          description: Webhook URL (HTTPS by default; HTTP can be enabled by environment configuration)
          example: https://partner.com/webhooks/exchange
        events:
          type: array
          description: Array of event types to subscribe to
          minItems: 1
          items:
            $ref: '#/components/schemas/WebhookEventType'
          example:
            - deposit.initiated
            - deposit.progressed
            - deposit.received
            - withdraw.initiated
            - withdraw.completed
            - onramp.completed
            - onramp.failed
            - offramp.failed
            - exchange.completed
            - exchange.failed
            - exchange.cancelled
        description:
          type: string
          description: Optional webhook description
          maxLength: 500
          example: Production webhook endpoint
    FailedWebhookEventResponse:
      type: object
      required:
        - eventId
        - eventType
        - payload
        - failedCount
        - failedAt
        - createdAt
      properties:
        eventId:
          type: string
          description: Unique event identifier
          example: evt-550e8400-e29b-41d4-a716-446655440000
        eventType:
          type: string
          description: Type of webhook event
          example: exchange.completed
        payload:
          type: string
          description: Original webhook payload JSON
        failedCount:
          type: integer
          description: Number of delivery attempts that failed
          example: 5
        failedAt:
          type: string
          format: date-time
          description: Timestamp of last failed delivery attempt (ISO 8601 format)
          example: '2023-11-04T10:35:56.789Z'
        createdAt:
          type: string
          format: date-time
          description: Timestamp when event was originally created (ISO 8601 format)
          example: '2023-11-04T10:30:56.789Z'
    FailedWebhookEventListResponse:
      type: object
      required:
        - events
        - pagination
      properties:
        events:
          type: array
          description: List of failed webhook events
          items:
            $ref: '#/components/schemas/FailedWebhookEventResponse'
        pagination:
          $ref: '#/components/schemas/PaginationInfo'
          description: Pagination metadata
