openapi: 3.0.3
info:
  title: Payblis Gateway API
  version: 1.0.0
  description: |
    Fiat-to-crypto payment gateway — merchant REST API (sandbox & live).

    ## Authentication

    All merchant endpoints require `X-API-Key` (sandbox or live key from the dashboard).
    Environment is selected by URL path: `/api/sandbox/…` (sandbox key) or `/api/…` (live key).
    Do not pass `?environment=`.

    Mutating requests require HMAC headers with the merchant secret key:
    `X-Payblis-Timestamp`, `X-Payblis-Signature` (`timestamp + '.' + raw_body`), `X-Payblis-Nonce`.

    ## Outbound webhooks (IPN)

    Payblis POSTs JSON to `ipnURL` or the profile default webhook URL, signed with the secret key
    (`X-Payblis-Signature` + body `signature`). Verify HMAC-SHA256 on the JSON without the
    `signature` field.

    Payment events: `payment.created`, `payment.completed`, `payment.failed`, `payment.cancelled`,
    `payment.expired`. Payment payloads do not include `crypto_currency` or `network`.

    After PayGate confirms funds are forwarded to the merchant wallet, a second webhook
    `settlement.completed` is sent with `amount_crypto`, `crypto_currency`, `wallet_address`,
    `network`, and `sent_at`.

servers:
  - url: http://localhost:8888/payblis_app
    description: Local MAMP — use /api/sandbox/… (sandbox key) or /api/… (live key)
  - url: https://payblis.com
    description: Production — use /api/sandbox/… (sandbox key) or /api/… (live key)

tags:
  - name: Payments
    description: Create, query, cancel payments and list transactions
  - name: Balance
    description: Merchant settlement buckets
  - name: Quotes
    description: Indicative rates before checkout
  - name: Webhooks
    description: Test outbound webhook delivery
  - name: Sandbox
    description: Sandbox-only simulate endpoints

paths:
  /api/payment/create:
    post:
      tags: [Payments]
      summary: Create a payment
      description: Hosted checkout. Returns `RefOrder` + `checkout_url`.
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PaymentCreateRequest'
            examples:
              checkout:
                summary: Hosted checkout
                value:
                  amount: 19.99
                  currency: EUR
                  product_name: Premium plan
                  method: credit_cards
                  RefOrder: ORDER_123
                  Customer_Email: buyer@example.com
                  Customer_Name: Jane
                  Customer_LastName: Doe
                  country: FR
                  store_name: Example Store
                  urlOK: https://example.com/success
                  urlKO: https://example.com/cancel
                  ipnURL: https://example.com/webhooks/payblis
      responses:
        '201':
          description: Payment created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaymentCreateResponse'
        '422':
          description: Validation or business rule error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
      callbacks:
        paymentLifecycle:
          '{$request.body#/ipnURL}':
            post:
              summary: Payment lifecycle webhook (outbound to merchant)
              description: |
                Payblis POSTs to the `ipnURL` supplied at payment creation whenever the payment status changes.
                See `components/schemas/PaymentWebhookEvent`.
              requestBody:
                required: true
                content:
                  application/json:
                    schema:
                      $ref: '#/components/schemas/PaymentWebhookEvent'
              responses:
                '200':
                  description: Merchant acknowledged (any 2xx stops retries for synchronous dispatch)
                '4XX':
                  description: Client error — queued for retry when initial dispatch fails
                '5XX':
                  description: Server error — queued for retry

  /api/payment/{RefOrder}:
    get:
      tags: [Payments]
      summary: Get payment details by merchant RefOrder
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/RefOrder'
      responses:
        '200':
          description: Payment resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaymentShowResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'

  /api/payment/{RefOrder}/status:
    get:
      tags: [Payments]
      summary: Get payment status by merchant RefOrder (lightweight)
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/RefOrder'
      responses:
        '200':
          description: Status payload
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaymentStatusResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'

  /api/payment/{RefOrder}/cancel:
    post:
      tags: [Payments]
      summary: Cancel a pending payment by merchant RefOrder
      description: |
        Requires API key plus HMAC request signature headers
        (`X-Payblis-Timestamp`, `X-Payblis-Signature`, `X-Payblis-Nonce`).
        Body may be empty; signature uses `timestamp + '.' + ''`.
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/RefOrder'
      responses:
        '200':
          description: Cancelled (triggers `payment.cancelled` webhook)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaymentTransitionResponse'
        '401':
          description: Missing or invalid signature
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '422':
          description: Cannot cancel (terminal status or not found)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'

  /api/transactions:
    get:
      tags: [Payments]
      summary: List merchant transactions
      security:
        - ApiKeyAuth: []
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: per_page
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: q
          in: query
          description: Search RefOrder, product, customer email
          schema:
            type: string
        - name: status
          in: query
          schema:
            $ref: '#/components/schemas/PaymentStatus'
      responses:
        '200':
          description: Paginated list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TransactionListResponse'

  /api/balance:
    get:
      tags: [Balance]
      summary: Merchant balance buckets
      description: |
        Returns pending (unsettled completed payments), in-settlement, paid-out totals,
        and lifetime completed volume for the authenticated merchant environment.
      security:
        - ApiKeyAuth: []
      responses:
        '200':
          description: Balance buckets
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BalanceResponse'

  /api/quote:
    get:
      tags: [Quotes]
      summary: Indicative crypto quote
      description: |
        Sandbox/live indicative rate for a route selected by the routing engine.
        Requires merchant wallet to be configured.
      security:
        - ApiKeyAuth: []
      parameters:
        - name: amount
          in: query
          required: true
          schema:
            type: number
            exclusiveMinimum: 0
        - name: currency
          in: query
          description: Alias `fiat_currency`
          schema:
            type: string
            enum: [EUR, USD, CAD]
            example: EUR
        - name: country
          in: query
          required: true
          schema:
            type: string
            minLength: 2
            maxLength: 2
            example: FR
        - name: crypto_currency
          in: query
          schema:
            type: string
            enum: [USDC, USDT, ETH, BTC]
            default: USDC
        - name: network
          in: query
          schema:
            type: string
            enum: [polygon, ethereum, arbitrum, base, bsc]
            default: polygon
      responses:
        '200':
          description: Quote
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/QuoteResponse'
        '422':
          description: Validation error or no provider
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'

  /api/rates:
    get:
      tags: [Quotes]
      summary: Reference exchange rates
      security:
        - ApiKeyAuth: []
      responses:
        '200':
          description: Indicative rate table
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RatesResponse'

  /api/webhook/test:
    post:
      tags: [Webhooks]
      summary: Send test webhook to merchant URL
      description: |
        Dispatches a synthetic `webhook.test` (or custom `event`) payload signed like production webhooks.
        Uses the merchant secret key. URL defaults to profile default webhook when omitted.
      security:
        - ApiKeyAuth: []
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookTestRequest'
      responses:
        '200':
          description: Test dispatched
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookDispatchResult'
        '422':
          description: Invalid URL or merchant not configured
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'

  /api/sandbox/payment/{RefOrder}/complete:
    post:
      tags: [Sandbox]
      summary: Mark a sandbox payment completed
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/RefOrder'
      responses:
        '200':
          description: Payment completed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaymentTransitionResponse'
        '422':
          description: Cannot complete
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'

  /api/sandbox/payment/{RefOrder}/fail:
    post:
      tags: [Sandbox]
      summary: Mark a sandbox payment failed
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/RefOrder'
      responses:
        '200':
          description: Payment failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaymentTransitionResponse'
        '422':
          description: Cannot fail
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: Sandbox or live API key from merchant dashboard
    HmacAuth:
      type: apiKey
      in: header
      name: X-Payblis-Signature
      description: |
        Optional HMAC-SHA256 over raw JSON request body (production inbound API).
        Also send `X-Payblis-Timestamp`. Outbound webhooks use the same header name.

  parameters:
    RefOrder:
      name: RefOrder
      in: path
      required: true
      description: Merchant order reference used at create time (URL-encode if needed)
      schema:
        type: string
        example: ORDER_123

  schemas:
    Environment:
      type: string
      enum: [sandbox, live]

    PaymentMethod:
      type: string
      enum: [credit_cards, apple_pay, google_pay, paypal, bank_transfer, open_banking, cash_app, ideal, klarna]

    PaymentStatus:
      type: string
      description: Current payment status
      enum:
        - pending
        - waiting_payment
        - processing
        - completed
        - paid
        - failed
        - expired
        - cancelled
        - refunded
        - chargeback

    PaymentEventType:
      type: string
      description: Outbound webhook event name for payments
      enum:
        - payment.created
        - payment.completed
        - payment.failed
        - payment.cancelled
        - payment.expired
      example: payment.completed

    MerchantEventType:
      type: string
      enum:
        - kyb.approved
        - kyb.rejected
        - webhook.test

    ApiErrorResponse:
      type: object
      required: [success, error]
      properties:
        success:
          type: boolean
          example: false
        error:
          type: string
        code:
          type: string
          description: Machine-readable error code
          enum:
            - validation_error
            - invalid_amount
            - wallet_not_configured
            - environment_mismatch
            - no_provider
            - not_found

    MerchantOrderFields:
      type: object
      description: Merchant-facing field names echoed in webhooks and create requests
      properties:
        RefOrder:
          type: string
          description: Merchant order reference (alias `order_id`)
        amount:
          type: number
          format: double
        currency:
          type: string
          enum: [EUR, USD, CAD]
          description: Fiat currency (alias `fiat_currency` in internal API)
        product_name:
          type: string
        method:
          $ref: '#/components/schemas/PaymentMethod'
          description: Optional — buyer can choose on the Payblis checkout page.
        Customer_Email:
          type: string
          format: email
        Customer_Name:
          type: string
        Customer_LastName:
          type: string
        country:
          type: string
          minLength: 2
          maxLength: 2
          description: ISO 3166-1 alpha-2
        store_name:
          type: string
        urlOK:
          type: string
          format: uri
        urlKO:
          type: string
          format: uri
        ipnURL:
          type: string
          format: uri
          description: Webhook URL for this payment

    PaymentCreateRequest:
      allOf:
        - $ref: '#/components/schemas/MerchantOrderFields'
        - type: object
          required:
            - amount
            - currency
            - product_name
            - RefOrder
            - Customer_Email
            - Customer_Name
            - Customer_LastName
            - country
            - store_name
            - urlOK
            - urlKO
            - ipnURL
          properties: {}

    PaymentCreateResponse:
      type: object
      required: [success, RefOrder, checkout_url, status, environment]
      properties:
        success:
          type: boolean
          example: true
        RefOrder:
          type: string
          example: ORDER_123
          description: Merchant order reference (use this for get/status/cancel)
        checkout_url:
          type: string
          format: uri
          description: Hosted Payblis checkout URL
        status:
          $ref: '#/components/schemas/PaymentStatus'
        environment:
          $ref: '#/components/schemas/Environment'
        expires_at:
          type: string
          format: date-time
          nullable: true
          description: Checkout expiry (when applicable)

    PaymentResource:
      type: object
      required: [RefOrder, amount, fiat_currency, status, environment]
      properties:
        RefOrder:
          type: string
          description: Merchant order reference
        amount:
          type: number
          format: double
        fiat_currency:
          type: string
        status:
          $ref: '#/components/schemas/PaymentStatus'
        environment:
          $ref: '#/components/schemas/Environment'
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
          nullable: true

    PaymentShowResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        payment:
          $ref: '#/components/schemas/PaymentResource'

    PaymentStatusResponse:
      type: object
      required: [success, RefOrder, status]
      properties:
        success:
          type: boolean
          example: true
        RefOrder:
          type: string
        status:
          $ref: '#/components/schemas/PaymentStatus'

    PaymentTransitionResponse:
      type: object
      properties:
        success:
          type: boolean
        RefOrder:
          type: string
        payment_id:
          type: string
        status:
          $ref: '#/components/schemas/PaymentStatus'
        webhook:
          $ref: '#/components/schemas/WebhookDispatchResult'

    TransactionSummary:
      type: object
      properties:
        RefOrder:
          type: string
        amount:
          type: number
          format: double
        fiat_currency:
          type: string
        product_name:
          type: string
        payment_method:
          type: string
          nullable: true
        status:
          $ref: '#/components/schemas/PaymentStatus'
        environment:
          $ref: '#/components/schemas/Environment'
        created_at:
          type: string
          format: date-time
        customer_email:
          type: string
        country:
          type: string
        store_name:
          type: string
        settlement_status:
          type: string
          nullable: true
        settlement_ref:
          type: string
          nullable: true

    TransactionListResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        page:
          type: integer
        per_page:
          type: integer
        total:
          type: integer
        transactions:
          type: array
          items:
            $ref: '#/components/schemas/TransactionSummary'

    BalanceBucket:
      type: object
      properties:
        amount:
          type: number
          format: double
        payment_count:
          type: integer
          description: Used for `pending` and `total_completed`
        settlement_count:
          type: integer
          description: Used for `in_settlement` and `paid_out`

    BalanceResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        environment:
          $ref: '#/components/schemas/Environment'
        currency:
          type: string
          example: EUR
        pending:
          type: object
          properties:
            amount:
              type: number
            payment_count:
              type: integer
        in_settlement:
          type: object
          properties:
            amount:
              type: number
            settlement_count:
              type: integer
        paid_out:
          type: object
          properties:
            amount:
              type: number
            settlement_count:
              type: integer
        total_completed:
          type: object
          properties:
            amount:
              type: number
            payment_count:
              type: integer

    Quote:
      type: object
      properties:
        amount:
          type: number
        currency:
          type: string
        crypto_currency:
          type: string
        network:
          type: string
        crypto_amount:
          type: number
        exchange_rate:
          type: number
          description: Fiat units per 1 crypto unit
        fee_percent:
          type: number
        fee_amount:
          type: number
        fee_currency:
          type: string
        country:
          type: string
        provider:
          type: string
        provider_name:
          type: string
        environment:
          $ref: '#/components/schemas/Environment'
        valid_until:
          type: string
          format: date-time
        indicative:
          type: boolean
          example: true

    QuoteResponse:
      type: object
      properties:
        success:
          type: boolean
        quote:
          $ref: '#/components/schemas/Quote'

    RateRow:
      type: object
      properties:
        fiat:
          type: string
        crypto:
          type: string
        network:
          type: string
        rate:
          type: number
        inverse_rate:
          type: number

    RatesResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        environment:
          $ref: '#/components/schemas/Environment'
        rates:
          type: array
          items:
            $ref: '#/components/schemas/RateRow'
        updated_at:
          type: string
          format: date-time
        indicative:
          type: boolean
          example: true

    WebhookTestRequest:
      type: object
      properties:
        url:
          type: string
          format: uri
          description: Override merchant default webhook URL for this test
        event:
          type: string
          default: webhook.test

    WebhookDispatchResult:
      type: object
      properties:
        success:
          type: boolean
        skipped:
          type: boolean
          description: True when no webhook URL configured
        reason:
          type: string
        http_status:
          type: integer
        url:
          type: string
          format: uri
        response_body:
          type: string
          description: Truncated response from merchant endpoint
        error:
          type: string

    WebhookSignature:
      type: object
      properties:
        signature:
          type: string
          description: HMAC-SHA256 hex digest (duplicate of `X-Payblis-Signature` header)

    PaymentWebhookEvent:
      allOf:
        - type: object
          required: [event, amount, fiat_currency, status, environment]
          properties:
            event:
              oneOf:
                - $ref: '#/components/schemas/PaymentEventType'
                - type: string
                  pattern: '^payment\.[a-z_]+$'
                  description: Dynamic status events e.g. `payment.waiting_payment`
            amount:
              type: number
              format: double
            fiat_currency:
              type: string
            status:
              $ref: '#/components/schemas/PaymentStatus'
            environment:
              $ref: '#/components/schemas/Environment'
            test:
              type: boolean
              description: Present on test webhooks only
            timestamp:
              type: string
              format: date-time
              description: Present on test webhooks
        - $ref: '#/components/schemas/MerchantOrderFields'
        - $ref: '#/components/schemas/WebhookSignature'

    SettlementCompletedWebhookEvent:
      allOf:
        - type: object
          required: [event, amount_crypto, crypto_currency, sent_at]
          properties:
            event:
              type: string
              enum: [settlement.completed]
            RefOrder:
              type: string
              nullable: true
            payment_id:
              type: string
              nullable: true
            settlement_ref:
              type: string
              nullable: true
            amount_crypto:
              type: number
              format: double
              description: Net crypto amount received on the merchant wallet
            crypto_currency:
              type: string
              example: USDC
            wallet_address:
              type: string
              nullable: true
              description: Destination payout wallet
            network:
              type: string
              example: polygon
            txid_out:
              type: string
              nullable: true
            environment:
              $ref: '#/components/schemas/Environment'
            sent_at:
              type: string
              format: date-time
              description: Webhook send time (UTC)
            timestamp:
              type: string
              format: date-time
              description: Alias of sent_at
        - $ref: '#/components/schemas/WebhookSignature'

    KybWebhookEvent:
      allOf:
        - type: object
          required: [event, merchant_id, environment, timestamp]
          properties:
            event:
              type: string
              enum: [kyb.approved, kyb.rejected]
            merchant_id:
              type: string
              description: Public merchant identifier
            environment:
              $ref: '#/components/schemas/Environment'
            timestamp:
              type: string
              format: date-time
            kyb_status:
              type: string
              example: approved
            lifecycle_status:
              type: string
              example: live
            company_legal_name:
              type: string
              nullable: true
            review_notes:
              type: string
              nullable: true
        - $ref: '#/components/schemas/WebhookSignature'

    WebhookTestEvent:
      description: Payload sent by POST /api/webhook/test
      allOf:
        - $ref: '#/components/schemas/PaymentWebhookEvent'
        - type: object
          properties:
            event:
              type: string
              enum: [webhook.test]
            test:
              type: boolean
              example: true

    WebhookEvent:
      description: Union of outbound webhook payloads (discriminate on `event`)
      oneOf:
        - $ref: '#/components/schemas/PaymentWebhookEvent'
        - $ref: '#/components/schemas/SettlementCompletedWebhookEvent'
        - $ref: '#/components/schemas/KybWebhookEvent'
        - $ref: '#/components/schemas/WebhookTestEvent'
      discriminator:
        propertyName: event
        mapping:
          payment.created: '#/components/schemas/PaymentWebhookEvent'
          payment.completed: '#/components/schemas/PaymentWebhookEvent'
          payment.failed: '#/components/schemas/PaymentWebhookEvent'
          payment.cancelled: '#/components/schemas/PaymentWebhookEvent'
          payment.expired: '#/components/schemas/PaymentWebhookEvent'
          settlement.completed: '#/components/schemas/SettlementCompletedWebhookEvent'
          kyb.approved: '#/components/schemas/KybWebhookEvent'
          kyb.rejected: '#/components/schemas/KybWebhookEvent'
          webhook.test: '#/components/schemas/WebhookTestEvent'
