openapi: 3.1.0
info:
  title: Coinplexo Merchant API
  version: 1.0.0-alpha
  summary: Crypto Payment Infrastructure
  description: |
    Self-hosted, non-custodial crypto payment gateway.

    **Authentication:** every `/api/v1` request carries a merchant API key in `X-API-Key`
    (or `Authorization: Bearer <key>`). Keys are scoped (`invoices:write`, `invoices:read`,
    `merchant:read`, `webhooks:test`) and can be rotated/revoked from the dashboard.

    **Idempotency:** `POST /api/v1/invoices` honours an `Idempotency-Key` header (max 255 chars).
    Replaying the same key + body returns the original response with `Idempotent-Replayed: true`;
    the same key with a different body returns `422 idempotency_mismatch`.

    **Amounts:** on-chain amounts are strings of integer base units (satoshi, wei, lamports, token
    base units). Fiat amounts are decimal strings. Never parse amounts as floats.

    **Webhooks:** signed with `Coinplexo-Signature: t=<unix>,v1=<hex hmac-sha256(secret, "<t>.<body>")>`.
    Reject signatures older than 5 minutes and verify in constant time. Deliveries retry with
    exponential backoff (30s → 6h) up to the configured maximum; handlers must be idempotent on `event_id`.
servers:
  - url: https://api.coinplexo.com
    description: Production (example)
  - url: http://localhost:8080
    description: Local development
security:
  - ApiKeyAuth: []
tags:
  - name: Invoices
  - name: Merchants
  - name: Webhooks
  - name: Assets
  - name: Public Checkout
paths:
  /api/v1/assets:
    get:
      tags: [Assets]
      summary: List enabled assets
      responses:
        "200":
          description: Enabled assets across enabled networks
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items: { $ref: "#/components/schemas/Asset" }
  /api/v1/min-amount:
    get:
      tags: [Assets]
      summary: Minimum invoice amounts per chain
      description: >-
        Per-chain minimum invoice amount in the merchant's fiat currency ($9.99 floor, Bitcoin $29.99 by default;
        configurable by the operator), expressed per asset together with the equivalent base units at the current rate
        and, when the merchant's `network_fee_payer` is `customer`, the fee gross-up a customer would pay today.
        Use it to hide or grey out chains that make no sense for the order amount. Payments below a minimum are not
        credited and are flagged for operator review.
      parameters:
        - { name: chain, in: query, schema: { type: string }, description: "Network id filter (e.g. bitcoin, solana)" }
        - { name: asset, in: query, schema: { type: string }, description: "Asset id filter (e.g. USDC_SOL)" }
        - { name: currency, in: query, schema: { type: string }, description: "Fiat currency (default: merchant currency)" }
      responses:
        "200":
          description: Minimums
          content:
            application/json:
              schema:
                type: object
                properties:
                  network_fee_payer: { type: string, enum: [merchant, customer] }
                  items:
                    type: array
                    items:
                      type: object
                      properties:
                        network_id: { type: string }
                        asset_id: { type: string }
                        fiat_currency: { type: string }
                        min_fiat: { type: string, example: "9.99" }
                        min_units: { $ref: "#/components/schemas/Units" }
                        fee_grossup_fiat: { type: string, example: "1", description: "Legacy name of processing_fee_fiat" }
                        processing_fee_fiat: { type: string, example: "1", description: "Processing fee the customer pays on top of the order amount, in the requested currency (USDT on TRON $1.00, TRX $0.10; 0 when none)" }
                        enabled: { type: boolean }
  /api/v1/invoices:
    post:
      tags: [Invoices]
      summary: Create an invoice
      description: Locks an exchange rate, derives a unique deposit address and returns the hosted checkout URL.
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CreateInvoiceRequest" }
            example:
              external_order_id: "order-10492"
              customer_ref: "user_883"
              asset: "USDC_SOL"
              fiat_currency: "USD"
              fiat_amount: "49.00"
              description: "Pro plan — 1 month"
              success_url: "https://shop.example.com/thanks"
              metadata: { plan: "pro" }
      responses:
        "201":
          description: Invoice created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Invoice" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "409": { $ref: "#/components/responses/Conflict" }
        "422": { $ref: "#/components/responses/Validation" }
        "429": { $ref: "#/components/responses/RateLimited" }
    get:
      tags: [Invoices]
      summary: List invoices for the authenticated merchant
      parameters:
        - { name: status, in: query, schema: { type: string }, description: Comma-separated statuses }
        - { name: asset, in: query, schema: { type: string } }
        - { name: q, in: query, schema: { type: string }, description: Search id prefix, order id, address, tx hash }
        - { name: limit, in: query, schema: { type: integer, default: 50, maximum: 200 } }
        - { name: offset, in: query, schema: { type: integer, default: 0 } }
      responses:
        "200":
          description: Page of invoices
          content:
            application/json:
              schema: { $ref: "#/components/schemas/InvoicePage" }
  /api/v1/invoices/{id}:
    get:
      tags: [Invoices]
      summary: Get an invoice with its payments
      parameters: [{ $ref: "#/components/parameters/InvoiceID" }]
      responses:
        "200":
          description: Invoice
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Invoice" }
        "404": { $ref: "#/components/responses/NotFound" }
  /api/v1/invoices/{id}/payments:
    get:
      tags: [Invoices]
      summary: List on-chain payments recorded against an invoice
      parameters: [{ $ref: "#/components/parameters/InvoiceID" }]
      responses:
        "200":
          description: Payments
          content:
            application/json:
              schema:
                type: object
                properties:
                  invoice_id: { type: string, format: uuid }
                  payments:
                    type: array
                    items: { $ref: "#/components/schemas/Payment" }
  /api/v1/merchants/{id}/transactions:
    get:
      tags: [Merchants]
      summary: List all payments for the merchant
      parameters:
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
        - { name: limit, in: query, schema: { type: integer } }
        - { name: offset, in: query, schema: { type: integer } }
      responses:
        "200":
          description: Payments page
          content:
            application/json:
              schema:
                type: object
                properties:
                  items: { type: array, items: { $ref: "#/components/schemas/Payment" } }
                  total: { type: integer }
  /api/v1/merchants/{id}/balances:
    get:
      tags: [Merchants]
      summary: Ledger balances credited to the merchant (per asset)
      parameters:
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        "200":
          description: Balances
          content:
            application/json:
              schema:
                type: object
                properties:
                  merchant_id: { type: string, format: uuid }
                  balances:
                    type: array
                    items:
                      type: object
                      properties:
                        code: { type: string, example: "merchant:7f3c…:USDC_SOL" }
                        asset_id: { type: string }
                        balance: { $ref: "#/components/schemas/Units" }
  /api/v1/webhooks/test:
    post:
      tags: [Webhooks]
      summary: Send a signed test event to every enabled webhook
      responses:
        "202":
          description: Queued
          content:
            application/json:
              schema:
                type: object
                properties:
                  event_id: { type: string, format: uuid }
                  status: { type: string, example: queued }
  /pub/v1/invoices/{id}:
    get:
      tags: [Public Checkout]
      security: []
      summary: Customer-safe invoice projection used by the hosted checkout page
      parameters: [{ $ref: "#/components/parameters/InvoiceID" }]
      responses:
        "200":
          description: Public invoice
          content:
            application/json:
              schema: { $ref: "#/components/schemas/PublicInvoice" }
  /pub/v1/invoices/{id}/events:
    get:
      tags: [Public Checkout]
      security: []
      summary: Server-Sent Events stream of status updates
      description: |
        Emits `snapshot` on connect, then `update` events with `{status, detected_amount, confirmed_amount,
        confirmations, required_confirmations, tx_hash}`. Clients should reconnect on close; the stream
        closes itself after a terminal status or 30 minutes.
      parameters: [{ $ref: "#/components/parameters/InvoiceID" }]
      responses:
        "200":
          description: text/event-stream
          content:
            text/event-stream:
              schema: { type: string }
components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
  parameters:
    InvoiceID:
      name: id
      in: path
      required: true
      schema: { type: string, format: uuid }
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      schema: { type: string, maxLength: 255 }
  responses:
    Unauthorized:
      description: Missing or invalid API key
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    NotFound:
      description: Not found
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Conflict:
      description: Idempotency conflict
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Validation:
      description: Validation error
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    RateLimited:
      description: Rate limit exceeded (see X-RateLimit-* headers)
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
  schemas:
    Error:
      type: object
      required: [error, message]
      properties:
        error: { type: string, example: validation_error }
        message: { type: string }
    Units:
      type: string
      pattern: "^[0-9]+$"
      description: Exact integer amount in the asset's smallest unit.
      example: "49000000"
    InvoiceStatus:
      type: string
      enum: [CREATED, WAITING_PAYMENT, PAYMENT_DETECTED, PARTIALLY_PAID, CONFIRMING, PAID, UNDERPAID, OVERPAID, EXPIRED, LATE_PAYMENT, MANUAL_REVIEW, FAILED]
    Asset:
      type: object
      properties:
        id: { type: string, example: USDC_SOL }
        network_id: { type: string, example: solana }
        symbol: { type: string, example: USDC }
        name: { type: string }
        kind: { type: string, enum: [native, erc20, spl, trc20] }
        contract_address: { type: [string, "null"] }
        decimals: { type: integer }
        min_payment_units: { $ref: "#/components/schemas/Units" }
    CreateInvoiceRequest:
      type: object
      required: [asset, fiat_amount]
      properties:
        external_order_id: { type: string, maxLength: 128 }
        customer_ref: { type: string, maxLength: 128 }
        asset: { type: string, description: Asset id such as USDC_SOL, BTC, USDT_TRON }
        fiat_currency: { type: string, default: merchant default, example: USD }
        fiat_amount: { type: string, example: "49.00" }
        description: { type: string, maxLength: 500 }
        metadata: { type: object, additionalProperties: true, maxProperties: 20 }
        success_url: { type: string, format: uri }
        cancel_url: { type: string, format: uri }
        ttl_seconds: { type: integer, minimum: 300, maximum: 86400, description: Quote validity (default 3600) }
    Invoice:
      type: object
      properties:
        id: { type: string, format: uuid }
        merchant_id: { type: string, format: uuid }
        external_order_id: { type: [string, "null"] }
        customer_ref: { type: [string, "null"] }
        network_id: { type: string }
        asset_id: { type: string }
        asset_symbol: { type: string }
        asset_decimals: { type: integer }
        status: { $ref: "#/components/schemas/InvoiceStatus" }
        fiat_currency: { type: string }
        fiat_amount: { type: string }
        rate_locked: { type: string }
        rate_expires_at: { type: string, format: date-time }
        expected_amount: { $ref: "#/components/schemas/Units" }
        expected_amount_human: { type: string, example: "49" }
        fee_grossup_fiat: { type: string, example: "0", description: "Legacy name of processing_fee_fiat" }
        fee_grossup_units: { $ref: "#/components/schemas/Units" }
        processing_fee_fiat: { type: string, example: "1", description: "Customer-paid processing fee in fiat_currency, already included in expected_amount. The merchant is credited fiat_amount; the fee is not part of the merchant's balance." }
        processing_fee_units: { $ref: "#/components/schemas/Units" }
        tolerance_bps: { type: integer }
        required_confirmations: { type: integer }
        deposit_address: { type: string }
        detected_amount: { $ref: "#/components/schemas/Units" }
        confirmed_amount: { $ref: "#/components/schemas/Units" }
        credited_amount: { oneOf: [{ $ref: "#/components/schemas/Units" }, { type: "null" }] }
        sweep_status: { type: string, enum: [none, pending, swept, partial, failed, uneconomical] }
        checkout_url: { type: string, format: uri }
        explorer_address_url: { type: string }
        created_at: { type: string, format: date-time }
        expires_at: { type: string, format: date-time }
        paid_at: { type: [string, "null"], format: date-time }
        payments:
          type: array
          items: { $ref: "#/components/schemas/Payment" }
    InvoicePage:
      type: object
      properties:
        items: { type: array, items: { $ref: "#/components/schemas/Invoice" } }
        total: { type: integer }
        limit: { type: integer }
        offset: { type: integer }
    Payment:
      type: object
      properties:
        id: { type: string, format: uuid }
        invoice_id: { type: string, format: uuid }
        tx_hash: { type: string }
        asset_id: { type: string }
        deposit_address: { type: string }
        output_index: { type: integer }
        amount: { $ref: "#/components/schemas/Units" }
        amount_human: { type: string }
        status: { type: string, enum: [detected, confirming, confirmed, reorged, failed] }
        is_late: { type: boolean }
        confirmations: { type: integer }
        required_confirmations: { type: integer }
        credited: { type: boolean }
        explorer_tx_url: { type: string }
        detected_at: { type: string, format: date-time }
        confirmed_at: { type: [string, "null"], format: date-time }
    PublicInvoice:
      type: object
      properties:
        id: { type: string, format: uuid }
        merchant_name: { type: string }
        status: { $ref: "#/components/schemas/InvoiceStatus" }
        network: { type: string }
        network_name: { type: string }
        asset: { type: string }
        asset_symbol: { type: string }
        asset_kind: { type: string }
        contract_address: { type: [string, "null"] }
        decimals: { type: integer }
        fiat_amount: { type: string }
        fiat_currency: { type: string }
        expected_amount: { $ref: "#/components/schemas/Units" }
        expected_amount_human: { type: string }
        detected_amount: { $ref: "#/components/schemas/Units" }
        confirmed_amount: { $ref: "#/components/schemas/Units" }
        remaining_amount: { $ref: "#/components/schemas/Units" }
        remaining_amount_human: { type: string }
        deposit_address: { type: string }
        payment_uri: { type: string, description: BIP21 / EIP-681 / Solana Pay URI for QR codes }
        required_confirmations: { type: integer }
        confirmations: { type: integer }
        description: { type: string }
        created_at: { type: string, format: date-time }
        expires_at: { type: string, format: date-time }
        success_url: { type: [string, "null"] }
        cancel_url: { type: [string, "null"] }
        explorer_address_url: { type: string }
        latest_tx_hash: { type: string }
        latest_tx_url: { type: string }
    WebhookEvent:
      type: object
      description: Body delivered to merchant webhook endpoints.
      properties:
        event_id: { type: string, format: uuid, description: From header Coinplexo-Event-Id }
        event_type:
          type: string
          enum: [invoice.created, payment.detected, payment.confirming, payment.confirmed, payment.partial, payment.overpaid, invoice.expired, payment.late, payment.review_required, sweep.completed, sweep.failed, webhook.test]
        invoice_id: { type: string, format: uuid }
        merchant_id: { type: string, format: uuid }
        transaction_id: { type: [string, "null"], description: On-chain tx hash }
        timestamp: { type: string, format: date-time }
        payment_amount: { oneOf: [{ $ref: "#/components/schemas/Units" }, { type: "null" }] }
        network: { type: string }
        asset: { type: string }
        status: { $ref: "#/components/schemas/InvoiceStatus" }
        expected_amount: { $ref: "#/components/schemas/Units" }
        confirmed_amount: { $ref: "#/components/schemas/Units" }
        confirmations: { type: integer }
        required_confirmations: { type: integer }
