openapi: "3.0.3"
info:
  title: "MineSec (glue) Payment API"
  description: "External client-facing API for end-to-end payment operations"
  version: "183-20261028-9a02ec5"
servers:
  - url: "https://glue-stage.mspayhub.com"
security:
  - OpenApiBearerAuth: []
tags:
  - name: "Transactions"
    description: "Transaction operations"
  - name: "Tokenization"
    description: "Card tokenization (same permission as creating transactions)"
  - name: "Batches"
    description: "Batch settlement operations"
  - name: "Profiles"
    description: "Profile operations"
paths:

  /api/v1/pos/profiles/providerReference/{providerReference}:
    get:
      tags:
        - "Profiles"
      summary: "Get profile by provider reference"
      parameters:
        - $ref: "#/components/parameters/XMineSecRequestId"
        - name: "providerReference"
          in: "path"
          required: true
          schema:
            type: "string"
      responses:
        "200":
          description: "Profile retrieved successfully"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProfileWithAcceptances"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: "Profile not found"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /api/v1/pos/profiles/{profileId}:
    get:
      tags:
        - "Profiles"
      summary: "Get profile by ID"
      parameters:
        - $ref: "#/components/parameters/XMineSecRequestId"
        - name: "profileId"
          in: "path"
          required: true
          schema:
            type: "string"
      responses:
        "200":
          description: "Profile retrieved successfully"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProfileWithAcceptances"
        "404":
          description: "Profile not found"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /api/v1/pos/transactions:
    post:
      tags:
        - "Transactions"
      summary: "Create a new transaction"
      parameters:
        - $ref: "#/components/parameters/XMineSecRequestId"
        - $ref: "#/components/parameters/XIdempotencyKey"
      requestBody:
        description: |
          Request body to create a new transaction.

          **poiRequest fields:**
          - `tranType` (required): Transaction type - SALE, AUTH, REFUND, AUTH_COMP, DEFERRED_AUTH, INSTALL, CASH_IN, CASH_OUT
          - `amount` (required): Transaction amount with value and currency
          - `profileId` (required): Profile identifier
          - `cvmSignatureMode` (required): ELECTRONIC_SIGNATURE or SIGN_ON_PAPER
          - `acptId` (optional): Specific acceptance ID to use
          - `primaryTid` (optional): Specific TID to select
          - `description` (optional): Transaction description
          - `posReference` (optional): POS reference identifier
          - `preferredAcceptanceTag` (optional): Tag to select acceptance
          - `forcePaymentMethod` (optional): List of allowed payment methods
          - `forceFetchProfile` (optional): Force fetch profile from server (default: false)
          - `tapToOwnDevice` (optional): Tap to Own Device scenario (default: false)
          - `extOrderId` (optional): External order ID supplied by the integrator
          - `installmentPlan` (optional, legacy): Number of installments. Superseded by `installments`; do not send both
          - `installments` (optional): Installment capability of a SALE. Send it with `tranType: SALE` to pay the sale in installments (IPP / EMI); it is not a separate transaction type
            - `planId` (required): Installment plan ID configured in MMS
            - `installmentCount` (required): Number of installments requested
            - `revolving.initialPaymentAmount` (optional): Upfront amount, only for a revolving installment
          - `linkedTranId` (optional): Linked transaction ID
          - `extra` (optional): Additional parameters map

          **cardRead fields:**
          - `paymentMethod` (required): VISA, MASTERCARD, AMEX, etc.
          - `entryMode` (required): NFC, SWIPE, CHIP, MANUAL_KEYED, etc.
          - `maskedPan` (required): Masked card number
          - `needOnline` (optional): Requires online authorization (default: true)
          - `cardKsn` (optional): Card Key Serial Number
          - `cvmPerformed` (optional): NO_CVM, SIGNATURE, PIN, OFFLINE_PIN, CDCVM
          - `pinKsn` (optional): PIN Key Serial Number
          - `panToken` (optional): PAN token
          - `reader` (optional): Reader device information
          - `appleTTP` (optional): Apple Tap to Pay data
          - `emvData` (optional): EMV tag data map

          **Tokenization (optional):**
          - `tokenizationAction` (optional): Tokenize the card after the payment is approved. Omit (or send `null`) for a payment-only transaction.
            - `command` (required): `TOKENIZE_CARD`
            - `customerId` (optional): Tokenization customer to link the instrument to
            - `metadata` (optional): String key/value metadata stored with the instrument
          - When `tokenizationAction` is present, the `x-idempotency-key` header is required (`400` if missing or blank).
          - The response then carries `tokenizationActionResponse`. Tokenization never changes the payment result:
            a non-approved payment returns `status = SKIPPED` (`reason = NOT_ELIGIBLE`), and a tokenization error on an
            approved payment returns `status = FAILED` with the error code in `reason`.
          - No extra permission is needed: the same scope as creating a transaction (`pos:transactions` or `pos:transactions:write`).

          **Payment with a saved token (optional):**
          - Send `paymentToken` instead of `cardRead` to charge a card tokenized earlier (see `TokenTransactionRequest`).
            - `tokenValue` (required): the `paymentToken.tokenValue` returned when the card was tokenized
          - `paymentToken` cannot be combined with `cardRead` or `tokenizationAction` (`400`).
          - There is no card data to decrypt; Glue forwards the request to Enabler unchanged. Enabler resolves the token
            within the merchant of `poiRequest.profileId` and charges it as a card-not-present (`MANUAL_KEYED`) payment.
          - The token works only for the merchant it was created for, and only while Tokenization is enabled for that
            merchant in the Merchant Portal.
          - Same scope as creating a transaction (`pos:transactions` or `pos:transactions:write`); `x-idempotency-key` is not required.
          - Requests that carry `cardRead` are not affected.
        content:
          application/json:
            schema:
              oneOf:
                - $ref: "#/components/schemas/TransactionRequest"
                - $ref: "#/components/schemas/TokenTransactionRequest"
            examples:
              sale:
                summary: "Sale (NFC)"
                description: |
                  Sale transaction via NFC tap.
                  Required EMV tags for NFC: 57, 82, 95, 9A, 9C, 9F02, 9F10, 9F1A, 9F26, 9F27, 9F33, 9F36, 9F37

                  Reader fields:
                  - SoftPOS app: model, brand, sdkId, sdkVersion
                  - Physical POS: model, brand, hardwareSN, firmwareVersion, emvKernelVersion
                value:
                  poiRequest:
                    tranType: "SALE"
                    amount:
                      value: "1.00"
                      currency: "SGD"
                    posReference: "string"
                    profileId: "string"
                    acptId: "string"
                    description: "string"
                    preferredAcceptanceTag: "string"
                    cvmSignatureMode: "SIGN_ON_PAPER"
                    forceFetchProfile: true
                  cardRead:
                    paymentMethod: "VISA"
                    entryMode: "NFC"
                    maskedPan: "476173******0027"
                    needOnline: true
                    cardKsn: "string"
                    cvmPerformed: "NO_CVM"
                    panToken: "string"
                    reader:
                      model: "MineHades"
                      brand: "MineSec"
                      sdkId: "string"
                      sdkVersion: "string"
                      hardwareSN: "string"
                      firmwareVersion: "string"
                      emvKernelVersion: "string"
                    emvData:
                      "57": "string"
                      "82": "string"
                      "95": "string"
                      "9A": "string"
                      "9C": "string"
                      "9F02": "string"
                      "9F10": "string"
                      "9F1A": "string"
                      "9F26": "string"
                      "9F27": "string"
                      "9F33": "string"
                      "9F36": "string"
                      "9F37": "string"
                  sdkId: "string"
              saleWithIpp:
                summary: "Sale with installment (IPP)"
                description: |
                  Sale paid by Installment Payment Plan (IPP). Installment is a capability of SALE:
                  send a normal `SALE` with `poiRequest.installments`; `tranType` stays `SALE`.
                  - `planId`: the IPP plan configured in MMS for this profile
                  - `installmentCount`: number of monthly installments, must be offered by the plan
                  
                  Plan attributes (interest model, installment type, plan name) are resolved from MMS, not from the request,
                  and are returned in `Transaction.installments`. Do not send the legacy `installmentPlan` together with `installments`.
                value:
                  poiRequest:
                    tranType: "SALE"
                    amount:
                      value: "1200.00"
                      currency: "MYR"
                    posReference: "ipp-sale-0001"
                    profileId: "string"
                    description: "string"
                    cvmSignatureMode: "SIGN_ON_PAPER"
                    installments:
                      planId: "IPP_0PCT_12M"
                      installmentCount: 12
                  cardRead:
                    paymentMethod: "VISA"
                    entryMode: "NFC"
                    maskedPan: "476173******0027"
                    needOnline: true
                    cardKsn: "string"
                    cvmPerformed: "NO_CVM"
                    panToken: "string"
                    reader:
                      model: "MineHades"
                      brand: "MineSec"
                      sdkId: "string"
                      sdkVersion: "string"
                    emvData:
                      "57": "string"
                      "82": "string"
                      "95": "string"
                      "9A": "string"
                      "9C": "string"
                      "9F02": "string"
                      "9F10": "string"
                      "9F1A": "string"
                      "9F26": "string"
                      "9F27": "string"
                      "9F33": "string"
                      "9F36": "string"
                      "9F37": "string"
                  sdkId: "string"
              saleWithEmi:
                summary: "Sale with installment (EMI)"
                description: |
                  Sale paid by Equated Monthly Installments (EMI). Installment is a capability of SALE:
                  send a normal `SALE` with `poiRequest.installments`. Same request shape as IPP; only the MMS plan referenced by `planId` differs (e.g. an interest-bearing EMI plan).
                  - `planId`: the EMI plan configured in MMS for this profile
                  - `installmentCount`: number of equated monthly installments, must be offered by the plan
                  
                  For a revolving installment, see the `saleWithRevolving` example.
                  Do not send the legacy `installmentPlan` together with `installments`.
                value:
                  poiRequest:
                    tranType: "SALE"
                    amount:
                      value: "60000.00"
                      currency: "PKR"
                    posReference: "emi-sale-0001"
                    profileId: "string"
                    description: "string"
                    cvmSignatureMode: "SIGN_ON_PAPER"
                    installments:
                      planId: "EMI_6M"
                      installmentCount: 6
                  cardRead:
                    paymentMethod: "VISA"
                    entryMode: "NFC"
                    maskedPan: "476173******0027"
                    needOnline: true
                    cardKsn: "string"
                    cvmPerformed: "NO_CVM"
                    panToken: "string"
                    reader:
                      model: "MineHades"
                      brand: "MineSec"
                      sdkId: "string"
                      sdkVersion: "string"
                    emvData:
                      "57": "string"
                      "82": "string"
                      "95": "string"
                      "9A": "string"
                      "9C": "string"
                      "9F02": "string"
                      "9F10": "string"
                      "9F1A": "string"
                      "9F26": "string"
                      "9F27": "string"
                      "9F33": "string"
                      "9F36": "string"
                      "9F37": "string"
                  sdkId: "string"
              saleWithRevolving:
                summary: "Sale with installment (Revolving)"
                description: |
                  Sale paid by a Revolving installment. Installment is a capability of SALE:
                  send a normal `SALE` with `poiRequest.installments` plus `installments.revolving`.
                  - `planId`: a Revolving plan configured in MMS for this profile
                  - `installmentCount`: number of installments, must be offered by the plan
                  - `revolving.initialPaymentAmount`: the amount paid upfront, in the same currency as `amount`
                    and lower than it

                  The remaining amount (`amount` - `initialPaymentAmount`) is derived by the Gateway and returned in
                  `Transaction.installments.revolving.remainingAmount`, with `installmentType: REVOLVING`.
                  Only send `revolving` with a Revolving plan. Do not send the legacy `installmentPlan` together with `installments`.
                value:
                  poiRequest:
                    tranType: "SALE"
                    amount:
                      value: "3000.00"
                      currency: "MYR"
                    posReference: "revolving-sale-0001"
                    profileId: "string"
                    description: "string"
                    cvmSignatureMode: "SIGN_ON_PAPER"
                    installments:
                      planId: "REVOLVING_6M"
                      installmentCount: 6
                      revolving:
                        initialPaymentAmount:
                          value: "500.00"
                          currency: "MYR"
                  cardRead:
                    paymentMethod: "VISA"
                    entryMode: "NFC"
                    maskedPan: "476173******0027"
                    needOnline: true
                    cardKsn: "string"
                    cvmPerformed: "NO_CVM"
                    panToken: "string"
                    reader:
                      model: "MineHades"
                      brand: "MineSec"
                      sdkId: "string"
                      sdkVersion: "string"
                    emvData:
                      "57": "string"
                      "82": "string"
                      "95": "string"
                      "9A": "string"
                      "9C": "string"
                      "9F02": "string"
                      "9F10": "string"
                      "9F1A": "string"
                      "9F26": "string"
                      "9F27": "string"
                      "9F33": "string"
                      "9F36": "string"
                      "9F37": "string"
                  sdkId: "string"
              refund:
                summary: "Refund (NFC)"
                description: |
                  Refund transaction via NFC tap (card present).
                  Required EMV tags for NFC: 57, 82, 95, 9A, 9C, 9F02, 9F10, 9F1A, 9F26, 9F27, 9F33, 9F36, 9F37

                  Reader fields:
                  - SoftPOS app: model, brand, sdkId, sdkVersion
                  - Physical POS: model, brand, hardwareSN, firmwareVersion, emvKernelVersion
                value:
                  poiRequest:
                    tranType: "REFUND"
                    amount:
                      value: "1.00"
                      currency: "SGD"
                    posReference: "string"
                    profileId: "string"
                    cvmSignatureMode: "SIGN_ON_PAPER"
                  cardRead:
                    paymentMethod: "VISA"
                    entryMode: "NFC"
                    maskedPan: "476173******0027"
                    needOnline: true
                    cardKsn: "string"
                    cvmPerformed: "NO_CVM"
                    panToken: "string"
                    reader:
                      model: "MineHades"
                      brand: "MineSec"
                      sdkId: "string"
                      sdkVersion: "string"
                      hardwareSN: "string"
                      firmwareVersion: "string"
                      emvKernelVersion: "string"
                    emvData:
                      "57": "string"
                      "82": "string"
                      "95": "string"
                      "9A": "string"
                      "9C": "string"
                      "9F02": "string"
                      "9F10": "string"
                      "9F1A": "string"
                      "9F26": "string"
                      "9F27": "string"
                      "9F33": "string"
                      "9F36": "string"
                      "9F37": "string"
                  sdkId: "string"
              appleTapToPay:
                summary: "Sale - Apple Tap to Pay"
                description: "Sale transaction using Apple Tap to Pay (iOS SoftPOS)"
                value:
                  poiRequest:
                    tranType: "SALE"
                    amount:
                      value: "1.00"
                      currency: "SGD"
                    posReference: "string"
                    profileId: "string"
                    cvmSignatureMode: "SIGN_ON_PAPER"
                    tapToOwnDevice: true
                  cardRead:
                    paymentMethod: "VISA"
                    entryMode: "NFC"
                    maskedPan: "476173******0027"
                    needOnline: true
                    cvmPerformed: "NO_CVM"
                    reader:
                      model: "Tap2Pay"
                      brand: "MineSec"
                      sdkId: "string"
                      sdkVersion: "string"
                    appleTTP:
                      readerToken: "string"
                      readerId: "string"
                      iosTrxId: "string"
                      protectedPaymentData: "string"
                  sdkId: "string"
              saleWithTokenization:
                summary: "Sale with tokenization"
                description: |
                  Sale transaction that also tokenizes the card once the payment is approved.
                  Requires the `x-idempotency-key` header; reuse the same key when retrying.
                value:
                  poiRequest:
                    tranType: "SALE"
                    amount:
                      value: "1.00"
                      currency: "SGD"
                    posReference: "string"
                    profileId: "string"
                    cvmSignatureMode: "SIGN_ON_PAPER"
                  cardRead:
                    paymentMethod: "VISA"
                    entryMode: "NFC"
                    maskedPan: "476173******0027"
                    needOnline: true
                    cardKsn: "string"
                    cvmPerformed: "NO_CVM"
                    reader:
                      model: "MineHades"
                      brand: "MineSec"
                      sdkId: "string"
                      sdkVersion: "string"
                    emvData:
                      "57": "string"
                      "82": "string"
                      "95": "string"
                      "9A": "string"
                      "9C": "string"
                      "9F02": "string"
                      "9F10": "string"
                      "9F1A": "string"
                      "9F26": "string"
                      "9F27": "string"
                      "9F33": "string"
                      "9F36": "string"
                      "9F37": "string"
                  sdkId: "string"
                  tokenizationAction:
                    command: "TOKENIZE_CARD"
                    customerId: "string"
                    metadata:
                      channel: "sdk"
              saleWithPaymentToken:
                summary: "Sale with a saved payment token"
                description: |
                  Charges a card tokenized earlier, without reading the card. `paymentToken` replaces `cardRead`.
                value:
                  poiRequest:
                    tranType: "SALE"
                    amount:
                      value: "1.00"
                      currency: "USD"
                    posReference: "string"
                    profileId: "string"
                  paymentToken:
                    tokenValue: "pst_string"
                  sdkId: "string"
        required: true
      responses:
        "201":
          description: |
            Transaction performed successfully.
            When the request carried `tokenizationAction`, the body also includes `tokenizationActionResponse`.
            A payment with `paymentToken` returns the same transaction, with `entryMode = MANUAL_KEYED`.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Transaction"
        "400":
          description: |
            Bad request due to invalid transaction input.
            `TOKENIZATION_PAYMENT_TOKEN_INVALID`: `paymentToken` is blank or combined with `cardRead` / `tokenizationAction`.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: |
            Insufficient scope in JWT token.
            Payment with a saved token: `TOKENIZATION_SCOPE_NOT_FOUND` / `TOKENIZATION_SCOPE_DISABLED` when Tokenization
            is not set up or not enabled for the merchant, or the profile is disabled. Nothing is charged.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: |
            Payment with a saved token: `TOKENIZATION_PAYMENT_TOKEN_NOT_FOUND` when the token is unknown or belongs to
            another merchant. Nothing is charged.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "504":
          description: "Gateway Timeout"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /api/v1/pos/transactions/mpqr:
    post:
      tags:
        - "Transactions"
      summary: "Request Merchant Presented QR (MPQR) transaction"
      description: |
        Request a merchant-presented QR code transaction.

        Most MPQR flows are asynchronous. Once the QR code is requested from the upstream gateway,
        the process may involve polling or waiting for an upstream status update.

        **poiRequest fields:**
        - `tranType` (required): Transaction type - typically SALE
        - `amount` (required): Transaction amount with value and currency
        - `profileId` (required): Profile identifier
        - `cvmSignatureMode` (required): ELECTRONIC_SIGNATURE or SIGN_ON_PAPER
        - `posReference` (optional): POS reference identifier
        - `description` (optional): Transaction description
        - `extra` (optional): Additional parameters map

        **Top-level fields:**
        - `paymentMethod` (required): Payment method e.g. ALIPAY, WECHAT_PAY
        - `sdkId` (optional): SDK identifier
        - `callbackUrl` (optional): URL for async result notification
      parameters:
        - $ref: "#/components/parameters/XMineSecRequestId"
      requestBody:
        description: "Details for requesting the MPQR transaction."
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TransactionRequestMpQr"
            examples:
              mpqr:
                summary: "MPQR Sale"
                value:
                  poiRequest:
                    tranType: "SALE"
                    amount:
                      value: "1.00"
                      currency: "SGD"
                    profileId: "prof_0123"
                    acptId: null
                    primaryTid: null
                    description: null
                    posReference: "your-pos-reference"
                    preferredAcceptanceTag: null
                    cvmSignatureMode: "SIGN_ON_PAPER"
                    forcePaymentMethod: null
                    forceFetchProfile: false
                    tapToOwnDevice: false
                    installmentPlan: null
                    linkedTranId: null
                    extra:
                      extra1: "value1"
                      extra2: "value2"
                  paymentMethod: "ALIPAY"
                  sdkId: "sdk_0123"
                  callbackUrl: "https://your-call-back-url.com"
        required: true
      responses:
        "201":
          description: "MPQR transaction requested successfully"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Transaction"
        "400":
          description: "Bad request due to invalid MPQR transaction input"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "504":
          description: "Gateway Timeout"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /api/v1/pos/transactions/{tranId}:
    get:
      tags:
        - "Transactions"
      summary: "Get transaction by ID"
      parameters:
        - $ref: "#/components/parameters/XMineSecRequestId"
        - name: "tranId"
          in: "path"
          required: true
          schema:
            type: "string"
      responses:
        "200":
          description: "Transaction retrieved successfully"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Transaction"
        "404":
          description: "Transaction not found"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /api/v1/pos/transactions/{tranId}/history-tree:
    get:
      tags:
        - "Transactions"
      summary: "Get transaction history tree"
      description: |
        Returns the complete relationship hierarchy (original transaction and its related child
        transactions) that the given transaction belongs to. `tranId` may be the original transaction
        or any child/descendant; the tree always starts from the root transaction.
        The hierarchy, ordering and `treeComplete` flag are returned as-is from the backend.
      parameters:
        - $ref: "#/components/parameters/XMineSecRequestId"
        - name: "tranId"
          in: "path"
          required: true
          schema:
            type: "string"
      responses:
        "200":
          description: "Transaction history tree retrieved successfully"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HistoryTree"
              example:
                selectedTransactionId: "tran_01KF12TA15CPFDW2NKKQDQRJ5F"
                treeComplete: true
                root:
                  tranId: "tran_01KF12TA15CPFDW2NKKQDQRJ5E"
                  linkedTranId: null
                  tranType: "SALE"
                  tranStatus: "APPROVED"
                  amount:
                    value: "100.00"
                    currency: "HKD"
                  createdAt: "2026-01-15T08:30:00Z"
                  updatedAt: "2026-01-15T09:00:00Z"
                  children:
                    - tranId: "tran_01KF12TA15CPFDW2NKKQDQRJ5F"
                      linkedTranId: "tran_01KF12TA15CPFDW2NKKQDQRJ5E"
                      tranType: "REFUND"
                      tranStatus: "APPROVED"
                      amount:
                        value: "30.00"
                        currency: "HKD"
                      createdAt: "2026-01-15T09:00:00Z"
                      updatedAt: null
                      children: []
        "404":
          description: "Transaction not found"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /api/v1/pos/transactions/{tranId}/actions:
    post:
      tags:
        - "Transactions"
      summary: "Perform action on transaction (capture, void, etc.)"
      parameters:
        - $ref: "#/components/parameters/XMineSecRequestId"
        - name: "tranId"
          in: "path"
          required: true
          schema:
            type: "string"
      requestBody:
        description: |
          Request body to perform an action on a transaction.
          - **VOID**: Void the entire transaction. Only `actionType` is required.
          - **ADJUST**: Adjust the transaction amount. Requires `actionType` and `amount`.
          - **AUTH_COMP**: Complete/capture an authorized transaction. Requires `actionType` and `amount`.
          - **INCREMENTAL_AUTH**: Raise the amount an open AUTH preauthorization is authorized for. Requires `actionType` and `amount`.
          - **LINK_REFUND**: Refund without card present. Requires `actionType`, optionally `amount` and `posReference`.

          Preauthorization family: `AUTH` opens a preauthorization, `INCREMENTAL_AUTH` raises the amount it is
          authorized for, and `AUTH_COMP` completes/captures it.
          - For INCREMENTAL_AUTH, `amount` is **mandatory** and means the **new total authorized amount**, not
            the increment delta. It must use the currency the preauthorization was authorized in and be
            strictly greater than the amount currently authorized.
          - Allowed only while the preauthorization is still open; it is rejected once the preauthorization has
            been completed, voided, reversed, declined or (where a validity window is configured) expired.
          - No new transaction is created: the increment is recorded as an action on the original AUTH and the
            preauthorization stays AUTH_ONLY. It can be repeated where the platform configuration allows, and
            each increment resolves from the latest successful authorized state.
          - AUTH_COMP and VOID afterwards resolve against the latest successful INCREMENTAL_AUTH, not the amount
            the preauthorization was opened with. An `originalActionId` pointing at the superseded original AUTH
            is rejected — either omit it or reference the latest successful INCREMENTAL_AUTH action.

          Optional fields:
          - `amount`: Applicable to AUTH_COMP, INCREMENTAL_AUTH, LINK_REFUND, ADJUST. Discarded for VOID.
          - `originalActionId`: ID of the original action if modifying a previous action.
          - `posReference`: POS reference for LINK_REFUND (new transaction will be created).
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ActionRequest"
            examples:
              void:
                summary: "Void transaction"
                description: "Void the entire transaction amount"
                value:
                  actionType: "VOID"
              adjust:
                summary: "Adjust transaction"
                description: "Adjust the transaction to a new total amount"
                value:
                  actionType: "ADJUST"
                  amount:
                    value: "1.50"
                    currency: "SGD"
              authComp:
                summary: "Authorization completion"
                description: "Capture/complete a previously authorized transaction"
                value:
                  actionType: "AUTH_COMP"
                  amount:
                    value: "1.00"
                    currency: "SGD"
              incrementalAuth:
                summary: "Incremental authorization"
                description: "Raise an open preauthorization to a new total authorized amount (not the delta)"
                value:
                  actionType: "INCREMENTAL_AUTH"
                  amount:
                    value: "1.50"
                    currency: "SGD"
              linkRefund:
                summary: "Linked refund"
                description: "Refund without card present (cardholder-not-present)"
                value:
                  actionType: "LINK_REFUND"
                  amount:
                    value: "0.50"
                    currency: "SGD"
                  posReference: "refund-ref-001"
        required: true
      responses:
        "200":
          description: "Transaction modified successfully after performing the action"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Transaction"
        "400":
          description: "Bad request due to invalid action input or unsupported action type"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: "Transaction not found"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "504":
          description: "Gateway Timeout"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /api/v1/pos/transactions/{tranId}/signature:
    put:
      tags:
        - "Transactions"
      summary: "Upload signature for transaction"
      description: "Upload a base64-encoded PNG image of the customer signature"
      parameters:
        - $ref: "#/components/parameters/XMineSecRequestId"
        - name: "tranId"
          in: "path"
          required: true
          schema:
            type: "string"
      requestBody:
        description: "Base64-encoded PNG image of the signature"
        content:
          text/plain:
            schema:
              type: "string"
            example: "string"
        required: true
      responses:
        "200":
          description: "Signature uploaded successfully"
        "400":
          description: "Bad request if the provided signature is not a valid base64-encoded PNG"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /api/v1/pos/transactions/posReference/{posReference}:
    get:
      tags:
        - "Transactions"
      summary: "Get transaction by POS reference"
      parameters:
        - $ref: "#/components/parameters/XMineSecRequestId"
        - name: "posReference"
          in: "path"
          required: true
          schema:
            type: "string"
      responses:
        "200":
          description: "Transaction retrieved successfully"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Transaction"
        "404":
          description: "Transaction not found for the given POS reference"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /api/v1/pos/transactions/requestId/{requestId}:
    get:
      tags:
        - "Transactions"
      summary: "Get transaction by request ID"
      parameters:
        - $ref: "#/components/parameters/XMineSecRequestId"
        - name: "requestId"
          in: "path"
          required: true
          schema:
            type: "string"
      responses:
        "200":
          description: "Transaction retrieved successfully"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Transaction"
        "404":
          description: "Transaction not found for the given request ID"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /api/v1/pos/transactions/reversal/{originalRequestId}:
    post:
      tags:
        - "Transactions"
      summary: "Reverse transaction by request ID"
      parameters:
        - $ref: "#/components/parameters/XMineSecRequestId"
        - name: "originalRequestId"
          in: "path"
          required: true
          schema:
            type: "string"
      responses:
        "200":
          description: "Reversal performed successfully"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Transaction"
        "400":
          description: "Bad request due to invalid request ID or upstream failure"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /api/v1/pos/transactions/reversal/posReference/{originalPosReference}:
    post:
      tags:
        - "Transactions"
      summary: "Reverse transaction by POS reference"
      parameters:
        - $ref: "#/components/parameters/XMineSecRequestId"
        - name: "originalPosReference"
          in: "path"
          required: true
          schema:
            type: "string"
      responses:
        "200":
          description: "Reversal performed successfully"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Transaction"
        "400":
          description: "Bad request due to invalid POS reference or upstream failure"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /api/v1/pos/instruments:
    post:
      tags:
        - "Tokenization"
      summary: "Tokenize a card (standalone, no payment)"
      description: |
        Tokenize a card without performing a payment.

        **Permission:** same as creating a transaction: `pos:transactions` or `pos:transactions:write` (or `pos:*`).
        No separate tokenization permission is required.

        **Request fields:**
        - `cardRead` (required): Encrypted card read, same shape as a transaction `cardRead`.
          Track 2 (tag `57`), or PAN (`5A`) with expiry (`5F24`), must be present in `emvData`.
          Apple Tap to Pay card reads are not supported (`400`).
        - `profileId` (recommended): Terminal profile; its merchant decides the tokenization scope.
          Defaults to the `profileId` in the JWT when omitted.
        - `customerId` (optional): Tokenization customer to link the instrument to
        - `metadata` (optional): String key/value metadata stored with the instrument
        - `sdkId` (optional): SDK identifier

        **Idempotency:** `x-idempotency-key` is required. Generate one key per logical tokenization attempt and reuse it
        on retries (including after a `504`). The same key with the same card replays the original result
        (`idempotencyReplayed = true`); the same key with a different request returns `409`.

        **Error codes** (in `ErrorResponse.title`):

        | Code | HTTP | Terminal behavior |
        |---|---|---|
        | `TOKENIZATION_IDEMPOTENCY_KEY_REQUIRED` | 400 | Fix the request |
        | `TOKENIZATION_CARD_DATA_UNAVAILABLE` | 400 | Do not retry with the same card read |
        | `TOKENIZATION_VALIDATION_FAILED` | 400 | Do not retry; fix input |
        | `TOKENIZATION_SCOPE_NOT_FOUND` / `TOKENIZATION_SCOPE_DISABLED` | 403 | Do not retry; enable Tokenization for the merchant in the Merchant Portal |
        | `TOKENIZATION_IDEMPOTENCY_CONFLICT` | 409 | Start a new attempt with a new key |
        | `TOKENIZATION_DUPLICATE_CREDENTIAL` | 409 | Do not retry |
        | `TOKENIZATION_AUTH_FAILED` | 502 | Do not retry; platform configuration issue |
        | `TOKENIZATION_UNAVAILABLE` | 503 | Retry later with the same key |
        | `TOKENIZATION_TIMEOUT` | 504 | Retry with the same key |
      parameters:
        - $ref: "#/components/parameters/XMineSecRequestId"
        - name: "x-idempotency-key"
          in: "header"
          required: true
          description: "Idempotency key for this tokenization attempt. Reuse the same key when retrying."
          schema:
            type: "string"
            example: "idem_01KF12TA15CPFDW2NKKQDQRJ5E"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/InstrumentTokenizationRequest"
            examples:
              nfc:
                summary: "Tokenize card (NFC)"
                value:
                  cardRead:
                    paymentMethod: "VISA"
                    entryMode: "NFC"
                    maskedPan: "476173******0027"
                    cardKsn: "string"
                    reader:
                      model: "MineHades"
                      brand: "MineSec"
                      sdkId: "string"
                      sdkVersion: "string"
                    emvData:
                      "57": "string"
                      "5A": "string"
                      "5F24": "string"
                  profileId: "string"
                  customerId: "string"
                  metadata:
                    channel: "sdk"
                  sdkId: "string"
      responses:
        "201":
          description: "Card tokenized; a new instrument was created (`outcome = CREATED`)"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/InstrumentTokenizationResponse"
              example:
                outcome: "CREATED"
                idempotencyReplayed: false
                instrument:
                  instrumentId: "ins_01KF12TA15CPFDW2NKKQDQRJ5E"
                  instrumentType: "CARD"
                  status: "ACTIVE"
                  cardBrand: "VISA"
                  maskedPan: "476173******0027"
                  lastFour: "0027"
                  expiryMonth: 12
                  expiryYear: 2031
                paymentToken:
                  tokenId: "tok_01KF12TA15CPFDW2NKKQDQRJ5E"
                  tokenKind: "PAYMENT"
                  status: "ACTIVE"
                  paymentCapable: true
                  provider: "PAYSURFER"
                analyticsToken:
                  analyticsTokenId: "atk_01KF12TA15CPFDW2NKKQDQRJ5E"
                  status: "ACTIVE"
                  paymentCapable: false
        "200":
          description: "Card was already tokenized for this merchant (`outcome = EXISTING`)"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/InstrumentTokenizationResponse"
        "400":
          description: "Invalid request, missing `x-idempotency-key`, or card data not usable for tokenization"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                status: 400
                path: "/api/v1/pos/instruments"
                requestId: "req_01KF12TA15CPFDW2NKKQDQRJ5E"
                title: "TOKENIZATION_CARD_DATA_UNAVAILABLE"
                message: "Card data is not available for tokenization"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          description: "Insufficient scope, or tokenization is not enabled for the merchant (`TOKENIZATION_SCOPE_NOT_FOUND` / `TOKENIZATION_SCOPE_DISABLED`)"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: "Profile not found"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: "`TOKENIZATION_IDEMPOTENCY_CONFLICT` or `TOKENIZATION_DUPLICATE_CREDENTIAL`"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "502":
          description: "`TOKENIZATION_AUTH_FAILED`"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "503":
          description: "`TOKENIZATION_UNAVAILABLE`; retry later with the same `x-idempotency-key`"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "504":
          description: "`TOKENIZATION_TIMEOUT`, or Gateway Timeout when the payment backend cannot be reached; retry with the same `x-idempotency-key`"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /api/v1/pos/batches/{batchId}:
    get:
      tags:
        - "Batches"
      summary: "Get batch by ID"
      parameters:
        - $ref: "#/components/parameters/XMineSecRequestId"
        - name: "batchId"
          in: "path"
          required: true
          schema:
            type: "string"
      responses:
        "200":
          description: "Batch retrieved successfully"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Batch"
        "404":
          description: "Batch not found"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /api/v1/pos/batches/{batchId}/settle:
    post:
      tags:
        - "Batches"
      summary: "Settle batch"
      parameters:
        - $ref: "#/components/parameters/XMineSecRequestId"
        - name: "batchId"
          in: "path"
          required: true
          schema:
            type: "string"
      responses:
        "200":
          description: "Batch settlement performed successfully"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Batch"
        "400":
          description: "Bad request due to invalid batch ID"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "504":
          description: "Gateway Timeout"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

components:
  securitySchemes:
    OpenApiBearerAuth:
      type: "http"
      scheme: "bearer"
      bearerFormat: "JWT"
      description: "Bearer token authentication for POS API access"
  responses:
    Unauthorized:
      description: "Missing or invalid JWT token"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            status: 401
            path: "/api/v1/pos/transactions"
            requestId: "req_01KF12TA15CPFDW2NKKQDQRJ5E"
            title: "Unauthorized"
            message: "Token is not valid or has expired"
    Forbidden:
      description: "Insufficient scope in JWT token"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            status: 403
            path: "/api/v1/pos/transactions"
            requestId: "req_01KF12TA15CPFDW2NKKQDQRJ5E"
            title: "AuthorizationException"
            message: "Insufficient scope for this operation"
  parameters:
    XMineSecRequestId:
      name: "x-minesec-request-id"
      in: "header"
      required: false
      description: "Optional request ID for tracing. Accepts ULID or req_ULID format."
      schema:
        type: "string"
        example: "req_01KF12TA15CPFDW2NKKQDQRJ5E"
    XIdempotencyKey:
      name: "x-idempotency-key"
      in: "header"
      required: false
      description: "Required when the request carries `tokenizationAction`; ignored otherwise. Reuse the same key when retrying the same tokenization attempt."
      schema:
        type: "string"
        example: "idem_01KF12TA15CPFDW2NKKQDQRJ5E"
  schemas:
    # ==================== Common Schemas ====================
    ErrorResponse:
      type: "object"
      properties:
        status:
          type: "integer"
          format: "int32"
        path:
          type: "string"
        requestId:
          type: "string"
        title:
          type: "string"
        message:
          type: "string"
      required:
        - "path"
        - "requestId"
        - "title"
        - "message"
        - "status"
    Amount:
      type: "object"
      properties:
        value:
          type: "string"
          description: "Monetary value as string"
        currency:
          type: "string"
          description: "ISO alpha-3 currency code"
      required:
        - "value"
        - "currency"

    HistoryTree:
      type: "object"
      properties:
        selectedTransactionId:
          type: "string"
          description: "The transaction ID supplied in the request"
        treeComplete:
          type: "boolean"
          description: "false when the backend cannot guarantee the full historical hierarchy"
        root:
          $ref: "#/components/schemas/HistoryTreeNode"
      required:
        - "selectedTransactionId"
        - "treeComplete"
        - "root"
    HistoryTreeNode:
      type: "object"
      properties:
        tranId:
          type: "string"
        linkedTranId:
          type: "string"
          nullable: true
          description: "Parent transaction ID; null for the root transaction"
        tranType:
          $ref: "#/components/schemas/TranType"
        tranStatus:
          $ref: "#/components/schemas/TranStatus"
        amount:
          $ref: "#/components/schemas/Amount"
        createdAt:
          type: "string"
          format: "date-time"
        updatedAt:
          type: "string"
          format: "date-time"
          nullable: true
        children:
          type: "array"
          description: "Child transactions, in backend order"
          items:
            $ref: "#/components/schemas/HistoryTreeNode"
      required:
        - "tranId"
        - "tranType"
        - "tranStatus"
        - "amount"
        - "createdAt"
        - "children"

    # ==================== Enums ====================
    TranType:
      type: "string"
      enum:
        - "SALE"
        - "REFUND"
        - "AUTH"
        - "AUTH_COMP"
        - "DEFERRED_AUTH"
        - "INSTALL"
        - "CASH_IN"
        - "CASH_OUT"
    TranStatus:
      type: "string"
      enum:
        - "PROCESSING"
        - "APPROVED"
        - "DECLINED"
        - "VOIDED"
        - "REVERSED"
        - "AUTH_ONLY"
        - "ADJUSTED"
    PaymentMethod:
      type: "string"
      enum:
        - "VISA"
        - "MASTERCARD"
        - "UNIONPAY"
        - "AMEX"
        - "JCB"
        - "DISCOVER"
        - "DINERS"
        - "MAESTRO"
        - "HIMYAN"
        - "NAPS"
        - "MY_DEBIT"
        - "ELO"
        - "CPACE"
        - "NAPAS"
        - "TROY"
        - "TAKAPAY"
        - "SADEEQ"
        - "ALIPAY"
        - "WECHAT"
        - "VISA_QR"
        - "MASTERCARD_QR"
        - "UNIONPAY_QR"
        - "SHOPEE_PAY_QR"
        - "GRAB_PAY_QR"
        - "ALIPAY_PLUS_QR"
        - "DUIT_NOW_QR"
        - "PAY_NOW"
        - "PH_QR"
        - "JCB_EMV_QR"
        - "LINEPAY_QR"
        - "EZ_PAY"
        - "PI_QR"
        - "ESUNWALLET_QR"
        - "TAIWANPAY_QR"
        - "EASYWALLET_QR"
        - "JKOPAY_QR"
        - "IPASSMONEY_PAY_QR"
        - "ICASH_PAY_QR"
        - "PLUSPAY_QR"
        - "PXPAYPLUS_QR"
        - "GAMA_PAY_QR"
        - "OPAY_QR"
        - "LINEPAYMONEY_QR"
        - "PAYPAY_QR"
        - "BCCARD_QR"
        - "NETS_QR"
        - "NTTDATA_QR"
        - "UCARD"
        - "CUP"
        - "SMARTPAY"
        - "BONUSPAY_QR"
        - "CASHLEZ_QR"
        - "UNKNOWN"
    EntryMode:
      type: "string"
      enum:
        - "NFC"
        - "SWIPE"
        - "CHIP"
        - "FALLBACK"
        - "MANUAL_KEYED"
        - "MERCHANT_PRESENT"
        - "CONSUMER_PRESENT"
        - "RETRIEVE"
        - "UNKNOWN"
    CvmPerformed:
      type: "string"
      enum:
        - "NO_CVM"
        - "SIGNATURE"
        - "PIN"
        - "OFFLINE_PIN"
        - "CDCVM"
        - "PIN_BYPASSED"
    CvmSignatureMode:
      type: "string"
      enum:
        - "ELECTRONIC_SIGNATURE"
        - "SIGN_ON_PAPER"
    EntityStatus:
      type: "string"
      enum:
        - "ACTIVE"
        - "DISABLED"
    BatchStatus:
      type: "string"
      enum:
        - "OPEN"
        - "SETTLED"
        - "SETTLE_FAILED"
        - "SETTLEMENT_IN_PROGRESS"
    BatchMode:
      type: "string"
      enum:
        - "AUTO"
        - "MANUAL"
        - "NO_NEED"
        - "AGGREGATED"
        - "MERCHANT_MANUAL"
    ActionType:
      type: "string"
      enum:
        - "NEW"
        - "ADJUST"
        - "VOID"
        - "AUTH_COMP"
        - "LINK_REFUND"
        - "CONFIRM"
        - "MERCHANT_PRESENT_QR"
        - "CONSUMER_PRESENT_QR"
        - "NOTIFIED"
        - "INQUIRY"
        - "CLOSE"
        - "INCREMENTAL_AUTH"
    ActionStatus:
      type: "string"
      enum:
        - "PROCESSING"
        - "SUCCESS"
        - "REJECTED"
        - "TIMEOUT"
        - "REVERSED"
        - "TO_BE_REVERSE"
    HostMessageFormat:
      type: "string"
      enum:
        - "MOCK"
        - "MPGS_AP"
        - "CARD_NOW"
        - "TWOC2P"
        - "CARDWORKS"
        - "SHIFT4"
        - "SHIFT4_HD"
        - "PAY_STORE"
        - "POMELO"
        - "EFT_PAY"
        - "EZPAY"
        - "BANKNBOX"
        - "IULINK"
        - "RAZER"
        - "WALLYT"
        - "GCAG"
        - "POSTBRIDGE"
        - "RENDEZVOUS"
        - "STD_JSON"
        - "S2M_SELECT"
        - "VAULTSPAY"
        - "SMART_VISTA"
        - "SADAD"
        - "AIRWALLEX"
        - "AIRWALLEX_EU"
        - "WORLDPAY"
        - "CASHPLUS"
        - "PSS"
        - "RAWBANK"
        - "NEOCASH"
        - "CASHLEZ"
        - "QRIS"
        - "MASARAT"
        - "BONUSPAY"
        - "ALTO"
        - "AXS"
        - "CARDINFOLINK"
        - "BBL"
        - "ONERWAY"
        - "ACB"
        - "KEENU"
        - "SELECT_STANDARD"
        - "SELECT_MANUAL_MODE"
        - "SELECT_AUTO_MODE"
        - "SELECT_V24_MODE"
        - "STD_JSON_BSD"
        - "STD_JSON_YEDPAY"

    # ==================== Transaction Schemas ====================
    GeoResult:
      type: "string"
      description: "Geo-fence outcome decided by the caller (SRS/TMS) before the request reached Enabler"
      enum:
        - "NA"
        - "ALLOWED"
        - "FLAGGED"
        - "DENIED"
    InstallmentRequest:
      type: "object"
      description: "Installment selection for a SALE (installment is a sale capability, not a transaction type). Supersedes the legacy count-only installmentPlan"
      properties:
        planId:
          type: "string"
          description: "The Installment Plan selected, as managed centrally in MMS"
        installmentCount:
          type: "integer"
          format: "int32"
          description: "The number of installments requested"
        revolving:
          $ref: "#/components/schemas/RevolvingRequest"
      required:
        - "planId"
        - "installmentCount"
    Installment:
      type: "object"
      description: "The Installment Plan applied to a transaction. Plan-owned attributes come from the resolved MMS configuration"
      properties:
        planId:
          type: "string"
        planName:
          type: "string"
          description: "The plan's display name at the time it was applied"
        installmentCount:
          type: "integer"
          format: "int32"
        interestModel:
          $ref: "#/components/schemas/InterestModel"
        installmentType:
          $ref: "#/components/schemas/InstallmentType"
        revolving:
          $ref: "#/components/schemas/Revolving"
        issuerPreapprovalReference:
          type: "string"
          description: "Issuer/acquirer enrolment or approval reference, normalised from the authorisation response; absent if the host returned none"
      required:
        - "planId"
        - "planName"
        - "installmentCount"
        - "interestModel"
        - "installmentType"
    AllowedActions:
      type: "object"
      description: "Backend-calculated flags for which derived operations a transaction currently permits"
      properties:
        refund:
          type: "boolean"
          default: false
        completion:
          type: "boolean"
          default: false
        incrementalAuth:
          type: "boolean"
          default: false
    InterestModel:
      type: "string"
      enum:
        - "WITH_INTEREST"
        - "NO_INTEREST"
    InstallmentType:
      type: "string"
      enum:
        - "REGULAR"
        - "REVOLVING"
    RevolvingRequest:
      type: "object"
      properties:
        initialPaymentAmount:
          $ref: "#/components/schemas/Amount"
      required:
        - "initialPaymentAmount"
    Revolving:
      type: "object"
      properties:
        initialPaymentAmount:
          $ref: "#/components/schemas/Amount"
        remainingAmount:
          $ref: "#/components/schemas/Amount"
      required:
        - "initialPaymentAmount"
        - "remainingAmount"
    ActionNew:
      type: "object"
      description: "POI request for new transaction"
      properties:
        tranType:
          $ref: "#/components/schemas/TranType"
        amount:
          $ref: "#/components/schemas/Amount"
        profileId:
          type: "string"
        acptId:
          type: "string"
          description: "Optional specific acceptance ID"
        primaryTid:
          type: "string"
          description: "Optional TID to select"
        description:
          type: "string"
        posReference:
          type: "string"
        extOrderId:
          type: "string"
          description: "External order ID supplied by the integrator"
        preferredAcceptanceTag:
          type: "string"
        cvmSignatureMode:
          $ref: "#/components/schemas/CvmSignatureMode"
        forcePaymentMethod:
          type: "array"
          items:
            $ref: "#/components/schemas/PaymentMethod"
        forceFetchProfile:
          type: "boolean"
          default: false
        tapToOwnDevice:
          type: "boolean"
          default: false
        installmentPlan:
          type: "integer"
          description: "Legacy count-only installment plan, superseded by installments; do not send both"
        installments:
          description: "Installment capability of a SALE: only sent with tranType SALE to pay the sale in installments (IPP / EMI). Supersedes installmentPlan"
          allOf:
            - $ref: "#/components/schemas/InstallmentRequest"
        linkedTranId:
          type: "string"
          description: "Linked transaction ID for SCA second transaction"
        isScaSecond:
          type: "boolean"
          description: "Indicates if this is a SCA (Strong Customer Authentication) second transaction. When true, linkedTranId is required."
        extra:
          type: "object"
          additionalProperties:
            type: "string"
        geoResult:
          $ref: "#/components/schemas/GeoResult"
        geoLocation:
          type: "object"
          additionalProperties: true
          description: "Device location snapshot captured when the geo-fence check ran; passed through verbatim"
        geoId:
          type: "string"
          description: "Identifier of the geo-fence rule the caller matched"
      required:
        - "tranType"
        - "amount"
        - "profileId"
    CardReadSuccess:
      type: "object"
      description: "Successful card read result"
      properties:
        paymentMethod:
          $ref: "#/components/schemas/PaymentMethod"
        entryMode:
          $ref: "#/components/schemas/EntryMode"
        maskedPan:
          type: "string"
        needOnline:
          type: "boolean"
          default: true
        cardKsn:
          type: "string"
        cvmPerformed:
          $ref: "#/components/schemas/CvmPerformed"
        pinKsn:
          type: "string"
        panToken:
          type: "string"
        reader:
          $ref: "#/components/schemas/Reader"
        appleTTP:
          $ref: "#/components/schemas/AppleTap2PayTransData"
        emvData:
          type: "object"
          additionalProperties:
            type: "string"
      required:
        - "paymentMethod"
        - "entryMode"
        - "maskedPan"
    Reader:
      type: "object"
      description: "Card reader device information"
      properties:
        model:
          type: "string"
          description: "Model identifier (e.g., MineHades, D177, Tap2Pay)"
        brand:
          type: "string"
          description: "Brand/manufacturer (e.g., MineSec, PAX, NEXGO)"
        sdkId:
          type: "string"
        sdkVersion:
          type: "string"
        hardwareSN:
          type: "string"
          description: "Hardware serial number"
        firmwareVersion:
          type: "string"
        emvKernelVersion:
          type: "string"
      required:
        - "model"
        - "brand"
    AppleTap2PayTransData:
      type: "object"
      description: "Apple Tap to Pay card read data"
      properties:
        readerToken:
          type: "string"
          description: "JWT token for iOS proximity reader activation"
        readerId:
          type: "string"
          description: "Unique physical ID of iPhone Proximity Reader"
        iosTrxId:
          type: "string"
          description: "iPhone generated unique ID for card tapping"
        protectedPaymentData:
          type: "string"
          description: "Base64 encoded payment data from iPhone Proximity Reader"
      required:
        - "readerToken"
        - "readerId"
        - "iosTrxId"
        - "protectedPaymentData"
    TransactionRequest:
      type: "object"
      properties:
        poiRequest:
          $ref: "#/components/schemas/ActionNew"
        cardRead:
          $ref: "#/components/schemas/CardReadSuccess"
        sdkId:
          type: "string"
        tokenizationAction:
          $ref: "#/components/schemas/TokenizationAction"
      required:
        - "poiRequest"
        - "cardRead"
        - "sdkId"
    TokenTransactionRequest:
      type: "object"
      description: "Card transaction paid with a saved payment token instead of a card read."
      properties:
        poiRequest:
          $ref: "#/components/schemas/ActionNew"
        paymentToken:
          $ref: "#/components/schemas/PaymentTokenReference"
        sdkId:
          type: "string"
      required:
        - "poiRequest"
        - "paymentToken"
        - "sdkId"
    PaymentTokenReference:
      type: "object"
      properties:
        tokenValue:
          type: "string"
          description: "`paymentToken.tokenValue` returned when the card was tokenized"
          example: "pst_string"
      required:
        - "tokenValue"
    TransactionRequestMpQr:
      type: "object"
      properties:
        poiRequest:
          $ref: "#/components/schemas/ActionNew"
        paymentMethod:
          $ref: "#/components/schemas/PaymentMethod"
        sdkId:
          type: "string"
        callbackUrl:
          type: "string"
      required:
        - "poiRequest"
        - "paymentMethod"
    Transaction:
      type: "object"
      description: "Transaction response"
      properties:
        tranId:
          type: "string"
        tranType:
          $ref: "#/components/schemas/TranType"
        tranStatus:
          $ref: "#/components/schemas/TranStatus"
        amount:
          $ref: "#/components/schemas/Amount"
        paymentMethod:
          $ref: "#/components/schemas/PaymentMethod"
        entryMode:
          $ref: "#/components/schemas/EntryMode"
        accountMasked:
          type: "string"
        accountBin:
          type: "string"
        accountLast4:
          type: "string"
        issCountryCode:
          type: "string"
        cvmPerformed:
          $ref: "#/components/schemas/CvmPerformed"
        cvmSignatureUrl:
          type: "string"
        cvmSignatureBase64:
          type: "string"
        aid:
          type: "string"
        appName:
          type: "string"
        tc:
          type: "string"
        tvr:
          type: "string"
        tsi:
          type: "string"
        atc:
          type: "string"
        profileId:
          type: "string"
        acceptanceId:
          type: "string"
          deprecated: true
          description: "Deprecated. Use acptId instead"
        acptId:
          type: "string"
        sdkId:
          type: "string"
        posReference:
          type: "string"
        extOrderId:
          type: "string"
          description: "External order ID supplied by the integrator"
        trace:
          type: "string"
        description:
          type: "string"
        callbackUrl:
          type: "string"
        installmentPlan:
          type: "integer"
          description: "Legacy count-only installment plan, superseded by installments"
        installments:
          description: "Installment applied to this SALE, if it was paid in installments"
          allOf:
            - $ref: "#/components/schemas/Installment"
        merchantName:
          type: "string"
        merchantAddr:
          type: "string"
        mcc:
          type: "string"
        primaryMid:
          type: "string"
        primaryTid:
          type: "string"
        subMid:
          type: "string"
        subTid:
          type: "string"
        hostMessageFormat:
          $ref: "#/components/schemas/HostMessageFormat"
        providerReference:
          type: "string"
        providerMchId:
          type: "string"
        isvId:
          type: "string"
        isvName:
          type: "string"
        shopId:
          type: "string"
        shopName:
          type: "string"
        linkedTranId:
          type: "string"
          description: "ID of the immediate parent transaction (e.g. the original sale for a refund)"
        linkedSrsTranId:
          type: "string"
        hasChildren:
          type: "boolean"
          description: "True when at least one derived transaction points to this one. Absent when not evaluated (e.g. list responses)"
        refundedAmount:
          $ref: "#/components/schemas/Amount"
        availableRefundAmount:
          $ref: "#/components/schemas/Amount"
        completedAmount:
          $ref: "#/components/schemas/Amount"
        availableCompletionAmount:
          $ref: "#/components/schemas/Amount"
        allowedActions:
          $ref: "#/components/schemas/AllowedActions"
        preferredAcceptanceTag:
          type: "string"
        extraData:
          type: "string"
        rrn:
          type: "string"
        approvalCode:
          type: "string"
        batchId:
          type: "string"
        batchNo:
          type: "string"
        actions:
          type: "array"
          items:
            $ref: "#/components/schemas/Action"
        srsTranId:
          type: "string"
        consumerPaymentDevice:
          type: "string"
        geoResult:
          $ref: "#/components/schemas/GeoResult"
        geoLocation:
          type: "object"
          additionalProperties: true
          description: "Device location snapshot captured when the geo-fence check ran; passed through verbatim"
        geoId:
          type: "string"
          description: "Identifier of the geo-fence rule the caller matched"
        tokenizationActionResponse:
          $ref: "#/components/schemas/TokenizationActionResponse"
        createdAt:
          type: "string"
          format: "date-time"
          example: "2026-01-26T12:57:01.373+08:00"
        updatedAt:
          type: "string"
          format: "date-time"
          example: "2026-01-26T12:57:01.381+08:00"
      required:
        - "tranId"
        - "tranType"
        - "tranStatus"
        - "amount"
        - "paymentMethod"
        - "entryMode"
        - "profileId"
        - "acptId"
        - "trace"
        - "merchantName"
        - "merchantAddr"
        - "mcc"
        - "primaryMid"
        - "primaryTid"
        - "hostMessageFormat"
        - "batchId"
        - "batchNo"
        - "actions"
        - "createdAt"
    # ==================== Tokenization Schemas ====================
    TokenizationAction:
      type: "object"
      description: "Optional card tokenization intent on a transaction request. Requires the `x-idempotency-key` header."
      properties:
        command:
          type: "string"
          enum:
            - "TOKENIZE_CARD"
        customerId:
          type: "string"
          description: "Tokenization customer to link the instrument to"
        metadata:
          type: "object"
          additionalProperties:
            type: "string"
      required:
        - "command"
    TokenizationActionStatus:
      type: "string"
      enum:
        - "SUCCEEDED"
        - "SKIPPED"
        - "FAILED"
    TokenizationActionResponse:
      type: "object"
      description: |
        Tokenization result attached to a transaction that carried `tokenizationAction`.
        The payment result is independent: SKIPPED or FAILED never changes the transaction.
      properties:
        command:
          type: "string"
          example: "TOKENIZE_CARD"
        status:
          $ref: "#/components/schemas/TokenizationActionStatus"
        reason:
          type: "string"
          description: "Skip or error code when status is not SUCCEEDED, e.g. NOT_ELIGIBLE, TOKENIZATION_UNAVAILABLE"
        message:
          type: "string"
        outcome:
          type: "string"
          description: "CREATED or EXISTING when status is SUCCEEDED"
        idempotencyReplayed:
          type: "boolean"
        instrument:
          $ref: "#/components/schemas/TokenizedInstrument"
        paymentToken:
          $ref: "#/components/schemas/TokenizedPaymentToken"
        analyticsToken:
          $ref: "#/components/schemas/TokenizedAnalyticsToken"
      required:
        - "command"
        - "status"
      example:
        command: "TOKENIZE_CARD"
        status: "SUCCEEDED"
        outcome: "CREATED"
        idempotencyReplayed: false
        instrument:
          instrumentId: "ins_01KF12TA15CPFDW2NKKQDQRJ5E"
          instrumentType: "CARD"
          status: "ACTIVE"
          cardBrand: "VISA"
          maskedPan: "476173******0027"
          lastFour: "0027"
          expiryMonth: 12
          expiryYear: 2031
        paymentToken:
          tokenId: "tok_01KF12TA15CPFDW2NKKQDQRJ5E"
          tokenKind: "PAYMENT"
          status: "ACTIVE"
          paymentCapable: true
          provider: "PAYSURFER"
        analyticsToken:
          analyticsTokenId: "atk_01KF12TA15CPFDW2NKKQDQRJ5E"
          status: "ACTIVE"
          paymentCapable: false
    InstrumentTokenizationRequest:
      type: "object"
      description: "Standalone card tokenization request (no payment)"
      properties:
        cardRead:
          $ref: "#/components/schemas/CardReadSuccess"
        profileId:
          type: "string"
          description: "Terminal profile whose merchant owns the tokenization scope. Defaults to the JWT profileId."
        customerId:
          type: "string"
          description: "Tokenization customer to link the instrument to"
        metadata:
          type: "object"
          additionalProperties:
            type: "string"
        sdkId:
          type: "string"
      required:
        - "cardRead"
    InstrumentTokenizationResponse:
      type: "object"
      properties:
        outcome:
          type: "string"
          description: "CREATED for a new instrument, EXISTING when the card was already tokenized for this merchant"
        idempotencyReplayed:
          type: "boolean"
        instrument:
          $ref: "#/components/schemas/TokenizedInstrument"
        paymentToken:
          $ref: "#/components/schemas/TokenizedPaymentToken"
        analyticsToken:
          $ref: "#/components/schemas/TokenizedAnalyticsToken"
      required:
        - "outcome"
        - "idempotencyReplayed"
        - "instrument"
        - "paymentToken"
        - "analyticsToken"
    TokenizedInstrument:
      type: "object"
      properties:
        instrumentId:
          type: "string"
        instrumentType:
          type: "string"
          example: "CARD"
        status:
          type: "string"
        cardBrand:
          type: "string"
        maskedPan:
          type: "string"
        lastFour:
          type: "string"
        expiryMonth:
          type: "integer"
        expiryYear:
          type: "integer"
      required:
        - "instrumentId"
        - "instrumentType"
        - "status"
        - "cardBrand"
        - "maskedPan"
        - "lastFour"
        - "expiryMonth"
        - "expiryYear"
    TokenizedPaymentToken:
      type: "object"
      properties:
        tokenId:
          type: "string"
        tokenKind:
          type: "string"
          example: "PAYMENT"
        status:
          type: "string"
        paymentCapable:
          type: "boolean"
        provider:
          type: "string"
        tokenValue:
          type: "string"
          description: "Present only when the tokenization exposure policy permits returning it"
      required:
        - "tokenId"
        - "tokenKind"
        - "status"
        - "paymentCapable"
        - "provider"
    TokenizedAnalyticsToken:
      type: "object"
      properties:
        analyticsTokenId:
          type: "string"
        status:
          type: "string"
        paymentCapable:
          type: "boolean"
      required:
        - "analyticsTokenId"
        - "status"
        - "paymentCapable"
    ActionRequest:
      type: "object"
      description: "Request to perform action on transaction."
      properties:
        actionType:
          $ref: "#/components/schemas/ActionType"
        amount:
          $ref: "#/components/schemas/Amount"
        originalActionId:
          type: "string"
        posReference:
          type: "string"
        srsTranId:
          type: "string"
      required:
        - "actionType"
    Action:
      type: "object"
      description: "Transaction action"
      properties:
        actionId:
          type: "string"
        trace:
          type: "string"
        actionType:
          $ref: "#/components/schemas/ActionType"
        actionStatus:
          $ref: "#/components/schemas/ActionStatus"
        requestId:
          type: "string"
        amount:
          $ref: "#/components/schemas/Amount"
        tranId:
          type: "string"
        reason:
          type: "string"
        hostRespCode:
          type: "string"
        hostRespMessage:
          type: "string"
        posReference:
          type: "string"
        extraData:
          type: "string"
        hostRespExtra:
          type: "object"
        geoResult:
          $ref: "#/components/schemas/GeoResult"
        geoLocation:
          type: "object"
          additionalProperties: true
          description: "Device location snapshot captured when the geo-fence check ran; passed through verbatim"
        geoId:
          type: "string"
          description: "Identifier of the geo-fence rule the caller matched"
        createdAt:
          type: "string"
          format: "date-time"
          example: "2026-01-26T12:57:01.373+08:00"
        updatedAt:
          type: "string"
          format: "date-time"
          example: "2026-01-26T12:57:01.381+08:00"
      required:
        - "actionId"
        - "trace"
        - "actionType"
        - "actionStatus"
        - "requestId"
        - "createdAt"

    # ==================== Batch Schemas ====================
    Batch:
      type: "object"
      properties:
        batchId:
          type: "string"
        batchNo:
          type: "string"
        batchStatus:
          $ref: "#/components/schemas/BatchStatus"
        batchMode:
          $ref: "#/components/schemas/BatchMode"
        profileId:
          type: "string"
        acptId:
          type: "string"
        merchantName:
          type: "string"
        providerMchId:
          type: "string"
        isvId:
          type: "string"
        isvName:
          type: "string"
        shopId:
          type: "string"
        shopName:
          type: "string"
        currency:
          type: "string"
        primaryMid:
          type: "string"
        primaryTid:
          type: "string"
        hostMessageFormat:
          $ref: "#/components/schemas/HostMessageFormat"
        signedOn:
          type: "boolean"
        tranSummaries:
          type: "array"
          items:
            $ref: "#/components/schemas/TranTypeSummary"
        batchSummary:
          $ref: "#/components/schemas/BatchSummary"
        createdAt:
          type: "string"
          format: "date-time"
          example: "2026-01-26T12:57:01.373+08:00"
        updatedAt:
          type: "string"
          format: "date-time"
          example: "2026-01-26T12:57:01.381+08:00"
      required:
        - "batchId"
        - "batchNo"
        - "batchStatus"
        - "profileId"
        - "acptId"
        - "currency"
        - "primaryMid"
        - "primaryTid"
        - "hostMessageFormat"
        - "tranSummaries"
        - "batchSummary"
        - "createdAt"
    TranTypeSummary:
      type: "object"
      properties:
        tranType:
          $ref: "#/components/schemas/TranType"
        tranStatus:
          $ref: "#/components/schemas/TranStatus"
        paymentMethod:
          $ref: "#/components/schemas/PaymentMethod"
        count:
          type: "integer"
          format: "int32"
        total:
          $ref: "#/components/schemas/Amount"
      required:
        - "tranType"
        - "tranStatus"
        - "count"
        - "total"
    BatchSummary:
      type: "object"
      properties:
        currency:
          type: "string"
        mchCreditTotal:
          type: "string"
        mchCreditCount:
          type: "integer"
          format: "int32"
        mchDebitTotal:
          type: "string"
        mchDebitCount:
          type: "integer"
          format: "int32"
      required:
        - "currency"
        - "mchCreditTotal"
        - "mchCreditCount"
        - "mchDebitTotal"
        - "mchDebitCount"

    # ==================== Profile Schemas ====================
    Profile:
      type: "object"
      properties:
        profileId:
          type: "string"
        profileName:
          type: "string"
        providerReference:
          type: "string"
        providerMchId:
          type: "string"
        isvId:
          type: "string"
        isvName:
          type: "string"
        shopId:
          type: "string"
        shopName:
          type: "string"
        merchantName:
          type: "string"
        merchantAddr:
          type: "string"
        merchantPhone:
          type: "string"
        merchantEmail:
          type: "string"
        merchantCity:
          type: "string"
        merchantPostalCode:
          type: "string"
        mcc:
          type: "string"
        countryCode:
          type: "string"
        currency:
          type: "string"
        entityStatus:
          $ref: "#/components/schemas/EntityStatus"
        createdAt:
          type: "string"
          format: "date-time"
          example: "2026-01-26T12:57:01.373+08:00"
        updatedAt:
          type: "string"
          format: "date-time"
          example: "2026-01-26T12:57:01.381+08:00"
      required:
        - "profileId"
        - "profileName"
        - "merchantName"
        - "mcc"
        - "countryCode"
        - "currency"
        - "entityStatus"
        - "createdAt"
        - "merchantAddr"
    ProfileWithAcceptances:
      type: "object"
      properties:
        profileId:
          type: "string"
        profileName:
          type: "string"
        providerReference:
          type: "string"
        providerMchId:
          type: "string"
        isvId:
          type: "string"
        isvName:
          type: "string"
        shopId:
          type: "string"
        shopName:
          type: "string"
        merchantName:
          type: "string"
        merchantAddr:
          type: "string"
        merchantPhone:
          type: "string"
        merchantEmail:
          type: "string"
        merchantCity:
          type: "string"
        merchantPostalCode:
          type: "string"
        mcc:
          type: "string"
        countryCode:
          type: "string"
        currency:
          type: "string"
        acceptances:
          type: "array"
          items:
            $ref: "#/components/schemas/Acceptance"
        acptSelection:
          type: "object"
          additionalProperties:
            type: "array"
            items:
              $ref: "#/components/schemas/PaymentMethod"
        entityStatus:
          $ref: "#/components/schemas/EntityStatus"
        createdAt:
          type: "string"
          format: "date-time"
          example: "2026-01-26T12:57:01.373+08:00"
        updatedAt:
          type: "string"
          format: "date-time"
          example: "2026-01-26T12:57:01.381+08:00"
      required:
        - "profileId"
        - "profileName"
        - "merchantName"
        - "mcc"
        - "countryCode"
        - "currency"
        - "acceptances"
        - "entityStatus"
        - "createdAt"
        - "merchantAddr"

    Acceptance:
      type: "object"
      properties:
        acptId:
          type: "string"
        acptName:
          type: "string"
        acptReference:
          type: "string"
        hostMessageFormat:
          $ref: "#/components/schemas/HostMessageFormat"
        supportedPayments:
          type: "array"
          items:
            $ref: "#/components/schemas/PaymentMethod"
        primaryMid:
          type: "string"
        primaryTid:
          type: "string"
        extraParams:
          type: "object"
          additionalProperties:
            type: "string"
        keyId:
          type: "string"
        tags:
          type: "array"
          items:
            type: "string"
        entityStatus:
          $ref: "#/components/schemas/EntityStatus"
        createdAt:
          type: "string"
          format: "date-time"
          example: "2026-01-26T12:57:01.373+08:00"
        updatedAt:
          type: "string"
          format: "date-time"
          example: "2026-01-26T12:57:01.381+08:00"
      required:
        - "acptId"
        - "hostMessageFormat"
        - "supportedPayments"
        - "primaryMid"
        - "primaryTid"
        - "entityStatus"
        - "createdAt"
        - "extraParams"
