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/gmpay/v1/target-readiness:
    get:
      operationId: queryGmpayTargetReadiness
      summary: Inspect strict per-order receiving-target supply
      description: Edge extension for merchant adapters. Requires an orders:create key; sign all non-empty query fields except signature like order creation. Reports the instance attribution policy, target-history schema readiness, chain connection health, and the number of enabled targets for the token and network that have never served an order. Targets are operator-entered public addresses; the response does not prove custody or durable storage.
      parameters:
        - { name: pid, in: query, required: true, schema: { type: string } }
        - { name: token, in: query, required: true, schema: { type: string, maxLength: 20 } }
        - { name: network, in: query, required: true, schema: { type: string, maxLength: 32 } }
        - { name: signature, in: query, required: true, schema: { type: string, pattern: "^[0-9a-f]{64}$" } }
      responses:
        "200":
          description: Target supply readiness
          content:
            application/json:
              schema: { $ref: "#/components/schemas/GmpayTargetReadinessResponse" }
        "400": { $ref: "#/components/responses/GatewayError" }
        "401": { $ref: "#/components/responses/AuthenticationFailed" }
        "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 }
        payment_target: { $ref: "#/components/schemas/GmpayPaymentTarget" }
        payment_evidence: { $ref: "#/components/schemas/GmpayPaymentEvidence" }
    GmpayPaymentTarget:
      type: object
      description: Present once a receiving method is selected; read from the stored order snapshot.
      required: [rail_kind, contract_address, decimals, expected_amount_units, required_confirmations, target_policy, allocation_policy, target_ownership, received_amount_units]
      properties:
        rail_kind: { type: string, enum: [chain, exchange, wallet] }
        contract_address: { type: [string, "null"] }
        decimals: { type: integer }
        expected_amount_units: { type: string, pattern: "^(0|[1-9][0-9]*)$" }
        required_confirmations: { type: integer, description: Configured threshold captured at allocation; not an observed count }
        target_policy: { type: string, enum: [strict, legacy_shared], description: Current instance policy at response time; may differ from allocation_policy }
        allocation_policy: { type: string, enum: [strict, legacy_shared, unknown], description: Policy recorded atomically with the order snapshot. unknown for snapshots created before migration 0011 or carrying an unrecognised value; never retrospectively strict }
        target_ownership: { type: string, enum: [dedicated, shared_or_unknown, not_applicable] }
        received_amount_units: { type: string, pattern: "^(0|[1-9][0-9]*)$", description: Observed units including unconfirmed transfers; status_detail decides credit }
    GmpayPaymentEvidence:
      type: object
      description: Edge extension. Stored per-event facts behind the order balance, one per order_payments row joined to its blockchain_transactions fact. No external query is made. Sum only events whose fact is consistent and whose payment_status and chain_status are both confirmed, deduplicate by (network, tx_hash, event_index), and treat complete=false as not fulfilable.
      required: [complete, incomplete_reason, max_events, event_count, events]
      properties:
        complete: { type: boolean }
        incomplete_reason: { type: [string, "null"], enum: [rail_not_chain, too_many_events, chain_fact_missing, chain_fact_inconsistent, received_sum_mismatch, null] }
        max_events: { type: integer, description: Maximum events listed, currently 20 }
        event_count: { type: integer, minimum: 0, description: Total payment rows for the order, including any not listed }
        events:
          type: array
          maxItems: 20
          items:
            type: object
            required: [network, tx_hash, event_index, recipient, asset_code, amount_units, block_number, block_hash, chain_status, payment_status, observed_confirmations, fact_updated_at, fact]
            properties:
              network: { type: string }
              tx_hash: { type: string }
              event_index: { type: integer, description: Log index; two transfers in one transaction are distinct events }
              recipient: { type: [string, "null"] }
              asset_code: { type: [string, "null"] }
              amount_units: { type: string, pattern: "^(0|[1-9][0-9]*)$", description: Units attributed by the payment row }
              block_number: { type: [string, "null"] }
              block_hash: { type: [string, "null"] }
              chain_status: { type: [string, "null"], enum: [pending, missing, confirmed, reorged, failed, null] }
              payment_status: { type: string, enum: [detected, confirming, confirmed, pending_review, reorged, rejected] }
              observed_confirmations: { type: [integer, "null"], description: Count recorded by the last scan, not the required threshold }
              fact_updated_at: { type: [integer, "null"], description: Unix milliseconds of the last stored fact update }
              fact: { type: string, enum: [consistent, missing, network_mismatch, recipient_mismatch, asset_mismatch, amount_mismatch] }
    GmpayTargetReadinessResponse:
      allOf:
        - { $ref: "#/components/schemas/GatewayEnvelope" }
        - type: object
          properties:
            data:
              type: object
              required: [token, network, target_policy, schema_ready, rail_kind, connection_ready, available_targets, ready, target_provenance]
              properties:
                token: { type: string }
                network: { type: string }
                target_policy: { type: string, enum: [strict, legacy_shared] }
                schema_ready: { type: boolean, description: Structural presence of the target-history table and snapshot allocation_policy column; not a migration version or checksum check }
                rail_kind: { type: [string, "null"] }
                connection_ready: { type: boolean }
                available_targets: { type: integer, minimum: 0 }
                ready: { type: boolean }
                target_provenance: { type: string, enum: [operator_provisioned_unverified] }
    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
