openapi: 3.1.0
info:
  title: gopay Collect & Payout API
  version: 1.0.0
  summary: Header-signed collection and payout endpoints, with the callback verification spec.
  description: |
    This document covers the gopay collection (pay-in) and payout (pay-out) endpoints. All endpoints live under `/api/v2`, request and response bodies are JSON, and every request must be signed.

    A field marked **required** in the tables below returns `INVALID_PARAMS` when absent. All other fields are optional; conditionally required fields state their condition.

    # 1 · Getting started

    ## 1.1 Credentials

    Credentials are issued as a pair and shown under **Profile** in the merchant portal, which asks for the current 6-digit authenticator code.

    | Credential | Format | Purpose |
    |---|---|---|
    | API key | `pk_` + 24 characters | Sent in the clear as `X-Api-Key`; identifies the account. |
    | API secret | 48 characters | Signing key. Server-side only; never sent with a request. |

    The secret is displayed once, at issue. If it is lost, support must reset the pair, which issues a new key and a new secret and invalidates the old ones immediately.

    The account also needs API access enabled, or every request returns `API_V2_NOT_ENABLED`. If an IP allowlist is configured, calls from other addresses return `IP_RESTRICTION`.

    ## 1.2 Verifying your signing implementation

    Before connecting to live data, reproduce the signature below from these fixed values.

    | Item | Value |
    |---|---|
    | API key | `pk_test000000000000000000000` |
    | API secret | `v2secretv2secretv2secretv2secretv2secretv2secret` |
    | `X-Timestamp` | `1753776000` |
    | `X-Nonce` | `abcdef1234567890` |

    Request body (`POST /api/v2/deposits`):

    ```json
    {"channel":"bank","amount":"100.00","tx_id":"v2-golden-000001","callback_url":"https://merchant.example/cb","redirect_url":"https://merchant.example/rd"}
    ```

    | Computed | Value |
    |---|---|
    | Body SHA-256 | `d5d9e95ba808488ddc9802b5d4ba0beece0f0744c2b25942c41f2e55418ef0b9` |
    | `X-Signature` | `cc77eded5990e920a71464a52f5e108c334a9ef9399e4c4ecca4142d1566e36b` |

    If your output differs, use the [signature tester](tester.html) to compare the body hash, the canonical string and the signature separately.

    ## 1.3 Your first request

    Use `GET /api/v2/balance` as the first call: no body, read-only, no side effects. With the credentials, timestamp and nonce above, its signature is `c3a922528796d81abb54e97cb2abb0f98ba9c1fb0a5cf651d08aa9e7843e44d9`.

    # 2 · Request signing

    ## 2.1 Headers

    Every request carries these four headers.

    | Header | Description |
    |---|---|
    | `X-Api-Key` | The API key, in the clear. |
    | `X-Timestamp` | Unix seconds, digits only, within ±300 seconds of server time. Keep your clock synchronised. |
    | `X-Nonce` | 16–64 random characters, not reused by the same merchant within 10 minutes. |
    | `X-Signature` | The signature from the next section, lowercase hex. |

    ## 2.2 Algorithm

    ```
    X-Signature = hmac_sha256( secret, "v2:" + METHOD + "\n" + PATH + "\n" + TIMESTAMP + "\n" + NONCE + "\n" + sha256hex(body) )
    ```

    | Component | Rule |
    |---|---|
    | `METHOD` | The HTTP method, uppercase. |
    | `PATH` | Path only, leading slash, no host and no query string. Include the id when querying one order, e.g. `/api/v2/deposits/GP-D-88120001`. |
    | `TIMESTAMP` | Byte-identical to the `X-Timestamp` header. |
    | `NONCE` | Byte-identical to the `X-Nonce` header. |
    | `body` | The request body bytes as sent. For `GET` this is the empty string, whose SHA-256 is always `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855`. |

    The `v2:` prefix and the `/api/v2` path segment are fixed parts of the protocol format and do not track a version number.

    ## 2.3 Body consistency

    Serialise the body once, hash that result, and send the same bytes.

    If you hash a JSON string and then hand the object to an HTTP client to serialise again, differences in key order or whitespace make the bytes you sent differ from the bytes you signed, and the gateway returns `INVALID_SIGNATURE`. This is the most common integration error.

    # 3 · Callbacks

    When an order reaches a final state, the gateway `POST`s the result to that order's `callback_url`. The callback is what determines that funds moved; the query endpoints exist to compensate for a missed callback and are not a substitute for handling one.

    The callback body is bare JSON. It does not use the response envelope the other endpoints return.

    ## 3.1 Verification

    Each delivery carries `X-Timestamp`, `X-Nonce` and `X-Signature`. The callback canonical string contains no method and no path: a callback URL may be rewritten by the merchant's own gateway, so the signature binds the body bytes only.

    ```
    X-Signature = hmac_sha256( secret, "v2-callback:" + TIMESTAMP + "\n" + NONCE + "\n" + sha256hex(rawBody) )
    ```

    Hash the raw body bytes as received; do not parse and re-serialise. Compare in constant time. Reject deliveries whose timestamp falls outside your own tolerance window, and treat a repeated nonce as a replay.

    ## 3.2 Idempotency

    The same order may be delivered more than once, including timeout retries and manual re-pushes by an administrator. Callback handling must be idempotent: a repeated delivery must not move your ledger again.

    ## 3.3 Reference values

    | Item | Value |
    |---|---|
    | `X-Timestamp` | `1753776000` |
    | `X-Nonce` | `abcdef1234567890` |
    | Body SHA-256 | `3ebcb27243ec898cdcc80e6309fffaf70bf4369e07c610f70445d1dbb361906c` |
    | `X-Signature` | `4d1e566dc184e84770dae832f4f62317887b1a02b73f54dadaec4c2107b5772c` |

    For this body:

    ```json
    {"order_id":"DP1753776000GOLDEN01","tx_id":"v2-golden-000001","transferror_name":"-","status":"COMPLETE","order_amount":"100.00","service_change":"2.00","final_amount":"98.00"}
    ```

    # 4 · Collection vs payout

    Both directions share one signing scheme, one envelope and one callback format. The differences are these.

    | Aspect | Collection `/deposits` | Payout `/withdrawals` |
    |---|---|---|
    | Fee direction | Deducted from the order: `final_amount = amount − service_change` | Added on top: `final_amount = amount + service_change` |
    | Funds flow | The customer pays `amount`; the merchant receives `final_amount` | The merchant balance is debited `final_amount`; insufficient balance returns `MERCHANT_INSUFFICIENT_BALANCE` |
    | Amount precision | Up to 2 decimals | Exactly 2 decimals |
    | Customer involved | Yes — show the collection account or redirect to `redirect_url` | No — place the order and wait for the callback |
    | Amount to display | `allocated_amount`, which may differ from the submitted `amount` by one cent | Not applicable |

    `tx_id` is the merchant's own reference. It must be unique within the account, is compared case-insensitively, and returns `DUPLICATE_TRANSACTION` if reused.

    # 5 · Error handling

    ## 5.1 Common causes

    | Code | Cause |
    |---|---|
    | `INVALID_SIGNATURE` | The bytes signed differ from the bytes sent; or `PATH` included a host or query string; or the timestamp or nonce in the canonical string differs from the header. |
    | `SIGNATURE_EXPIRED` | The timestamp is outside the ±300 second window or is not plain digits; or the nonce is the wrong length or was reused within 10 minutes. |
    | `INVALID_MERCHANT` | `X-Api-Key` is missing or invalid. A missing key and an invalid key return exactly the same response; the API does not reveal whether a key exists. |
    | `API_V2_NOT_ENABLED` | The credentials are valid, but API access is not enabled for the account. |

    ## 5.2 All result codes

    | Code | Meaning |
    |---|---|
    | `SUCCESS` | The request succeeded. |
    | `FAIL` | Unclassified failure: a server error, or an order failure with no finer type. |
    | `INVALID_MERCHANT` | `X-Api-Key` missing or invalid. |
    | `MERCHANT_BLOCKED` | The merchant account is not active. |
    | `API_V2_NOT_ENABLED` | API access is not enabled for the account. |
    | `SIGNATURE_EXPIRED` | Timestamp out of window or not numeric; also returned for a nonce of the wrong length or a replayed one. |
    | `INVALID_SIGNATURE` | The signature does not match the canonical string. |
    | `IP_RESTRICTION` | An IP allowlist is enabled and the caller is not on it. |
    | `INVALID_PARAMS` | Validation failed; `message` carries the first validation error. |
    | `DUPLICATE_TRANSACTION` | The `tx_id` has been used, or one `customer_id` has too many pending orders. |
    | `SERVICE_NOT_AVAILABLE` | The merchant has no collection or payout service enabled. |
    | `PAYMENT_METHOD_NOT_SUBSCRIBE` | The account is not subscribed to the channel or tier selected. |
    | `MERCHANT_INSUFFICIENT_BALANCE` | Payout amount plus fee exceeds the available balance. |
    | `TRANSACTION_NOT_FOUND` | No order with that `tx_id` exists under the account. |
    | `NO_SERVICE_PROVIDED` | No collection account could be allocated. Retry shortly or contact support. |
    | `SERVICE_UNDER_MAINTENANCE` | The selected channel is temporarily unavailable. |
    | `PAYMENT_GATEWAY_MAINTENENCE` | Signing subsystem maintenance. The spelling matches what the API returns. |
    | `TOO_MANY_REQUEST` | Rate limit reached. |
    | `PATH_NOT_FOUND` | Unknown internal method; not reachable through the documented endpoints. |

    Every response carries a `request_id`. Quote it when contacting support.

    # 6 · Conventions

    ## 6.1 Response envelope

    Every response except callbacks uses one structure: `status` (1 success / 0 failure), `code`, `message` (may be null), `data`, `request_id`.

    HTTP status codes: 403 for authentication failures, 422 for validation failures, 429 for rate limiting, and 500 for unhandled server errors, where `code` is always `FAIL` and no exception detail is exposed.

    ## 6.2 Rate limits

    300 requests per minute per merchant. Requests whose merchant cannot be resolved share a bucket of 20 per minute per IP.
  contact:
    name: Merchant support
servers:
  - url: https://api.gopayonline.asia
    description: gopay production gateway
tags:
  - name: Preparation
    description: Two read-only endpoints for checking balance and available banks before integrating.
  - name: Creating orders
    description: Collection and payout order creation. Both share one signing scheme, envelope and callback format.
  - name: Queries and callbacks
    description: Order status lookup, and the result callbacks the gateway pushes.
security:
  - ApiKey: []
    Timestamp: []
    Nonce: []
    Signature: []
paths:
  /api/v2/deposits:
    post:
      tags:
        - Creating orders
      operationId: createDeposit
      summary: Create a collection order
      description: |
        Creates a pay-in order and returns the order id, the fee breakdown and `data.payment_details`.

        `payment_details` supports two ways to collect. Use either.

        1. Render the details yourself — `bank_name`, `account_name`, `account_number` and `allocated_amount` — and have the customer transfer accordingly.
        2. Redirect the customer to `payment_details.redirect_url` and use the gateway's hosted payment page. This field is always present.

        ```json
        "payment_details": {
          "bank_name": "Hang Seng Bank",
          "account_name": "GOPAY COLLECTION SERVICES",
          "account_number": "024778150993",
          "allocated_amount": "100.00",
          "redirect_url": "https://pay.gopayonline.hk/o/DP1754870400GP88120001"
        }
        ```

        Display `allocated_amount` to the customer. So the payment can be matched, the figure to transfer may differ from the submitted `amount` by one cent; fall back to `order_amount` only when `allocated_amount` is `null`.

        The channel tier is selected automatically from the amount. The account must be subscribed to the tier selected, or the request returns `PAYMENT_METHOD_NOT_SUBSCRIBE`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DepositCreateRequest"
            examples:
              goldenVector:
                summary: Signature reference vector
                description: |
                  With key `pk_test000000000000000000000`, secret `v2secretv2secretv2secretv2secretv2secretv2secret`, `X-Timestamp: 1753776000` and `X-Nonce: abcdef1234567890`, this body signs to `cc77eded5990e920a71464a52f5e108c334a9ef9399e4c4ecca4142d1566e36b`. These values are the signing reference shared across brands.
                value:
                  channel: bank
                  amount: "100.00"
                  tx_id: v2-golden-000001
                  callback_url: https://merchant.example/cb
                  redirect_url: https://merchant.example/rd
              typical:
                summary: Ordinary request
                value:
                  channel: bank
                  amount: "100.00"
                  tx_id: GP-D-88120001
                  callback_url: https://api.yourshop.hk/gopay/callback
                  redirect_url: https://yourshop.hk/pay/done
                  transferror_name: LEE MEI LING
      responses:
        "200":
          description: Collection order created.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Envelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/DepositCreateData"
              examples:
                created:
                  value:
                    status: 1
                    code: SUCCESS
                    message: null
                    data:
                      order_id: DP1754870400GP88120001
                      tx_id: GP-D-88120001
                      order_amount: "100.00"
                      service_change: "2.00"
                      final_amount: "98.00"
                      transferror_name: LEE MEI LING
                      payment_details:
                        bank_name: Hang Seng Bank
                        account_name: GOPAY COLLECTION SERVICES
                        account_number: "024778150993"
                        allocated_amount: "100.00"
                        redirect_url: https://pay.gopayonline.hk/o/DP1754870400GP88120001
                    request_id: b7d41f02-9c33-4e1a-8f60-15c8a7e4d920
        "403":
          $ref: "#/components/responses/AuthError"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/ServerError"
  /api/v2/deposits/{tx_id}:
    get:
      tags:
        - Queries and callbacks
      operationId: getDeposit
      summary: Query a collection order
      description: |
        Returns the current state of an order by the merchant's own `tx_id`, compared case-insensitively and scoped to the account.

        This endpoint compensates for a missed callback; it is not a substitute for handling callbacks.

        The signed `PATH` must include the `tx_id`, for example `/api/v2/deposits/GP-D-88120001`.
      parameters:
        - $ref: "#/components/parameters/TxId"
      responses:
        "200":
          description: The order state, or `TRANSACTION_NOT_FOUND` in the envelope when no such order exists.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Envelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/DepositInquiryData"
              examples:
                found:
                  value:
                    status: 1
                    code: SUCCESS
                    message: null
                    data:
                      order_id: DP1754870400GP88120001
                      tx_id: GP-D-88120001
                      transferror_name: LEE MEI LING
                      status: COMPLETE
                      order_amount: "100.00"
                      service_change: "2.00"
                      final_amount: "98.00"
                    request_id: c81a5d63-2f74-4b09-9ea3-6d20b9f31c48
                notFound:
                  value:
                    status: 0
                    code: TRANSACTION_NOT_FOUND
                    message: null
                    data: []
                    request_id: c81a5d63-2f74-4b09-9ea3-6d20b9f31c48
        "403":
          $ref: "#/components/responses/AuthError"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/ServerError"
  /api/v2/withdrawals:
    post:
      tags:
        - Creating orders
      operationId: createWithdrawal
      summary: Create a payout order
      description: |
        Creates a pay-out order. The common fields are `channel`, `amount`, `tx_id` and `callback_url`; each channel adds the fields below.

        | `channel` | Additional required fields |
        |---|---|
        | `bank` | `bank_code`, `account_number`, `account_name` |
        | `fps` | `mobile_no` |

        The fee is added on top of the order amount: the merchant balance is debited `amount + service_change`, the `final_amount` in the response. Insufficient balance returns `MERCHANT_INSUFFICIENT_BALANCE`.

        `amount` here must carry exactly 2 decimals (`500.00`), which is stricter than the collection endpoint.

        Take `bank_code` from the [bank list](#tag/Preparation/operation/listBanks).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WithdrawCreateRequest"
            examples:
              bank:
                summary: Bank channel (channel = bank)
                value:
                  channel: bank
                  amount: "500.00"
                  tx_id: GP-W-88120007
                  callback_url: https://api.yourshop.hk/gopay/callback
                  account_number: "0247788901"
                  account_name: WONG KA HO
                  bank_code: "003"
              fps:
                summary: FPS channel (channel = fps)
                value:
                  channel: fps
                  amount: "500.00"
                  tx_id: GP-W-88120008
                  callback_url: https://api.yourshop.hk/gopay/callback
                  mobile_no: "98761234"
      responses:
        "200":
          description: Order created and the balance committed.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Envelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/WithdrawCreateData"
              examples:
                created:
                  value:
                    status: 1
                    code: SUCCESS
                    message: null
                    data:
                      order_id: WT1754870400GP88120007
                      tx_id: GP-W-88120007
                      order_amount: "500.00"
                      service_change: "10.00"
                      final_amount: "510.00"
                    request_id: d934e7b1-4a86-4c52-b7d1-08f6a2e59b37
        "403":
          $ref: "#/components/responses/AuthError"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/ServerError"
  /api/v2/withdrawals/{tx_id}:
    get:
      tags:
        - Queries and callbacks
      operationId: getWithdrawal
      summary: Query a payout order
      description: |
        Returns the current state of an order by the merchant's own `tx_id`, compared case-insensitively and scoped to the account. As on the collection side, this endpoint compensates for a missed callback.
      parameters:
        - $ref: "#/components/parameters/TxId"
      responses:
        "200":
          description: The order state, or `TRANSACTION_NOT_FOUND` in the envelope when no such order exists.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Envelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/WithdrawInquiryData"
              examples:
                found:
                  value:
                    status: 1
                    code: SUCCESS
                    message: null
                    data:
                      order_id: WT1754870400GP88120007
                      tx_id: GP-W-88120007
                      status: COMPLETE
                      order_amount: "500.00"
                      service_change: "10.00"
                      final_amount: "510.00"
                    request_id: e0b62c19-7d45-4f83-a1c6-93e5417d8a2b
        "403":
          $ref: "#/components/responses/AuthError"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/ServerError"
  /api/v2/banks:
    get:
      tags:
        - Preparation
      operationId: listBanks
      summary: Bank list
      description: |
        Returns the banks currently enabled. `bank_code` on a `channel: bank` payout order must come from this endpoint.

        The list changes. Fetch it at startup or cache it daily rather than hardcoding it.
      responses:
        "200":
          description: The banks currently enabled.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Envelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/Bank"
              examples:
                banks:
                  value:
                    status: 1
                    code: SUCCESS
                    message: null
                    data:
                      - id: 3
                        name: Standard Chartered Bank (Hong Kong)
                        bank_code: "003"
                      - id: 24
                        name: Hang Seng Bank
                        bank_code: "024"
                    request_id: f1c73d2a-8e56-4094-b2d7-a4f6528e9b3c
        "403":
          $ref: "#/components/responses/AuthError"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/ServerError"
  /api/v2/balance:
    get:
      tags:
        - Preparation
      operationId: getBalance
      summary: Account balance
      description: |
        Returns the merchant's available balance to two decimals. Calling it before creating a payout order avoids `MERCHANT_INSUFFICIENT_BALANCE` after the fact.

        The endpoint has no body, is read-only and has no side effects, which makes it a suitable first call when verifying an integration. With the credentials, timestamp and nonce from the reference vector, its signature is `c3a922528796d81abb54e97cb2abb0f98ba9c1fb0a5cf651d08aa9e7843e44d9`.
      responses:
        "200":
          description: The available balance.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Envelope"
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          balance:
                            type: number
                            description: Available balance, to two decimals.
                            examples:
                              - 88213.4
              examples:
                balance:
                  value:
                    status: 1
                    code: SUCCESS
                    message: null
                    data:
                      balance: 88213.4
                    request_id: f1c73d2a-8e56-4094-b2d7-a4f6528e9b3c
        "403":
          $ref: "#/components/responses/AuthError"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/ServerError"
webhooks:
  depositCallback:
    post:
      tags:
        - Queries and callbacks
      operationId: depositCallback
      summary: Collection result callback
      description: |
        Pushed to the order's `callback_url` when a collection order reaches a final state, and again on a manual re-push by an administrator.

        The body is bare JSON with no envelope. Verification, idempotency and the reference vector are in section 3, Callbacks.

        The only structural difference from the payout callback: this one carries `transferror_name`.
      parameters:
        - $ref: "#/components/parameters/CallbackTimestamp"
        - $ref: "#/components/parameters/CallbackNonce"
        - $ref: "#/components/parameters/CallbackSignature"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DepositCallbackBody"
            examples:
              goldenVector:
                summary: Body of the callback signature reference vector
                value:
                  order_id: DP1753776000GOLDEN01
                  tx_id: v2-golden-000001
                  transferror_name: "-"
                  status: COMPLETE
                  order_amount: "100.00"
                  service_change: "2.00"
                  final_amount: "98.00"
      responses:
        "200":
          description: |
            Return HTTP 200 to acknowledge. A non-200 response may trigger a re-push.
  withdrawCallback:
    post:
      tags:
        - Queries and callbacks
      operationId: withdrawCallback
      summary: Payout result callback
      description: |
        Pushed to the order's `callback_url` when a payout order reaches a final state.

        The body is bare JSON. The verification headers and algorithm are the same as the collection callback, keyed with the same API secret. The body carries one field fewer: no `transferror_name`.

        Idempotent handling is the merchant's responsibility.
      parameters:
        - $ref: "#/components/parameters/CallbackTimestamp"
        - $ref: "#/components/parameters/CallbackNonce"
        - $ref: "#/components/parameters/CallbackSignature"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WithdrawCallbackBody"
            examples:
              complete:
                value:
                  order_id: WT1754870400GP88120007
                  tx_id: GP-W-88120007
                  status: COMPLETE
                  order_amount: "500.00"
                  service_change: "10.00"
                  final_amount: "510.00"
      responses:
        "200":
          description: Return HTTP 200 to acknowledge.
components:
  securitySchemes:
    ApiKey:
      type: apiKey
      in: header
      name: X-Api-Key
      description: The API key (`pk_` + 24 characters). A public identifier, not a secret.
    Timestamp:
      type: apiKey
      in: header
      name: X-Timestamp
      description: Unix seconds, digits only, within ±300 seconds of server time.
    Nonce:
      type: apiKey
      in: header
      name: X-Nonce
      description: 16–64 random characters, not reused by the same merchant within 10 minutes.
    Signature:
      type: apiKey
      in: header
      name: X-Signature
      description: HMAC-SHA256 of `"v2:" + METHOD + "\n" + PATH + "\n" + TIMESTAMP + "\n" + NONCE + "\n" + sha256hex(body or "")`, keyed with the API secret, lowercase hex.
  parameters:
    TxId:
      name: tx_id
      in: path
      required: true
      description: The merchant's own reference, 6–200 characters, compared case-insensitively.
      schema:
        type: string
        minLength: 6
        maxLength: 200
      example: GP-D-88120001
    CallbackTimestamp:
      name: X-Timestamp
      in: header
      required: true
      description: Unix seconds at which the gateway signed this delivery.
      schema:
        type: string
      example: "1753776000"
    CallbackNonce:
      name: X-Nonce
      in: header
      required: true
      description: A random string unique to this delivery.
      schema:
        type: string
      example: abcdef1234567890
    CallbackSignature:
      name: X-Signature
      in: header
      required: true
      description: HMAC-SHA256 of the callback canonical string, hex.
      schema:
        type: string
      example: 4d1e566dc184e84770dae832f4f62317887b1a02b73f54dadaec4c2107b5772c
  responses:
    AuthError:
      description: |
        Authentication failed. `code` is one of `INVALID_MERCHANT`, `MERCHANT_BLOCKED`, `SIGNATURE_EXPIRED`, `INVALID_SIGNATURE`, `IP_RESTRICTION`, or `SERVICE_NOT_AVAILABLE` (the merchant has no such service enabled).
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Envelope"
          examples:
            invalidSignature:
              value:
                status: 0
                code: INVALID_SIGNATURE
                message: null
                data: []
                request_id: a2d84e3b-9f67-41a5-83e8-b5074639cd1e
    ValidationError:
      description: Validation failed; `message` carries the first validation error.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Envelope"
          examples:
            invalidParams:
              value:
                status: 0
                code: INVALID_PARAMS
                message: "channel must be one of: bank, fps"
                data: []
                request_id: a2d84e3b-9f67-41a5-83e8-b5074639cd1e
    RateLimited:
      description: "Rate limited: 300 per minute per merchant, or 20 per minute per IP when the merchant cannot be resolved."
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Envelope"
    ServerError:
      description: "Unhandled server error, masked uniformly as `code: \"FAIL\"`; no exception detail is exposed."
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Envelope"
          examples:
            fail:
              value:
                status: 0
                code: FAIL
                message: null
                data: []
                request_id: a2d84e3b-9f67-41a5-83e8-b5074639cd1e
  schemas:
    Envelope:
      type: object
      description: The structure used by every response except callbacks.
      required:
        - status
        - code
        - data
        - request_id
      properties:
        status:
          type: integer
          enum:
            - 0
            - 1
          description: 1 for success, 0 for failure.
        code:
          $ref: "#/components/schemas/ErrorCode"
        message:
          type:
            - string
            - "null"
          description: Supplementary detail; usually null on success.
        data:
          description: The result payload; an empty array or object on failure.
        request_id:
          type: string
          format: uuid
          description: Unique identifier for this request. Quote it when contacting support.
    ErrorCode:
      type: string
      description: The result code your integration branches on.
      enum:
        - SUCCESS
        - FAIL
        - INVALID_MERCHANT
        - MERCHANT_BLOCKED
        - API_V2_NOT_ENABLED
        - SIGNATURE_EXPIRED
        - INVALID_SIGNATURE
        - IP_RESTRICTION
        - INVALID_PARAMS
        - DUPLICATE_TRANSACTION
        - SERVICE_NOT_AVAILABLE
        - PAYMENT_METHOD_NOT_SUBSCRIBE
        - MERCHANT_INSUFFICIENT_BALANCE
        - TRANSACTION_NOT_FOUND
        - NO_SERVICE_PROVIDED
        - SERVICE_UNDER_MAINTENANCE
        - PAYMENT_GATEWAY_MAINTENENCE
        - TOO_MANY_REQUEST
        - PATH_NOT_FOUND
    DepositChannel:
      type: string
      description: |
        Collection channel. `bank` is a bank transfer, `fps` is Faster Payment System. Any other value returns `INVALID_PARAMS`.
      enum:
        - bank
        - fps
    WithdrawChannel:
      type: string
      description: Payout channel.
      enum:
        - bank
        - fps
    OrderStatus:
      type: string
      description: |
        Order state.

        | Value | Meaning |
        |---|---|
        | `PENDING` | Being processed. |
        | `COMPLETE` | Settled. Only this value means funds moved. |
        | `REJECT` | Rejected or failed. |
        | `OVERTIME` | Collection orders only; the customer did not pay within the validity period. |

        The only final states are `COMPLETE` and `REJECT`. Treat every other value, including any not listed here, as still processing: continue waiting for the callback and do not classify it as a failure.
      enum:
        - PENDING
        - COMPLETE
        - REJECT
        - OVERTIME
    DepositCreateRequest:
      type: object
      required:
        - channel
        - amount
        - tx_id
        - callback_url
        - redirect_url
        - transferror_name
      properties:
        channel:
          $ref: "#/components/schemas/DepositChannel"
        amount:
          type: string
          description: |
            Collection amount, up to 2 decimals, within the limits configured on the account. Send it as a string so float formatting cannot change the decimal places; a number is also accepted.
          examples:
            - "100.00"
        tx_id:
          type: string
          minLength: 6
          maxLength: 200
          description: The merchant's own reference. Unique within the account, compared case-insensitively.
        callback_url:
          type: string
          format: uri
          description: The address that receives the result callback.
        redirect_url:
          type: string
          format: uri
          description: The address the customer is sent to after finishing on the hosted payment page.
        transferror_name:
          type: string
          maxLength: 200
          description: |
            Payer name. The individual or company that will send the transfer.
    DepositCreateData:
      type: object
      description: Returned when a collection order is created.
      properties:
        order_id:
          type: string
          description: Gateway order id, prefixed `DP`.
        tx_id:
          type: string
        order_amount:
          type: string
          description: The order amount.
        service_change:
          type: string
          description: The fee, deducted from the collection amount.
        final_amount:
          type: string
          description: The amount actually received, order_amount minus service_change.
        transferror_name:
          type: string
          description: |
            The payer name recorded on the order, or `""` when none was recorded — including when one was submitted but the account lets the payer enter their own.
        payment_details:
          type: object
          description: |
            How this order is to be paid, for display to the customer.
          properties:
            bank_name:
              type: string
              description: Receiving bank name.
            account_name:
              type: string
              description: Receiving account holder. The transfer must go to exactly this name.
            account_number:
              type: string
              description: Receiving account number. Handle as a string; leading zeros are significant.
            allocated_amount:
              type:
                - string
                - "null"
              description: |
                The exact amount the customer must transfer. Fall back to `order_amount` when null.
            name:
              type: string
              description: Returned instead of `account_name` on some FPS orders.
            mobile_no:
              type: string
              description: Recipient mobile number or FPS identifier, returned on some FPS orders.
            redirect_url:
              type: string
              format: uri
              description: |
                The hosted payment page for this order. Always present.
          additionalProperties: true
    DepositInquiryData:
      type: object
      description: Collection order state; same shape as the collection callback body.
      properties:
        order_id:
          type: string
        tx_id:
          type: string
        transferror_name:
          type: string
          description: Payer name, or "-" when unset.
        status:
          $ref: "#/components/schemas/OrderStatus"
        order_amount:
          type: string
          description: |
            The amount this order actually received. If the customer transferred a different figure, this differs from the `order_amount` returned at creation.
        service_change:
          type: string
        final_amount:
          type: string
    WithdrawCreateRequest:
      type: object
      required:
        - channel
        - amount
        - tx_id
        - callback_url
      properties:
        channel:
          $ref: "#/components/schemas/WithdrawChannel"
        amount:
          type: string
          description: |
            Payout amount. Exactly 2 decimals, within the limits configured on the account. The fee is added on top.
          examples:
            - "500.00"
        tx_id:
          type: string
          minLength: 6
          maxLength: 200
          description: The merchant's own reference.
        callback_url:
          type: string
          format: uri
          description: The address that receives the result callback.
        bank_code:
          type: string
          description: |
            Required for `channel: bank`. The receiving bank, taken from `bank_code` in `GET /api/v2/banks`. Unused for `fps`.
        account_number:
          type: string
          description: "Required for `channel: bank`. Receiving account number; handle as a string, leading zeros are significant."
        account_name:
          type: string
          description: "Required for `channel: bank`. Receiving account holder, as registered with the bank."
        mobile_no:
          type: string
          description: "Required for `channel: fps`. The recipient's FPS mobile number or identifier."
        redirect_url:
          type: string
          format: uri
          description: Optional. Stored with the order; not used for payout routing.
    WithdrawCreateData:
      type: object
      description: Returned when a payout order is created.
      properties:
        order_id:
          type: string
          description: Gateway order id, prefixed `WT`.
        tx_id:
          type: string
        order_amount:
          type: string
          description: The payout amount requested.
        service_change:
          type: string
          description: The fee added on top of the payout amount.
        final_amount:
          type: string
          description: Total debited from the merchant balance, order_amount plus service_change.
    WithdrawInquiryData:
      type: object
      description: Payout order state; same shape as the payout callback body.
      properties:
        order_id:
          type: string
        tx_id:
          type: string
        status:
          $ref: "#/components/schemas/OrderStatus"
        order_amount:
          type: string
        service_change:
          type: string
        final_amount:
          type: string
    DepositCallbackBody:
      type: object
      description: The collection callback JSON body.
      required:
        - order_id
        - tx_id
        - status
        - order_amount
        - service_change
        - final_amount
      properties:
        order_id:
          type: string
        tx_id:
          type: string
        transferror_name:
          type: string
          description: Payer name, or "-" when unset.
        status:
          $ref: "#/components/schemas/OrderStatus"
        order_amount:
          type: string
        service_change:
          type: string
        final_amount:
          type: string
    WithdrawCallbackBody:
      type: object
      description: The payout callback JSON body.
      required:
        - order_id
        - tx_id
        - status
        - order_amount
        - service_change
        - final_amount
      properties:
        order_id:
          type: string
        tx_id:
          type: string
        status:
          $ref: "#/components/schemas/OrderStatus"
        order_amount:
          type: string
        service_change:
          type: string
        final_amount:
          type: string
    Bank:
      type: object
      properties:
        bank_code:
          type: string
          description: The value to pass as `bank_code` when creating a bank-channel payout order.
          examples:
            - "003"
        name:
          type: string
          description: Display name for the bank, for use in the merchant interface.
          examples:
            - Hang Seng Bank
        id:
          type: integer
          description: Internal identifier. Not used by the API; always pass `bank_code`.
