openapi: 3.1.0
info:
  title: GMPay Edge Merchant API
  version: 1.0.0
  description: >-
    GMPay Edge is a single-deployment, single-tenant gateway. GMPay is the
    primary merchant protocol, and EPay is a compatibility adapter over the
    same credential, order service, checkout, payment processor, and Webhook
    outbox. Internal operations use /admin and are outside this merchant API.
servers:
  - url: https://pay.example.com
paths:
  /payments/gmpay/v1/order/create-transaction:
    post:
      operationId: createGmpayTransaction
      summary: Create a GMPay transaction
      description: Accepts JSON or form data. Exclude signature and empty values, sort field names in ASCII order, join key=value pairs with &, and calculate lowercase HMAC-SHA256 using the API Secret as the HMAC key.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/GmpayCreateRequest" }
          application/x-www-form-urlencoded:
            schema: { $ref: "#/components/schemas/GmpayCreateRequest" }
      responses:
        "200":
          description: Transaction created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/GmpayCreateResponse" }
        "400": { $ref: "#/components/responses/GatewayError" }
        "401": { $ref: "#/components/responses/AuthenticationFailed" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "500": { $ref: "#/components/responses/GatewayError" }
        "502": { $ref: "#/components/responses/ProviderUnavailable" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }
      callbacks:
        orderNotification:
          '{$request.body#/notify_url}':
            post:
              summary: GMPay order notification
              description: The signature is lowercase HMAC-SHA256 over the sorted non-empty callback fields using the same API Secret as the HMAC key. Return plain text ok with HTTP 200.
              requestBody:
                required: true
                content:
                  application/json:
                    schema: { $ref: "#/components/schemas/GmpayNotification" }
              responses:
                "200":
                  description: Plain text ok acknowledgement
                  content:
                    text/plain:
                      schema: { type: string, const: ok }
  /payments/gmpay/v1/order/query:
    get:
      operationId: queryGmpayTransaction
      summary: Query a GMPay transaction
      description: Query by exactly one trade_id or order_id. Sign all non-empty query fields except signature using the same lowercase HMAC-SHA256 contract as order creation.
      parameters:
        - { name: pid, in: query, required: true, schema: { type: string } }
        - { name: trade_id, in: query, required: false, schema: { type: string, maxLength: 128 } }
        - { name: order_id, in: query, required: false, schema: { type: string, maxLength: 128 } }
        - { name: signature, in: query, required: true, schema: { type: string, pattern: "^[0-9a-f]{64}$" } }
      responses:
        "200":
          description: Transaction status and immutable payment snapshot
          content:
            application/json:
              schema: { $ref: "#/components/schemas/GmpayCreateResponse" }
        "400": { $ref: "#/components/responses/GatewayError" }
        "401": { $ref: "#/components/responses/AuthenticationFailed" }
        "404": { $ref: "#/components/responses/OrderNotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "500": { $ref: "#/components/responses/GatewayError" }
  /payments/epay/v1/order/create-transaction/submit.php:
    get:
      operationId: createEpayTransactionByQuery
      summary: Create an EPay-compatible transaction
      parameters:
        - { $ref: "#/components/parameters/EpayPid" }
        - { $ref: "#/components/parameters/EpayMoney" }
        - { $ref: "#/components/parameters/EpayOrderNo" }
        - { $ref: "#/components/parameters/EpayNotifyUrl" }
        - { $ref: "#/components/parameters/EpayReturnUrl" }
        - { $ref: "#/components/parameters/EpayName" }
        - { $ref: "#/components/parameters/EpayType" }
        - { $ref: "#/components/parameters/EpayParam" }
        - { $ref: "#/components/parameters/EpayClientIp" }
        - { $ref: "#/components/parameters/EpayDevice" }
        - { $ref: "#/components/parameters/EpaySign" }
        - { $ref: "#/components/parameters/EpaySignType" }
      responses:
        "200":
          description: Created; open data.payment_url to enter the unified checkout
          content:
            application/json:
              schema: { $ref: "#/components/schemas/GmpayCreateResponse" }
        "400": { $ref: "#/components/responses/GatewayError" }
        "401": { $ref: "#/components/responses/AuthenticationFailed" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "500": { $ref: "#/components/responses/GatewayError" }
        "502": { $ref: "#/components/responses/ProviderUnavailable" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }
    post:
      operationId: createEpayTransactionByForm
      summary: Create an EPay-compatible transaction
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema: { $ref: "#/components/schemas/EpayCreateRequest" }
      responses:
        "200":
          description: Created; open data.payment_url to enter the unified checkout
          content:
            application/json:
              schema: { $ref: "#/components/schemas/GmpayCreateResponse" }
        "400": { $ref: "#/components/responses/GatewayError" }
        "401": { $ref: "#/components/responses/AuthenticationFailed" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "500": { $ref: "#/components/responses/GatewayError" }
        "502": { $ref: "#/components/responses/ProviderUnavailable" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }
  /payments/epay/v1/order/create-transaction/mapi.php:
    get:
      operationId: createEpayMApiTransactionByQuery
      summary: Create an EPay transaction and return a Pro-compatible response
      parameters:
        - { $ref: "#/components/parameters/EpayPid" }
        - { $ref: "#/components/parameters/EpayMoney" }
        - { $ref: "#/components/parameters/EpayOrderNo" }
        - { $ref: "#/components/parameters/EpayNotifyUrl" }
        - { $ref: "#/components/parameters/EpayReturnUrl" }
        - { $ref: "#/components/parameters/EpayName" }
        - { $ref: "#/components/parameters/EpayType" }
        - { $ref: "#/components/parameters/EpayParam" }
        - { $ref: "#/components/parameters/EpayClientIp" }
        - { $ref: "#/components/parameters/EpayDevice" }
        - { $ref: "#/components/parameters/EpaySign" }
        - { $ref: "#/components/parameters/EpaySignType" }
      responses:
        "200":
          description: Transaction created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/EpayCreateResponse" }
        "400": { $ref: "#/components/responses/EpayError" }
        "401": { $ref: "#/components/responses/EpayError" }
        "429": { $ref: "#/components/responses/EpayError" }
        "500": { $ref: "#/components/responses/EpayError" }
        "502": { $ref: "#/components/responses/EpayError" }
        "503": { $ref: "#/components/responses/EpayError" }
    post:
      operationId: createEpayMApiTransactionByForm
      summary: Create an EPay transaction and return a Pro-compatible response
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema: { $ref: "#/components/schemas/EpayCreateRequest" }
      responses:
        "200":
          description: Transaction created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/EpayCreateResponse" }
        "400": { $ref: "#/components/responses/EpayError" }
        "401": { $ref: "#/components/responses/EpayError" }
        "413": { $ref: "#/components/responses/EpayError" }
        "429": { $ref: "#/components/responses/EpayError" }
        "500": { $ref: "#/components/responses/EpayError" }
        "502": { $ref: "#/components/responses/EpayError" }
        "503": { $ref: "#/components/responses/EpayError" }
  /payments/epay/v1/order/create-transaction/api.php:
    get:
      operationId: queryEpayTransaction
      summary: Query an EPay transaction
      description: Set act=order and sign the non-empty query fields with the merchant API secret. Plain-text keys are not accepted in URLs.
      parameters:
        - { name: act, in: query, required: true, schema: { const: order } }
        - { name: pid, in: query, required: true, schema: { type: string } }
        - { name: trade_no, in: query, required: false, schema: { type: string, maxLength: 128 } }
        - { name: out_trade_no, in: query, required: false, schema: { type: string, maxLength: 128 } }
        - { name: sign, in: query, required: true, schema: { type: string, pattern: "^[0-9a-f]{32}$" } }
        - { name: sign_type, in: query, required: false, schema: { const: MD5 } }
      responses:
        "200":
          description: EPay transaction status
          content:
            application/json:
              schema: { $ref: "#/components/schemas/EpayQueryResponse" }
        "400": { $ref: "#/components/responses/EpayError" }
        "401": { $ref: "#/components/responses/EpayError" }
        "404": { $ref: "#/components/responses/EpayError" }
        "429": { $ref: "#/components/responses/EpayError" }
        "500": { $ref: "#/components/responses/EpayError" }
components:
  parameters:
    EpayPid: { name: pid, in: query, required: true, schema: { type: string } }
    EpayMoney: { name: money, in: query, required: true, description: Positive decimal with at most 18 integer and 8 fraction digits., schema: { type: string, pattern: "^(?!0(?:\\.0+)?$)(?:0|[1-9]\\d{0,17})(?:\\.\\d{1,8})?$" } }
    EpayOrderNo: { name: out_trade_no, in: query, required: true, schema: { type: string, maxLength: 128 } }
    EpayNotifyUrl: { name: notify_url, in: query, required: true, schema: { type: string, format: uri } }
    EpayReturnUrl: { name: return_url, in: query, required: false, schema: { type: string, format: uri } }
    EpayName: { name: name, in: query, required: false, schema: { type: string, maxLength: 500 } }
    EpayType: { name: type, in: query, required: false, description: alipay keeps the existing selectable compatibility behavior; asset.network selects a payment method., schema: { type: string } }
    EpayParam: { name: param, in: query, required: false, description: Opaque merchant context returned unchanged in notifications, redirects, and queries., schema: { type: string, maxLength: 500 } }
    EpayClientIp: { name: clientip, in: query, required: false, schema: { type: string, maxLength: 64 } }
    EpayDevice: { name: device, in: query, required: false, schema: { type: string, maxLength: 64 } }
    EpaySign: { name: sign, in: query, required: true, schema: { type: string, pattern: "^[0-9a-f]{32}$" } }
    EpaySignType: { name: sign_type, in: query, required: false, schema: { const: MD5 } }
  responses:
    GatewayError:
      description: Gateway error
      content:
        application/json:
          schema: { $ref: "#/components/schemas/GatewayEnvelope" }
    EpayError:
      description: EPay compatibility error (code -1 with a short msg; rate limits, oversized bodies, and authentication failures use the same shape)
      content:
        application/json:
          schema: { $ref: "#/components/schemas/EpayError" }
    AuthenticationFailed:
      description: "status_code 401: PID, scope, or signature verification failed. An unknown PID and a bad signature are indistinguishable."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/GatewayEnvelope" }
    RateLimited:
      description: "status_code 429: the credential exceeded its per-minute request window, or the submitted PID accumulated more than 20 failed authentications within one minute. Retry after the window passes."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/GatewayEnvelope" }
    PayloadTooLarge:
      description: "status_code 10009: the request body exceeds 64 KiB."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/GatewayEnvelope" }
    OrderNotFound:
      description: "status_code 10001: no order matches trade_id or order_id under the authenticated credential."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/GatewayEnvelope" }
    ProviderUnavailable:
      description: "status_code 10003 (provider_unavailable): the hosted payment provider did not return a payment. The order and its external order ID were rolled back, so the same request may be retried."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/GatewayEnvelope" }
    ServiceUnavailable:
      description: "status_code 10003 when the receiving target or provider configuration is unavailable, or 10016 when no usable exchange rate exists for the order currency."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/GatewayEnvelope" }
  schemas:
    OrderStatus:
      type: string
      enum: [pending, confirming, partially_paid, paid, overpaid, expired, cancelled, failed, refunded]
    GmpayStatus:
      type: integer
      enum: [1, 2, 3, 4]
      description: 1 waiting for payment, 2 paid, 3 closed, 4 waiting for payment-method selection.
    GmpayCreateRequest:
      type: object
      required: [pid, order_id, currency, amount, notify_url, signature]
      properties:
        pid: { type: string, description: API credential PID }
        order_id: { type: string, minLength: 1, maxLength: 128 }
        currency: { type: string, pattern: "^[A-Za-z]{3}$", description: "Active ISO 4217 fiat currency code such as USD; an unsupported code answers status_code 10009." }
        token: { type: string, pattern: "^[A-Za-z0-9_-]{2,20}$", description: Omit together with network to let checkout select a payment method. }
        network: { type: string, pattern: "^[A-Za-z0-9-]{2,32}$", description: Omit together with token to let checkout select a payment method. }
        amount:
          description: "Positive decimal with at most 18 integer and 8 fraction digits; anything else answers status_code 10004."
          oneOf:
            - { type: string, pattern: "^(?!0(?:\\.0+)?$)(?:0|[1-9]\\d{0,17})(?:\\.\\d{1,8})?$" }
            - { type: number, exclusiveMinimum: 0 }
        notify_url: { type: string, format: uri, pattern: "^https://" }
        redirect_url: { type: string, format: uri, pattern: "^https://" }
        name: { type: string, maxLength: 500 }
        signature: { type: string, pattern: "^[0-9a-f]{64}$" }
      example:
        pid: "100000000001"
        order_id: invoice-1001
        currency: USD
        amount: "12.50"
        notify_url: https://merchant.example/notify
        signature: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
    GmpayCreateData:
      type: object
      required: [trade_id, order_id, amount, currency, actual_amount, receive_address, token, network, status, status_detail, expiration_time, payment_url]
      properties:
        trade_id: { type: string, pattern: "^[0-9]{20}$" }
        order_id: { type: string }
        amount: { type: string }
        currency: { type: string }
        actual_amount: { type: string }
        receive_address: { type: string }
        token: { type: string }
        network: { type: string }
        status: { $ref: "#/components/schemas/GmpayStatus" }
        status_detail: { $ref: "#/components/schemas/OrderStatus" }
        expiration_time: { type: integer }
        payment_url: { type: string, format: uri }
    GmpayCreateResponse:
      allOf:
        - { $ref: "#/components/schemas/GatewayEnvelope" }
        - type: object
          properties:
            data: { $ref: "#/components/schemas/GmpayCreateData" }
    GmpayNotification:
      type: object
      required: [pid, trade_id, order_id, amount, actual_amount, receive_address, token, block_transaction_id, status, signature]
      properties:
        pid: { type: string }
        trade_id: { type: string, pattern: "^[0-9]{20}$" }
        order_id: { type: string }
        amount: { type: string }
        actual_amount: { type: string }
        receive_address: { type: string }
        token: { type: string }
        block_transaction_id: { type: string }
        status:
          type: integer
          enum: [1, 2, 3]
        signature: { type: string, pattern: "^[0-9a-f]{64}$" }
      example:
        pid: "100000000001"
        trade_id: "26071406211234567890"
        order_id: invoice-1001
        amount: "12.50"
        actual_amount: "12.50"
        receive_address: TExampleAddress
        token: USDT
        block_transaction_id: transaction-hash
        status: 2
        signature: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
    EpayCreateRequest:
      type: object
      required: [pid, money, out_trade_no, notify_url, sign]
      properties:
        pid: { type: string }
        money: { type: string, pattern: "^(?!0(?:\\.0+)?$)(?:0|[1-9]\\d{0,17})(?:\\.\\d{1,8})?$" }
        out_trade_no: { type: string, maxLength: 128 }
        notify_url: { type: string, format: uri }
        return_url: { type: string, format: uri }
        name: { type: string, maxLength: 500 }
        type: { type: string, description: alipay or asset.network }
        param: { type: string, maxLength: 500 }
        clientip: { type: string, maxLength: 64 }
        device: { type: string, maxLength: 64 }
        sign: { type: string, pattern: "^[0-9a-f]{32}$" }
        sign_type: { const: MD5 }
    EpayError:
      type: object
      required: [code, msg]
      properties:
        code: { type: integer, const: -1 }
        msg: { type: string }
    EpayCreateResponse:
      type: object
      required: [code, msg, trade_no, payurl, qrcode, img, param]
      properties:
        code: { type: integer, const: 1 }
        msg: { type: string, const: success }
        trade_no: { type: string }
        payurl: { type: string, format: uri }
        qrcode: { type: string, format: uri }
        img: { type: string, format: uri }
        param: { type: string }
    EpayQueryResponse:
      type: object
      required: [code, msg, trade_no, out_trade_no, type, name, money, status, trade_status, param]
      properties:
        code: { type: integer, const: 1 }
        msg: { type: string, const: success }
        trade_no: { type: string }
        out_trade_no: { type: string }
        type: { type: string }
        name: { type: string }
        money: { type: string }
        status: { type: integer, enum: [0, 1] }
        trade_status: { type: string }
        param: { type: string }
    GatewayEnvelope:
      type: object
      required: [status_code, message, data, request_id]
      properties:
        status_code:
          type: integer
          description: "200 success; 10001 order not found (query, HTTP 404); 10002 external order ID already exists; 10003 receiving method or provider unavailable, including a hosted-provider failure (HTTP 400, 502, or 503); 10004 amount invalid (non-positive, or more than 18 integer / 8 fraction digits); 10009 invalid parameters (unsupported currency, notify_url not public HTTPS, redirect_url not HTTPS, token without network, or a body above 64 KiB with HTTP 413); 10016 asset, network, or exchange rate unavailable (HTTP 400 or 503); 401 authentication failed; 429 rate limit or authentication-failure limit exceeded; 500 system error."
        message: { type: string }
        data: {}
        request_id: { type: string }
      example:
        status_code: 10009
        message: invalid parameters
        data: null
        request_id: 73e3648b-b257-47e6-a510-12772cac0448
