openapi: 3.0.3
info:
  title: NxtGateW Merchant API
  version: 1.0.0
  description: |
    Crypto deposit wallets — create receive addresses, verify split configuration,
    and receive movement / balance webhooks.

    NFT / SBT module (NxtGateW) lives under `/api/v1/nft/*` and does **not** change
    deposit wallet creation (`POST /api/v1/wallets/create` still returns `private_key`
    for client wallets). See also `docs/NFT_API.md`.

    ## Authentication

    All endpoints require both headers:

    - `X-API-Key` — account API key (`merchant`, `psp`, or `admin`)
    - `X-API-Secret` — matching API secret
    - NFT write ops also require `Idempotency-Key`

    ## Webhooks (outbound)

    The `callback` URL registered at wallet creation receives `POST` JSON:

    - `wallet.balance_changed` — after each new indexed movement
    - `wallet.movements_snapshot` — once per reconciled / split-complete sync cycle
    - NFT events: `nft.mint_pending`, `nft.minted`, `nft.updated`, …

    If `ipn_token` was set (or generated), verify `X-IPN-Token` on inbound notifications.

servers:
  - url: https://nxtgatew.com
    description: Production

tags:
  - name: Wallets
    description: Create and inspect deposit wallets
  - name: NFT
    description: Membership / license NFT module (on-chain)
  - name: Webhooks
    description: Outbound callback payload shapes (not requestable endpoints)

paths:
  /api/v1/nft:
    get:
      tags: [NFT]
      summary: NFT module info
      security:
        - ApiKeyAuth: []
          ApiSecretAuth: []
      responses:
        '200':
          description: Module status (version, on_chain)
  /api/v1/nft/wallets:
    post:
      tags: [NFT]
      summary: Create or link NFT managed wallet
      description: |
        Get-or-create by `external_user_id` + network. Reuses existing client deposit
        wallet when present. May return `private_key` (same policy as client wallets).
      security:
        - ApiKeyAuth: []
          ApiSecretAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/NftWalletCreateRequest'
      responses:
        '201':
          description: Created
        '200':
          description: Reused
  /api/v1/nft/wallets/{wallet_id}:
    get:
      tags: [NFT]
      summary: Get NFT wallet
      security:
        - ApiKeyAuth: []
          ApiSecretAuth: []
      parameters:
        - $ref: '#/components/parameters/NftWalletId'
      responses:
        '200':
          description: OK
        '404':
          description: Not found
  /api/v1/nft/wallets/by-external-user/{external_user_id}:
    get:
      tags: [NFT]
      summary: Resolve wallet by external_user_id
      security:
        - ApiKeyAuth: []
          ApiSecretAuth: []
      parameters:
        - name: external_user_id
          in: path
          required: true
          schema:
            type: string
        - name: network
          in: query
          schema:
            type: string
            default: polygon
      responses:
        '200':
          description: OK
        '404':
          description: Not found
  /api/v1/nft/wallets/{wallet_id}/assets:
    get:
      tags: [NFT]
      summary: List NFTs for wallet
      security:
        - ApiKeyAuth: []
          ApiSecretAuth: []
      parameters:
        - $ref: '#/components/parameters/NftWalletId'
        - name: limit
          in: query
          schema:
            type: integer
            default: 25
      responses:
        '200':
          description: OK
  /api/v1/nft/collections:
    post:
      tags: [NFT]
      summary: Create NFT collection
      security:
        - ApiKeyAuth: []
          ApiSecretAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/NftCollectionCreateRequest'
      responses:
        '201':
          description: Created
    get:
      tags: [NFT]
      summary: List collections
      security:
        - ApiKeyAuth: []
          ApiSecretAuth: []
      responses:
        '200':
          description: OK
  /api/v1/nft/collections/{collection_id}:
    get:
      tags: [NFT]
      summary: Get collection
      security:
        - ApiKeyAuth: []
          ApiSecretAuth: []
      parameters:
        - $ref: '#/components/parameters/NftCollectionId'
      responses:
        '200':
          description: OK
        '404':
          description: Not found
    patch:
      tags: [NFT]
      summary: Update collection
      description: Update metadata fields or set `contract_address` after on-chain deploy.
      security:
        - ApiKeyAuth: []
          ApiSecretAuth: []
      parameters:
        - $ref: '#/components/parameters/NftCollectionId'
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                symbol:
                  type: string
                base_metadata_uri:
                  type: string
                contract_address:
                  type: string
                  description: 0x… EVM address
      responses:
        '200':
          description: Updated
  /api/v1/nft/collections/{collection_id}/deploy:
    post:
      tags: [NFT]
      summary: Deploy collection
      description: Marks pending_deploy until a real contract_address is set via PATCH after on-chain deploy.
      security:
        - ApiKeyAuth: []
          ApiSecretAuth: []
      parameters:
        - $ref: '#/components/parameters/NftCollectionId'
        - $ref: '#/components/parameters/IdempotencyKey'
        - $ref: '#/components/parameters/XTimestamp'
        - $ref: '#/components/parameters/XNonce'
        - $ref: '#/components/parameters/XApiSignature'
        - $ref: '#/components/parameters/XSignature'
      responses:
        '202':
          description: Deploy accepted
  /api/v1/nft/collections/{collection_id}/pause:
    post:
      tags: [NFT]
      summary: Pause collection mints
      security:
        - ApiKeyAuth: []
          ApiSecretAuth: []
      parameters:
        - $ref: '#/components/parameters/NftCollectionId'
      responses:
        '200':
          description: Paused
  /api/v1/nft/collections/{collection_id}/unpause:
    post:
      tags: [NFT]
      summary: Unpause collection
      security:
        - ApiKeyAuth: []
          ApiSecretAuth: []
      parameters:
        - $ref: '#/components/parameters/NftCollectionId'
      responses:
        '200':
          description: Unpaused
  /api/v1/nft/nfts/import:
    post:
      tags: [NFT]
      summary: Mode B — Import external on-chain mint
      description: |
        Index a mint already executed on-chain from `tx_hash` (Transfer from 0x0 on the collection contract).
        Does **not** broadcast a second mint. Emits webhook `nft.minted`.
        Use when a third-party checkout or authorized minter already minted.
        Requires Idempotency-Key. Do not also call `POST /nfts` for the same purchase.
      security:
        - ApiKeyAuth: []
          ApiSecretAuth: []
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [tx_hash, collection_id]
              properties:
                tx_hash:
                  type: string
                  example: "0xabc…"
                collection_id:
                  type: string
                network:
                  type: string
                  enum: [amoy, polygon, ethereum]
                asset_type:
                  type: string
                  enum: [membership, digital_license, order_receipt, reward, certificate, collectible]
                wallet_id:
                  type: string
                external_user_id:
                  type: string
                external_reference:
                  type: string
                attributes:
                  type: object
            example:
              tx_hash: "0x82ba842fe8f5edf7dae68e1e14ef0fda51f6e6d0bdf9d6fe00784f07cc34f8e1"
              collection_id: col_xxx
              network: polygon
              asset_type: order_receipt
              external_user_id: usr_84521
              external_reference: order_89452
              attributes:
                name: Order_order_89452
                order_ref: order_89452
                user_id: usr_84521
                amount_pol: "10.5"
                payment_rail: external_checkout
      responses:
        "201":
          description: Imported and nft.minted queued
        "200":
          description: Idempotent replay
        "422":
          description: Tx invalid / no mint Transfer

  /api/v1/nft/nfts:
    post:
      tags: [NFT]
      summary: Mode A — Queue platform mint (async)
      description: |
        Returns HTTP 202. Requires Idempotency-Key. Processed by
        `cron/process-nft-operations-web.php` (operator key). Optional HMAC headers when signing.
        If the NFT was already minted on-chain by a third party, use `POST /nfts/import` instead —
        do not double-mint.
      security:
        - ApiKeyAuth: []
          ApiSecretAuth: []
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
        - $ref: '#/components/parameters/XTimestamp'
        - $ref: '#/components/parameters/XNonce'
        - $ref: '#/components/parameters/XApiSignature'
        - $ref: '#/components/parameters/XSignature'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/NftMintRequest'
            examples:
              membership:
                summary: Membership NFT
                value:
                  asset_type: membership
                  collection_id: col_xxx
                  wallet_id: wal_xxx
                  external_user_id: usr_84521
                  external_reference: order_89452
                  transfer_policy: soulbound
                  attributes:
                    membership_level: bronze
              digital_license:
                summary: Digital license
                value:
                  asset_type: digital_license
                  collection_id: col_xxx
                  wallet_id: wal_xxx
                  external_user_id: usr_84521
                  external_order_id: ord_99871
                  external_product_id: prod_face_swap_pro
                  transfer_policy: non_transferable
                  license:
                    license_type: subscription
                    valid_from: '2026-08-01T00:00:00Z'
                    valid_until: '2027-08-01T00:00:00Z'
              order_receipt:
                summary: Order receipt (Mode A)
                value:
                  asset_type: order_receipt
                  collection_id: col_xxx
                  wallet_id: wal_xxx
                  external_user_id: usr_84521
                  external_reference: order_89452
                  transfer_policy: non_transferable
                  attributes:
                    name: Order_order_89452
                    order_ref: order_89452
                    user_id: usr_84521
                    amount_pol: '10.5'
      responses:
        '202':
          description: Mint queued
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/NftApiSuccess'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          operation:
                            $ref: '#/components/schemas/NftOperation'
                          nft:
                            $ref: '#/components/schemas/NftAsset'
        '409':
          description: NFT_ALREADY_EXISTS or IDEMPOTENCY_CONFLICT
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NftApiError'
        '422':
          description: Collection paused or validation failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NftApiError'
  /api/v1/nft/nfts/{nft_id}:
    get:
      tags: [NFT]
      summary: Get NFT
      security:
        - ApiKeyAuth: []
          ApiSecretAuth: []
      parameters:
        - $ref: '#/components/parameters/NftId'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/NftApiSuccess'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/NftAsset'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NftApiError'
    patch:
      tags: [NFT]
      summary: Update NFT metadata / level
      security:
        - ApiKeyAuth: []
          ApiSecretAuth: []
      parameters:
        - $ref: '#/components/parameters/NftId'
        - $ref: '#/components/parameters/IdempotencyKey'
        - $ref: '#/components/parameters/XTimestamp'
        - $ref: '#/components/parameters/XNonce'
        - $ref: '#/components/parameters/XApiSignature'
        - $ref: '#/components/parameters/XSignature'
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                attributes:
                  type: object
                  additionalProperties: true
                metadata_version:
                  type: integer
      responses:
        '200':
          description: Updated
  /api/v1/nft/nfts/{nft_id}/activate:
    post:
      tags: [NFT]
      summary: Activate NFT
      security:
        - ApiKeyAuth: []
          ApiSecretAuth: []
      parameters:
        - $ref: '#/components/parameters/NftId'
        - $ref: '#/components/parameters/IdempotencyKey'
        - $ref: '#/components/parameters/XTimestamp'
        - $ref: '#/components/parameters/XNonce'
        - $ref: '#/components/parameters/XApiSignature'
        - $ref: '#/components/parameters/XSignature'
      responses:
        '200':
          description: OK
  /api/v1/nft/nfts/{nft_id}/suspend:
    post:
      tags: [NFT]
      summary: Suspend NFT
      security:
        - ApiKeyAuth: []
          ApiSecretAuth: []
      parameters:
        - $ref: '#/components/parameters/NftId'
        - $ref: '#/components/parameters/IdempotencyKey'
        - $ref: '#/components/parameters/XTimestamp'
        - $ref: '#/components/parameters/XNonce'
        - $ref: '#/components/parameters/XApiSignature'
        - $ref: '#/components/parameters/XSignature'
      responses:
        '200':
          description: OK
  /api/v1/nft/nfts/{nft_id}/reactivate:
    post:
      tags: [NFT]
      summary: Reactivate NFT
      security:
        - ApiKeyAuth: []
          ApiSecretAuth: []
      parameters:
        - $ref: '#/components/parameters/NftId'
        - $ref: '#/components/parameters/IdempotencyKey'
        - $ref: '#/components/parameters/XTimestamp'
        - $ref: '#/components/parameters/XNonce'
        - $ref: '#/components/parameters/XApiSignature'
        - $ref: '#/components/parameters/XSignature'
      responses:
        '200':
          description: OK
  /api/v1/nft/nfts/{nft_id}/revoke:
    post:
      tags: [NFT]
      summary: Revoke NFT
      security:
        - ApiKeyAuth: []
          ApiSecretAuth: []
      parameters:
        - $ref: '#/components/parameters/NftId'
        - $ref: '#/components/parameters/IdempotencyKey'
        - $ref: '#/components/parameters/XTimestamp'
        - $ref: '#/components/parameters/XNonce'
        - $ref: '#/components/parameters/XApiSignature'
        - $ref: '#/components/parameters/XSignature'
      responses:
        '200':
          description: OK
  /api/v1/nft/nfts/{nft_id}/burn:
    post:
      tags: [NFT]
      summary: Burn NFT
      security:
        - ApiKeyAuth: []
          ApiSecretAuth: []
      parameters:
        - $ref: '#/components/parameters/NftId'
        - $ref: '#/components/parameters/IdempotencyKey'
        - $ref: '#/components/parameters/XTimestamp'
        - $ref: '#/components/parameters/XNonce'
        - $ref: '#/components/parameters/XApiSignature'
        - $ref: '#/components/parameters/XSignature'
      responses:
        '200':
          description: OK
  /api/v1/nft/nfts/{nft_id}/migrate:
    post:
      tags: [NFT]
      summary: Migrate NFT to another wallet
      security:
        - ApiKeyAuth: []
          ApiSecretAuth: []
      parameters:
        - $ref: '#/components/parameters/NftId'
        - $ref: '#/components/parameters/IdempotencyKey'
        - $ref: '#/components/parameters/XTimestamp'
        - $ref: '#/components/parameters/XNonce'
        - $ref: '#/components/parameters/XApiSignature'
        - $ref: '#/components/parameters/XSignature'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [target_wallet_id]
              properties:
                target_wallet_id:
                  type: string
      responses:
        '200':
          description: Migrated
  /api/v1/nft/nfts/{nft_id}/history:
    get:
      tags: [NFT]
      summary: NFT operation history
      security:
        - ApiKeyAuth: []
          ApiSecretAuth: []
      parameters:
        - $ref: '#/components/parameters/NftId'
      responses:
        '200':
          description: OK
  /api/v1/nft/nfts/by-token/{network}/{contract}/{token_id}:
    get:
      tags: [NFT]
      summary: Resolve NFT by on-chain identifiers
      security:
        - ApiKeyAuth: []
          ApiSecretAuth: []
      parameters:
        - name: network
          in: path
          required: true
          schema:
            type: string
        - name: contract
          in: path
          required: true
          schema:
            type: string
        - name: token_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: OK
        '404':
          description: Not found
  /api/v1/nft/operations/{operation_id}:
    get:
      tags: [NFT]
      summary: Get async operation status
      security:
        - ApiKeyAuth: []
          ApiSecretAuth: []
      parameters:
        - name: operation_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: OK
        '404':
          description: Not found
  /api/v1/nft/metadata/{nft_id}:
    get:
      tags: [NFT]
      summary: Public ERC-721 metadata JSON
      description: |
        No API key. Used as tokenURI when collection `base_metadata_uri` defaults to
        `{APP_URL}/api/v1/nft/metadata`. Path may include optional `.json` suffix.
      parameters:
        - name: nft_id
          in: path
          required: true
          schema:
            type: string
            example: nft_19542ef2f3.json
      responses:
        '200':
          description: Metadata document
          content:
            application/json:
              schema:
                type: object
                properties:
                  name: { type: string }
                  description: { type: string }
                  image: { type: string }
                  attributes:
                    type: array
                    items:
                      type: object
                  nft_id: { type: string }
                  network: { type: string }
                  status: { type: string }
        '404':
          description: Unknown nft_id
  /api/v1/nft/webhooks:
    post:
      tags: [NFT]
      summary: Register NFT webhook endpoint
      security:
        - ApiKeyAuth: []
          ApiSecretAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url:
                  type: string
                  format: uri
                secret:
                  type: string
                  description: Optional; generated if omitted (returned once)
                subscribed_events:
                  type: string
                  default: '*'
                  description: Comma-separated event names or *
      responses:
        '201':
          description: Created (includes secret once)
    get:
      tags: [NFT]
      summary: List webhook endpoints
      description: Secrets are not returned.
      security:
        - ApiKeyAuth: []
          ApiSecretAuth: []
      responses:
        '200':
          description: OK
  /api/v1/nft/webhooks/{webhook_id}:
    patch:
      tags: [NFT]
      summary: Update webhook endpoint
      description: |
        Update `url`, `subscribed_events`, `status` (`active`|`disabled`).
        Set `rotate_secret: true` to mint a new secret (returned once) and bump `secret_version`.
      security:
        - ApiKeyAuth: []
          ApiSecretAuth: []
      parameters:
        - name: webhook_id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                url:
                  type: string
                subscribed_events:
                  type: string
                status:
                  type: string
                  enum: [active, disabled]
                rotate_secret:
                  type: boolean
      responses:
        '200':
          description: Updated
        '404':
          description: Not found
    delete:
      tags: [NFT]
      summary: Disable webhook endpoint
      description: Soft-delete — sets `status=disabled`. Re-enable with PATCH.
      security:
        - ApiKeyAuth: []
          ApiSecretAuth: []
      parameters:
        - name: webhook_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Disabled
        '404':
          description: Not found
  /api/v1/nft/webhooks/test:
    post:
      tags: [NFT]
      summary: Queue nft.test webhook
      security:
        - ApiKeyAuth: []
          ApiSecretAuth: []
      responses:
        '202':
          description: Queued
  /api/v1/wallets/create:
    post:
      tags: [Wallets]
      summary: Create or reuse a deposit wallet
      description: |
        Creates a platform-controlled receive address tied to your account.

        - `mode=ephemeral` (default): new address every call → **201**. Private key is never returned.
        - `mode=client`: get-or-create by `(account, network, client_id)` → **201** new or **200** reused.
          Response includes `private_key` (plaintext) for export / migration to another provider.      security:
        - ApiKeyAuth: []
          ApiSecretAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WalletCreateRequest'
            examples:
              ephemeral:
                summary: Ephemeral wallet
                value:
                  address: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e'
                  callback: https://your-site.com/webhooks/nxtgatew
                  network: polygon
                  mode: ephemeral
                  affiliate_address: '0x4B31D368067127D10C59e655b8B22937E35D1196'
                  affiliate_percent: 1
                  subaff_address: '0x2222222222222222222222222222222222222222'
                  subaff_percent: 0.5
              client:
                summary: Client reusable wallet
                value:
                  address: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e'
                  callback: https://your-site.com/webhooks/nxtgatew
                  network: polygon
                  mode: client
                  client_id: cust_42
      responses:
        '201':
          description: Wallet created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WalletCreateResponse'
        '200':
          description: Existing client wallet reused
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WalletCreateResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Invalid API credentials
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Merchant account inactive
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Conflict (duplicate address or client wallet still processing)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /api/v1/wallets/check-config:
    get:
      tags: [Wallets]
      summary: Verify wallet split configuration
      description: Returns database fields plus on-chain configuration for a deposit wallet you own.
      security:
        - ApiKeyAuth: []
          ApiSecretAuth: []
      parameters:
        - name: wallet_address
          in: query
          required: true
          schema:
            type: string
          description: Deposit address returned by create (`data.address`)
        - name: network
          in: query
          required: false
          schema:
            type: string
            enum: [ethereum, polygon, amoy, tron, solana]
            default: polygon
      responses:
        '200':
          description: Configuration snapshot
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WalletCheckConfigResponse'
        '400':
          description: Missing wallet_address
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Invalid API credentials
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Wallet not found or not yours
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
    ApiSecretAuth:
      type: apiKey
      in: header
      name: X-API-Secret

  parameters:
    NftWalletId:
      name: wallet_id
      in: path
      required: true
      schema:
        type: string
    NftCollectionId:
      name: collection_id
      in: path
      required: true
      schema:
        type: string
    NftId:
      name: nft_id
      in: path
      required: true
      schema:
        type: string
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      schema:
        type: string
    XTimestamp:
      name: X-Timestamp
      in: header
      required: false
      schema:
        type: string
      description: Required when X-Api-Signature is sent (Unix epoch, ±5 minutes)
    XNonce:
      name: X-Nonce
      in: header
      required: false
      schema:
        type: string
      description: Unique per request; replay returns 401
    XApiSignature:
      name: X-Api-Signature
      in: header
      required: false
      schema:
        type: string
      description: |
        Optional hex HMAC-SHA256 of
        METHOD + "\n" + PATH + "\n" + TIMESTAMP + "\n" + NONCE + "\n" + SHA256(raw body),
        keyed with api_secret. PATH example: /api/v1/nft/nfts
    XSignature:
      name: X-Signature
      in: header
      required: false
      schema:
        type: string
      description: Alias of X-Api-Signature

  schemas:
    NftWalletCreateRequest:
      type: object
      required: [external_user_id]
      properties:
        external_user_id:
          type: string
        network:
          type: string
          default: polygon
        callback:
          type: string
          description: Alias callback_url
        address:
          type: string
          description: Optional merchant payout address

    NftCollectionCreateRequest:
      type: object
      required: [name]
      properties:
        name:
          type: string
        symbol:
          type: string
        asset_type:
          type: string
          enum: [membership, digital_license, order_receipt, reward, certificate, collectible]
        network:
          type: string
          default: polygon
        transfer_policy_default:
          type: string
          enum: [soulbound, non_transferable, transferable]
        base_metadata_uri:
          type: string

    NftMintRequest:
      type: object
      required: [asset_type, collection_id, wallet_id, external_user_id]
      properties:
        asset_type:
          type: string
          enum: [membership, digital_license, order_receipt, reward, certificate, collectible]
        collection_id:
          type: string
        wallet_id:
          type: string
        external_user_id:
          type: string
        external_reference:
          type: string
        external_order_id:
          type: string
        external_product_id:
          type: string
        transfer_policy:
          type: string
        attributes:
          type: object
          additionalProperties: true
        license:
          type: object
          additionalProperties: true


    NftAsset:
      type: object
      properties:
        nft_id: { type: string }
        asset_type: { type: string }
        collection_id: { type: string, nullable: true }
        wallet_id: { type: string }
        wallet_address: { type: string }
        external_user_id: { type: string, nullable: true }
        network: { type: string }
        contract_address: { type: string, nullable: true }
        token_id: { type: string, nullable: true }
        status: { type: string }
        blockchain_status: { type: string }
        membership_level: { type: string, nullable: true }
        metadata_uri: { type: string, nullable: true }
        metadata_version: { type: integer }
        attributes:
          type: object
          additionalProperties: true
        mint_tx_hash: { type: string, nullable: true }
        created_at: { type: string, format: date-time, nullable: true }

    NftOperation:
      type: object
      properties:
        operation_id: { type: string }
        operation_type: { type: string }
        operation_status:
          type: string
          description: pending | processing | awaiting_confirmation | confirmed | failed
        idempotency_key: { type: string, nullable: true }
        tx_hash: { type: string, nullable: true }
        nft:
          $ref: '#/components/schemas/NftAsset'

    NftApiError:
      type: object
      required: [success, error]
      properties:
        success:
          type: boolean
          example: false
        error:
          type: object
          required: [code, message, request_id]
          properties:
            code: { type: string, example: AUTHENTICATION_FAILED }
            message: { type: string }
            request_id: { type: string }
            details:
              type: object
              additionalProperties: true

    NftApiSuccess:
      type: object
      required: [success, data]
      properties:
        success:
          type: boolean
          example: true
        message: { type: string }
        data:
          type: object
          additionalProperties: true

    ErrorResponse:
      type: object
      required: [success, error]
      properties:
        success:
          type: boolean
          example: false
        error:
          type: string
        hint:
          type: string

    WalletCreateRequest:
      type: object
      required: [address, callback]
      properties:
        address:
          type: string
          description: Merchant payout address (final destination after split)
        callback:
          type: string
          format: uri
          description: Webhook / IPN URL (alias callback_url)
        network:
          type: string
          enum: [ethereum, polygon, amoy, tron, solana]
          default: ethereum
        mode:
          type: string
          enum: [ephemeral, client]
          default: ephemeral
        client_id:
          type: string
          minLength: 1
          maxLength: 128
          description: Required when mode=client (alias client_external_id)
        affiliate_address:
          type: string
        affiliate_percent:
          type: number
          description: Affiliate share as % of gross (1 = 1%)
        affiliate_percent_bps:
          type: integer
          description: Affiliate share in basis points (100 = 1%) unless split_amounts_unit=percent
        subaff_address:
          type: string
        subaff_percent:
          type: number
        subaff_percent_bps:
          type: integer
        split_amounts_unit:
          type: string
          enum: [bps, percent]
          default: bps
        ipn_token:
          type: string
          description: Optional merchant-supplied correlation token
        transaction_id:
          type: string
          maxLength: 128
          description: Merchant payment id (echoed in webhooks). Alias meta.transaction_id
        meta:
          type: object
          additionalProperties: true
          description: Optional metadata; `transaction_id` inside is accepted

    WalletCreateData:
      type: object
      properties:
        address:
          type: string
          description: Generated deposit address for the payer
        ipn_token:
          type: string
        network:
          type: string
        callback_url:
          type: string
        created_at:
          type: string
        wallet_mode:
          type: string
          enum: [ephemeral, client]
        client_id:
          type: string
          nullable: true
        transaction_id:
          type: string
          nullable: true
          description: Echoed when sent on create (merchant payment id)
        private_key:
          type: string
          description: >
            Deposit wallet private key (plaintext). Present only when wallet_mode=client
            (create or reuse). Never returned for ephemeral wallets. Treat as a secret —
            required to export / migrate the wallet to another provider.
          nullable: true
        reused:
          type: boolean
        affiliate_wallet_address:
          type: string
        subaff_wallet_address:
          type: string
        platform_percent:
          type: number
        affiliate_percent:
          type: number
        subaff_percent:
          type: number
        split_config:
          type: object
          description: Present only for admin API authentication on EVM networks
          additionalProperties: true

    WalletCreateResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        data:
          $ref: '#/components/schemas/WalletCreateData'
        message:
          type: string

    WalletCheckConfigResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: object
          properties:
            wallet_address:
              type: string
            network:
              type: string
            database:
              type: object
              additionalProperties: true
            on_chain:
              type: object
              additionalProperties: true
            match:
              type: boolean

    WalletBalanceChangedWebhook:
      type: object
      description: POST body for event wallet.balance_changed
      properties:
        event:
          type: string
          example: wallet.balance_changed
        wallet_id:
          type: integer
        deposit_address:
          type: string
        network:
          type: string
        chain_id:
          type: integer
        ipn_token:
          type: string
        wallet_mode:
          type: string
        client_id:
          type: string
          nullable: true
        transaction_id:
          type: string
          nullable: true
          description: Merchant payment id from create (StixPay order)
        labels:
          type: object
          additionalProperties: true
        movements_summary:
          type: object
          additionalProperties: true
        movement:
          type: object
          additionalProperties: true
        balance_after_raw:
          type: string
        asset_key:
          type: string

    WalletMovementsSnapshotWebhook:
      type: object
      description: POST body for event wallet.movements_snapshot
      properties:
        event:
          type: string
          example: wallet.movements_snapshot
        wallet_id:
          type: integer
        deposit_address:
          type: string
        network:
          type: string
        ipn_token:
          type: string
        wallet_mode:
          type: string
        client_id:
          type: string
          nullable: true
        transaction_id:
          type: string
          nullable: true
          description: Merchant payment id from create (StixPay order)
        labels:
          type: object
          additionalProperties: true
        split_complete:
          type: boolean
        movements_reconciled:
          type: boolean
        movements_summary:
          type: object
          additionalProperties: true
        movements:
          type: array
          items:
            type: object
            additionalProperties: true
        movements_truncated:
          type: boolean
