openapi: 3.1.0
info:
  title: Onlobo Payment Gateway API
  version: 1.0.1
  description: Merchant-facing signed API for direct card payments, refunds, subscriptions, payouts, funds, daily reports and asynchronous notifications. Amounts use integer minor units and business timestamps use yyyy-MM-dd HH:mm:ss in Asia/Shanghai.
servers:
  - url: https://onlobo.com
security:
  - MerchantSignature: []
tags:
  - name: Payments
  - name: Refunds
  - name: Subscriptions
  - name: Payouts
  - name: Funds
  - name: Reports
paths:
  /gateway/v1/payments:
    post:
      tags: [Payments]
      summary: Create payment
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/CreatePayment'}
      responses:
        '200': {$ref: '#/components/responses/Success'}
        '400': {$ref: '#/components/responses/Error'}
    get:
      tags: [Payments]
      summary: Query payments
      parameters:
        - {$ref: '#/components/parameters/Page'}
        - {$ref: '#/components/parameters/PageSize'}
        - {name: merchantOrderNo, in: query, schema: {type: string}}
        - {name: status, in: query, schema: {$ref: '#/components/schemas/PaymentStatus'}}
      responses:
        '200': {$ref: '#/components/responses/Success'}
  /gateway/v1/payments/{paymentNo}:
    get:
      tags: [Payments]
      summary: Get payment
      parameters:
        - {$ref: '#/components/parameters/PaymentNo'}
      responses:
        '200': {$ref: '#/components/responses/Success'}
        '404': {$ref: '#/components/responses/Error'}
  /gateway/v1/payments/{paymentNo}/attempts:
    get:
      tags: [Payments]
      summary: List routing attempts
      parameters:
        - {$ref: '#/components/parameters/PaymentNo'}
      responses:
        '200': {$ref: '#/components/responses/Success'}
  /gateway/v1/payments/{paymentNo}/provider-events:
    get:
      tags: [Payments]
      summary: List verified provider result events for a payment
      parameters:
        - {$ref: '#/components/parameters/PaymentNo'}
      responses:
        '200': {$ref: '#/components/responses/Success'}
  /gateway/v1/payments/{paymentNo}/refunds:
    post:
      tags: [Refunds]
      summary: Create refund
      parameters:
        - {$ref: '#/components/parameters/PaymentNo'}
        - {$ref: '#/components/parameters/IdempotencyKey'}
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/CreateRefund'}
      responses:
        '200': {$ref: '#/components/responses/Success'}
        '409': {$ref: '#/components/responses/Error'}
  /gateway/v1/refunds:
    get:
      tags: [Refunds]
      summary: Query refunds
      parameters:
        - {$ref: '#/components/parameters/Page'}
        - {$ref: '#/components/parameters/PageSize'}
      responses:
        '200': {$ref: '#/components/responses/Success'}
  /gateway/v1/subscriptions:
    post:
      tags: [Subscriptions]
      summary: Create subscription
      parameters:
        - {$ref: '#/components/parameters/IdempotencyKey'}
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/CreateSubscription'}
      responses:
        '200': {$ref: '#/components/responses/Success'}
    get:
      tags: [Subscriptions]
      summary: Query subscriptions
      parameters:
        - {$ref: '#/components/parameters/Page'}
        - {$ref: '#/components/parameters/PageSize'}
        - {name: subscriptionNo, in: query, schema: {type: string}}
        - {name: merchantSubscriptionNo, in: query, schema: {type: string}}
        - {name: providerSubscriptionId, in: query, schema: {type: string}}
        - {name: customerEmail, in: query, schema: {type: string, format: email}}
        - {name: status, in: query, schema: {$ref: '#/components/schemas/SubscriptionStatus'}}
      responses:
        '200': {$ref: '#/components/responses/Success'}
  /gateway/v1/subscriptions/{subscriptionNo}:
    get:
      tags: [Subscriptions]
      summary: Get subscription
      parameters:
        - {$ref: '#/components/parameters/SubscriptionNo'}
      responses:
        '200': {$ref: '#/components/responses/Success'}
  /gateway/v1/subscriptions/{subscriptionNo}/invoices:
    get:
      tags: [Subscriptions]
      summary: List every subscription cycle, including failed cycles
      parameters:
        - {$ref: '#/components/parameters/SubscriptionNo'}
      responses:
        '200': {$ref: '#/components/responses/Success'}
  /gateway/v1/subscriptions/{subscriptionNo}/cancel:
    post:
      tags: [Subscriptions]
      summary: Cancel subscription
      parameters:
        - {$ref: '#/components/parameters/SubscriptionNo'}
      responses:
        '200': {$ref: '#/components/responses/Success'}
  /gateway/v1/payouts:
    post:
      tags: [Payouts]
      summary: Create payout
      parameters:
        - {$ref: '#/components/parameters/IdempotencyKey'}
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/CreatePayout'}
      responses:
        '200': {$ref: '#/components/responses/Success'}
    get:
      tags: [Payouts]
      summary: Query payouts
      parameters:
        - {$ref: '#/components/parameters/Page'}
        - {$ref: '#/components/parameters/PageSize'}
        - {name: status, in: query, schema: {$ref: '#/components/schemas/PayoutStatus'}}
      responses:
        '200': {$ref: '#/components/responses/Success'}
  /gateway/v1/payouts/{payoutNo}:
    get:
      tags: [Payouts]
      summary: Get payout
      parameters:
        - {name: payoutNo, in: path, required: true, schema: {type: string}}
      responses:
        '200': {$ref: '#/components/responses/Success'}
  /gateway/v1/payouts/{payoutNo}/query:
    post:
      tags: [Payouts]
      summary: Query payout status from provider
      parameters:
        - {name: payoutNo, in: path, required: true, schema: {type: string}}
      responses:
        '200': {$ref: '#/components/responses/Success'}
  /gateway/v1/funds/ledger:
    get:
      tags: [Funds]
      summary: Query merchant ledger
      parameters:
        - {$ref: '#/components/parameters/Page'}
        - {$ref: '#/components/parameters/PageSize'}
      responses:
        '200': {$ref: '#/components/responses/Success'}
  /gateway/v1/daily-reports:
    get:
      tags: [Reports]
      summary: Query daily reports
      parameters:
        - {name: from, in: query, schema: {type: string, format: date}}
        - {name: to, in: query, schema: {type: string, format: date}}
        - {name: status, in: query, description: 0 means all statuses, schema: {type: integer, minimum: 0}}
        - {$ref: '#/components/parameters/Page'}
        - {$ref: '#/components/parameters/PageSize'}
      responses:
        '200': {$ref: '#/components/responses/Success'}
  /gateway/v1/daily-reports/{date}/download:
    get:
      tags: [Reports]
      summary: Download the fixed daily Excel report
      parameters:
        - {name: date, in: path, required: true, schema: {type: string, format: date}}
      responses:
        '200':
          description: Excel report stream
          content:
            application/vnd.openxmlformats-officedocument.spreadsheetml.sheet:
              schema: {type: string, format: binary}
webhooks:
  merchantEvent:
    post:
      summary: Merchant-level or order-level asynchronous event delivery
      security: []
      parameters:
        - {name: X-Gateway-Event-Id, in: header, required: true, schema: {type: string}}
        - {name: X-Gateway-Delivery-Id, in: header, required: true, schema: {type: string}}
        - {name: X-Gateway-Timestamp, in: header, required: true, schema: {type: string}}
        - {name: X-Gateway-Signature-Version, in: header, required: true, schema: {type: string}}
        - {name: X-Gateway-Signature, in: header, required: true, schema: {type: string, pattern: '^v1=[0-9a-f]{64}$'}}
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/MerchantEvent'}
      responses:
        '200':
          description: The response body, after trimming surrounding whitespace, must equal SUCCESS exactly.
components:
  securitySchemes:
    MerchantSignature:
      type: apiKey
      in: header
      name: X-Signature
      description: Requires X-Merchant-Id, X-Api-Key, X-Timestamp, X-Nonce and X-Signature. See the online signature guide.
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      schema: {type: string, maxLength: 128}
    Page:
      name: page
      in: query
      schema: {type: integer, minimum: 1, default: 1}
    PageSize:
      name: pageSize
      in: query
      schema: {type: integer, minimum: 1, maximum: 200, default: 50}
    PaymentNo:
      name: paymentNo
      in: path
      required: true
      schema: {type: string}
    SubscriptionNo:
      name: subscriptionNo
      in: path
      required: true
      schema: {type: string}
  responses:
    Success:
      description: Successful API response
      content:
        application/json:
          schema: {$ref: '#/components/schemas/ApiResponse'}
    Error:
      description: Standard error response
      content:
        application/json:
          schema: {$ref: '#/components/schemas/ApiResponse'}
  schemas:
    ApiResponse:
      type: object
      required: [code, message, requestId]
      properties:
        code: {type: integer, description: 1 means success}
        message: {type: string}
        data: {}
        requestId: {type: string}
    CreatePayment:
      type: object
      additionalProperties: false
      required: [merchantOrderNo, currency, amount, paymentMethod]
      properties:
        merchantOrderNo: {type: string, maxLength: 128}
        currency: {type: string, pattern: '^[A-Z]{3}$'}
        amount: {type: integer, minimum: 1, format: int64, description: Minor currency unit; 1234 means 12.34}
        paymentMethod: {type: string, maxLength: 64, description: Current live value is CARD}
        channelCode: {type: string, maxLength: 64}
        channelSelectionMode: {type: string, enum: [AUTO, PREFERRED, STRICT]}
        country: {type: string, pattern: '^[A-Z]{2}$'}
        returnUrl: {type: string, format: uri, maxLength: 2048}
        notifyUrl: {type: string, format: uri, maxLength: 2048}
        metadata:
          type: object
          maxProperties: 32
          description: Direct CARD requires first_name, last_name, shopper_email, card, expiration_year, expiration_month and security_code. Sensitive card values must never be logged or persisted by the merchant.
          additionalProperties: {type: string, maxLength: 2048}
    CreateRefund:
      type: object
      additionalProperties: false
      required: [merchantRefundNo, amount]
      properties:
        merchantRefundNo: {type: string, maxLength: 128}
        amount: {type: integer, minimum: 1, format: int64, description: Minor currency unit}
        reason: {type: string, maxLength: 512}
    CreateSubscription:
      type: object
      additionalProperties: false
      required: [merchantSubscriptionNo, customerReference, customerEmail, currency, planCode, paymentMethod]
      properties:
        merchantSubscriptionNo: {type: string, maxLength: 128}
        customerReference: {type: string, maxLength: 128}
        customerEmail: {type: string, format: email, maxLength: 320}
        currency: {type: string, pattern: '^[A-Z]{3}$'}
        planCode: {type: string, maxLength: 64}
        paymentMethod: {type: string, maxLength: 64, description: Current live value is CARD}
        notifyUrl: {type: string, format: uri, maxLength: 2048}
        paymentMethodData:
          type: object
          maxProperties: 32
          description: Direct CARD requires first_name, last_name, card, expiration_year, expiration_month and security_code. customerEmail is used as shopper_email.
          additionalProperties: {type: string, maxLength: 2048}
    CreatePayout:
      type: object
      additionalProperties: false
      required: [merchantPayoutNo, amount, currency, paymentMethod, recipientType, recipientName, recipientCountry, recipientAccountMask, recipientDetails]
      properties:
        merchantPayoutNo: {type: string, maxLength: 128}
        amount: {type: integer, minimum: 1, format: int64, description: Minor currency unit}
        currency: {type: string, pattern: '^[A-Z]{3}$'}
        paymentMethod: {type: string, maxLength: 64}
        channelCode: {type: string, maxLength: 64}
        channelSelectionMode: {type: string, enum: [AUTO, PREFERRED, STRICT]}
        recipientType: {type: integer, minimum: 1}
        recipientName: {type: string, maxLength: 128}
        recipientCountry: {type: string, pattern: '^[A-Z]{2}$'}
        recipientAccountMask: {type: string, maxLength: 128}
        recipientDetails: {type: object, minProperties: 1, maxProperties: 64, additionalProperties: {type: string, maxLength: 2048}}
        purpose: {type: string, maxLength: 500}
        notifyUrl: {type: string, format: uri, maxLength: 2048}
    PaymentStatus:
      type: string
      enum: [CREATED, PROCESSING, REQUIRES_ACTION, AUTHORIZED, SUCCEEDED, FAILED, CANCELLED, EXPIRED]
    RefundStatus:
      type: string
      enum: [CREATED, PROCESSING, SUCCEEDED, FAILED, CANCELLED, UNKNOWN]
    SubscriptionStatus:
      type: string
      enum: [INCOMPLETE, TRIALING, ACTIVE, PAST_DUE, PAUSED, CANCELLED, EXPIRED]
    PayoutStatus:
      type: string
      enum: [CREATED, PROCESSING, PENDING_CONFIRMATION, SUCCEEDED, FAILED, CLOSED]
    MerchantEvent:
      type: object
      required: [eventId, merchantId, eventType, aggregateType, aggregateId, aggregateVersion, data, occurredAt]
      properties:
        eventId: {type: string}
        merchantId: {type: integer, format: int64}
        eventType: {type: string, example: payment.succeeded}
        aggregateType: {type: string, example: PAYMENT}
        aggregateId: {type: string}
        aggregateVersion: {type: integer, format: int64}
        data: {type: object, additionalProperties: true}
        occurredAt: {type: string, pattern: '^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}$', description: Asia/Shanghai business time}
