openapi: 3.0.3
info:
  title: FusionPay API Solutions Gateway
  version: 1.0.0
  description: |
    Machine-readable OpenAPI contract for the public API Solutions gateway.

    This spec covers public runtime endpoints under `/api/solutions/v1`.
    It intentionally excludes admin-side client onboarding, API key management,
    and internal callback endpoints.

    All endpoints require `X-FusionPay-Api-Key`. The gateway may also emit
    rate-limit and usage-quota headers such as `X-RateLimit-Limit`,
    `X-RateLimit-Remaining`, `X-RateLimit-Reset`, `X-Quota-Daily-Limit`,
    `X-Quota-Daily-Remaining`, and `X-Quota-Monthly-Remaining`.
servers:
# Single server URL is injected at serve time from the backend APP_URL env
# (see serveOpenAPISpec).
- url: "https://app-api.myfusionpay.com"/api/solutions/v1
  description: FusionPay API Solutions gateway.
security:
- ApiKeyAuth: []
tags:
- name: Auth
  description: API key verification.
- name: Users & KYC
  description: Partner end-user registration and hosted KYC flow.
- name: Virtual Cards
  description: Virtual card issue, top-up, lock/unlock, details, and transactions.
- name: Physical Cards
  description: Physical card application, top-up, lock/unlock, delivery/application
    status, and PIN reset link.
- name: Deposits
  description: Deposit and card top-up creation plus deposit limits.
- name: Transfers
  description: Transfer authentication, quote, initiation, and history.
- name: Webhooks
  description: Partner outbound webhook subscriptions.
paths:
  "/token-check":
    get:
      tags:
      - Auth
      operationId: tokenCheck
      summary: Validate API key access
      responses:
        '200':
          description: API key is valid for API Solutions.
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/TokenCheckEnvelope"
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '429':
          "$ref": "#/components/responses/TooManyRequests"
        '500':
          "$ref": "#/components/responses/InternalError"
  "/users":
    post:
      tags:
      - Users & KYC
      operationId: createEndUser
      summary: Create or reuse a partner end-user mapping
      description: |-
        Creates or reuses an end-user mapping scoped to the authenticated client.

        Supported request variants:
        - Minimal contact: send `external_user_id` plus either `email` or `mobile`.
        - KYC-ready contact: send `mobile` in `+{countryCode} {number}` format so hosted KYC can be initiated later.
        - Full contact: send both `email` and `mobile`; `external_user_id` remains unique per client.

        Public contract: the response returns FusionPay `user_id` and public KYC state only. Internal upstream member IDs are never returned.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              "$ref": "#/components/schemas/CreateEndUserRequest"
            example:
              external_user_id: partner-user-001
              email: partner.user@example.com
              mobile: "+62 81234567890"
      responses:
        '200':
          description: End-user created or reused.
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/EndUserEnvelope"
        '400':
          description: Validation error. `external_user_id` is required; either
            `email` or `mobile` must be supplied; `mobile` must use the
            `+{countryCode} {number}` format.
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorEnvelope"
              examples:
                missingExternalUserId:
                  summary: Missing external_user_id
                  value:
                    request_id: 123e4567-e89b-12d3-a456-426614174000
                    http_status: 400
                    key: INVALID_REQUEST
                    message: INVALID_REQUEST
                    error: null
                    errors:
                      external_user_i_d: external user i d is required
                    trace: null
                missingContact:
                  summary: Neither email nor mobile supplied
                  value:
                    request_id: 123e4567-e89b-12d3-a456-426614174000
                    http_status: 400
                    key: INVALID_REQUEST
                    message: INVALID_REQUEST
                    error: email or mobile is required
                    errors: null
                    trace: email or mobile is required
                invalidMobile:
                  summary: Mobile not in +{countryCode} {number} format
                  value:
                    request_id: 123e4567-e89b-12d3-a456-426614174000
                    http_status: 400
                    key: INVALID_REQUEST
                    message: INVALID_REQUEST
                    error: mobile must be in format '+{countryCode} {number}',
                      for example '+62 81234567890'
                    errors: null
                    trace: mobile must be in format '+{countryCode} {number}',
                      for example '+62 81234567890'
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '429':
          "$ref": "#/components/responses/TooManyRequests"
        '500':
          "$ref": "#/components/responses/InternalError"
  "/users/{user_id}/kyc":
    post:
      tags:
      - Users & KYC
      operationId: initiateKYC
      summary: Start hosted KYC for an end-user
      description: |-
        Starts hosted KYC and returns a FusionPay `redirect_url`.

        Supported request variants:
        - `REGTANK`: general hosted KYC flow.
        - `SINGPASS`: Singapore identity flow when enabled for the end-user/product.
        - Locale aliases: `en-US`, `en`, `zh-CN`, and `zh` are normalized to the canonical gateway locales `en_US` and `zh_CN`.
        - Redirect URLs: `success_url` is required by the hosted flow; `error_url` is optional but recommended.

        Public contract: the returned `redirect_url` is a FusionPay URL. The upstream hosted URL and upstream status labels are not exposed.
      parameters:
      - "$ref": "#/components/parameters/UserID"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              "$ref": "#/components/schemas/InitiateKYCRequest"
            example:
              type: REGTANK
              locale: en_US
              success_url: https://partner.example.com/kyc/success
              error_url: https://partner.example.com/kyc/error
      responses:
        '200':
          description: KYC session initiated.
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/KYCEnvelope"
        '400':
          description: Validation error. `success_url` is required and must be an
            absolute URL; optional `error_url` must also be a valid absolute URL
            when provided; `type` is required when the user has no stored KYC type;
            the user must have a valid mobile; `locale` accepts only `en_US` /
            `zh_CN` (and aliases).
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorEnvelope"
              examples:
                missingSuccessUrl:
                  summary: Missing success_url
                  value:
                    request_id: 123e4567-e89b-12d3-a456-426614174000
                    http_status: 400
                    key: INVALID_REQUEST
                    message: INVALID_REQUEST
                    error: success_url is required
                    errors: null
                    trace: success_url is required
                invalidSuccessUrl:
                  summary: success_url is not an absolute URL
                  value:
                    request_id: 123e4567-e89b-12d3-a456-426614174000
                    http_status: 400
                    key: INVALID_REQUEST
                    message: INVALID_REQUEST
                    error: success_url must be a valid absolute URL
                    errors: null
                    trace: success_url must be a valid absolute URL
                invalidErrorUrl:
                  summary: error_url is not an absolute URL
                  value:
                    request_id: 123e4567-e89b-12d3-a456-426614174000
                    http_status: 400
                    key: INVALID_REQUEST
                    message: INVALID_REQUEST
                    error: error_url must be a valid absolute URL
                    errors: null
                    trace: error_url must be a valid absolute URL
                missingType:
                  summary: No KYC type supplied or stored
                  value:
                    request_id: 123e4567-e89b-12d3-a456-426614174000
                    http_status: 400
                    key: INVALID_REQUEST
                    message: INVALID_REQUEST
                    error: type is required and must be one of SINGPASS or REGTANK
                    errors: null
                    trace: type is required and must be one of SINGPASS or REGTANK
                missingMobile:
                  summary: End-user has no valid mobile for hosted KYC
                  value:
                    request_id: 123e4567-e89b-12d3-a456-426614174000
                    http_status: 400
                    key: INVALID_REQUEST
                    message: INVALID_REQUEST
                    error: mobile is required before initiating hosted KYC; create
                      the user with a valid mobile in format '+{countryCode} {number}'
                    errors: null
                    trace: mobile is required before initiating hosted KYC; create
                      the user with a valid mobile in format '+{countryCode} {number}'
                invalidLocale:
                  summary: Unsupported locale
                  value:
                    request_id: 123e4567-e89b-12d3-a456-426614174000
                    http_status: 400
                    key: INVALID_REQUEST
                    message: INVALID_REQUEST
                    error: locale must be one of en_US or zh_CN; aliases like en-US
                      and zh-CN are also accepted
                    errors: null
                    trace: locale must be one of en_US or zh_CN; aliases like en-US
                      and zh-CN are also accepted
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '404':
          "$ref": "#/components/responses/NotFound"
        '429':
          "$ref": "#/components/responses/TooManyRequests"
        '500':
          "$ref": "#/components/responses/InternalError"
    get:
      tags:
      - Users & KYC
      operationId: getKYCStatus
      summary: Read KYC status
      description: Returns local KYC state after a best-effort status refresh.
      parameters:
      - "$ref": "#/components/parameters/UserID"
      responses:
        '200':
          description: KYC status retrieved.
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/KYCEnvelope"
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '404':
          "$ref": "#/components/responses/NotFound"
        '429':
          "$ref": "#/components/responses/TooManyRequests"
        '500':
          "$ref": "#/components/responses/InternalError"
  "/virtual-cards":
    post:
      tags:
      - Virtual Cards
      operationId: issueVirtualCard
      summary: Issue virtual card
      description: |-
        Issues a virtual card for an approved end-user.

        Supported request variants:
        - Standard virtual card: `user_id` only.
        - Domestic/foreign product selection: use `is_singaporean` when the product policy needs that distinction.
        - Display hint: `card_name` may be supplied for partner-side display/support.

        Public contract: raw PAN and raw CVV are never returned. Card-product routing details are kept private.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              "$ref": "#/components/schemas/IssueVirtualCardRequest"
            example:
              user_id: 123e4567-e89b-12d3-a456-426614174000
              is_singaporean: false
              card_name: Marketing team card
              mailing_address:
                street_name: 71 Robinson Road
                city: Singapore
                neighborhood: Tanjong Pagar
                state: Singapore
                country_code: SG
                zip_code: '068895'
      responses:
        '200':
          description: Virtual card issued or application submitted.
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/CardEnvelope"
        '400':
          description: Validation error. `user_id` is required. `mailing_address`
            is optional for virtual cards, but when supplied all of its fields are
            required. The end-user must be KYC-approved.
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorEnvelope"
              examples:
                missingUserId:
                  summary: Missing user_id
                  value:
                    request_id: 123e4567-e89b-12d3-a456-426614174000
                    http_status: 400
                    key: INVALID_REQUEST
                    message: INVALID_REQUEST
                    error: null
                    errors:
                      user_i_d: user i d is required
                    trace: null
                incompleteMailingAddress:
                  summary: Partial mailing_address (all-or-none)
                  value:
                    request_id: 123e4567-e89b-12d3-a456-426614174000
                    http_status: 400
                    key: INVALID_REQUEST
                    message: INVALID_REQUEST
                    error: mailing_address.city is required
                    errors: null
                    trace: mailing_address.city is required
                kycNotApproved:
                  summary: End-user KYC is not approved
                  value:
                    request_id: 123e4567-e89b-12d3-a456-426614174000
                    http_status: 400
                    key: KYC_NOT_APPROVED
                    message: KYC_NOT_APPROVED
                    error: null
                    errors: null
                    trace: null
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '404':
          "$ref": "#/components/responses/NotFound"
        '429':
          "$ref": "#/components/responses/TooManyRequests"
        '500':
          "$ref": "#/components/responses/InternalError"
  "/virtual-cards/{card_id}":
    get:
      tags:
      - Virtual Cards
      operationId: getVirtualCard
      summary: Retrieve virtual card details
      parameters:
      - "$ref": "#/components/parameters/CardID"
      responses:
        '200':
          description: Card retrieved.
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/CardEnvelope"
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '404':
          "$ref": "#/components/responses/NotFound"
        '429':
          "$ref": "#/components/responses/TooManyRequests"
        '500':
          "$ref": "#/components/responses/InternalError"
  "/virtual-cards/{card_id}/topups":
    post:
      tags:
      - Virtual Cards
      operationId: topUpVirtualCard
      summary: Create top-up for virtual card
      parameters:
      - "$ref": "#/components/parameters/CardID"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              "$ref": "#/components/schemas/CreateCardTopUpRequest"
            examples:
              allFields:
                summary: Complete top-up payload template
                value:
                  user_id: 123e4567-e89b-12d3-a456-426614174000
                  mode: hosted_deposit
                  amount: '100.00'
                  currency: '840'
                  from_currency: '840'
                  channel_code: 1
                  deposit_country: SG
                  authentication_code: '123456'
                  payment_method:
                  - card
                  success_url: https://partner.example.com/topup/success
                  failure_url: https://partner.example.com/topup/failure
                  asset: USDT
                  network: TRC20
                  smart_contract_address: TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t
              hostedDeposit:
                summary: Hosted payment session top-up
                value:
                  mode: hosted_deposit
                  amount: '100.00'
                  currency: '840'
                  success_url: https://partner.example.com/topup/success
                  failure_url: https://partner.example.com/topup/failure
              walletAddress:
                summary: Crypto wallet-address top-up
                value:
                  mode: wallet_address
                  amount: '100.00'
                  currency: '840'
                  asset: USDT
                  network: TRC20
      responses:
        '200':
          description: Top-up created.
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/DepositEnvelope"
        '400':
          description: Validation error. `mode`, `amount`, and `currency` are
            required; `mode` must be `hosted_deposit` or `wallet_address`;
            `network` is required for `wallet_address`; `amount` must be a
            positive number. The card account must be provisioned (ready).
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorEnvelope"
              examples:
                missingFields:
                  summary: Missing required fields
                  value:
                    request_id: 123e4567-e89b-12d3-a456-426614174000
                    http_status: 400
                    key: INVALID_REQUEST
                    message: INVALID_REQUEST
                    error: null
                    errors:
                      mode: mode is required
                      amount: amount is required
                      currency: currency is required
                    trace: null
                invalidMode:
                  summary: Unsupported mode value
                  value:
                    request_id: 123e4567-e89b-12d3-a456-426614174000
                    http_status: 400
                    key: INVALID_REQUEST
                    message: INVALID_REQUEST
                    error: null
                    errors:
                      mode: Invalid value for mode
                    trace: null
                missingNetwork:
                  summary: wallet_address mode without network
                  value:
                    request_id: 123e4567-e89b-12d3-a456-426614174000
                    http_status: 400
                    key: INVALID_REQUEST
                    message: INVALID_REQUEST
                    error: network is required for wallet_address mode
                    errors: null
                    trace: network is required for wallet_address mode
                invalidAmount:
                  summary: Non-positive amount (wallet_address mode only)
                  value:
                    request_id: 123e4567-e89b-12d3-a456-426614174000
                    http_status: 400
                    key: INVALID_REQUEST
                    message: INVALID_REQUEST
                    error: amount must be a positive number
                    errors: null
                    trace: amount must be a positive number
                cardNotReady:
                  summary: Card account not yet provisioned
                  value:
                    request_id: 123e4567-e89b-12d3-a456-426614174000
                    http_status: 400
                    key: INVALID_REQUEST
                    message: INVALID_REQUEST
                    error: card account is not ready
                    errors: null
                    trace: card account is not ready
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '404':
          "$ref": "#/components/responses/NotFound"
        '429':
          "$ref": "#/components/responses/TooManyRequests"
        '500':
          "$ref": "#/components/responses/InternalError"
      description: |-
        Creates a top-up for the card in the path. The request body follows `POST /deposits`, but `card_id` is resolved from the URL.

        Supported request variants: `hosted_deposit`, authenticated `hosted_deposit`, hosted flow with return URLs, `wallet_address`, and token-specific `wallet_address`.
  "/virtual-cards/{card_id}/lock":
    post:
      tags:
      - Virtual Cards
      operationId: lockVirtualCard
      summary: Lock virtual card
      parameters:
      - "$ref": "#/components/parameters/CardID"
      responses:
        '200':
          description: Card locked.
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/CardEnvelope"
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '404':
          "$ref": "#/components/responses/NotFound"
        '429':
          "$ref": "#/components/responses/TooManyRequests"
        '500':
          "$ref": "#/components/responses/InternalError"
  "/virtual-cards/{card_id}/unlock":
    post:
      tags:
      - Virtual Cards
      operationId: unlockVirtualCard
      summary: Unlock virtual card
      parameters:
      - "$ref": "#/components/parameters/CardID"
      responses:
        '200':
          description: Card unlocked.
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/CardEnvelope"
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '404':
          "$ref": "#/components/responses/NotFound"
        '429':
          "$ref": "#/components/responses/TooManyRequests"
        '500':
          "$ref": "#/components/responses/InternalError"
  "/virtual-cards/{card_id}/transactions":
    get:
      tags:
      - Virtual Cards
      operationId: listVirtualCardTransactions
      summary: Retrieve virtual card transaction history
      parameters:
      - "$ref": "#/components/parameters/CardID"
      - "$ref": "#/components/parameters/Page"
      - "$ref": "#/components/parameters/Limit"
      responses:
        '200':
          description: Card transaction history retrieved.
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/CardTransactionListEnvelope"
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '404':
          "$ref": "#/components/responses/NotFound"
        '429':
          "$ref": "#/components/responses/TooManyRequests"
        '500':
          "$ref": "#/components/responses/InternalError"
      description: |-
        Returns a merged public transaction history for the card.

        Supported transaction item types:
        - `deposit`: card top-up records from `hosted_deposit` or `wallet_address` flows.
        - `transfer`: outgoing transfer records. The underlying transfer can be `p2p`, `alipay`, `wechat`, or `bank`; retrieve `/transfers/{transfer_id}` for transfer-specific fields.

        Public contract: transaction items expose FusionPay IDs and public status only. Upstream transaction references are not returned.
  "/physical-cards":
    post:
      tags:
      - Physical Cards
      operationId: applyPhysicalCard
      summary: Apply for physical card
      description: |-
        Applies for a physical card for an approved end-user.

        Supported request variants:
        - Standard physical card: `user_id` plus complete `mailing_address`.
        - Domestic/foreign product selection: use `is_singaporean` when the product policy needs that distinction.
        - Display hint: `card_name` may be supplied for partner-side display/support.

        Public contract: `delivery_status` is best-effort and may be `unknown` until delivery updates are available.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              "$ref": "#/components/schemas/IssuePhysicalCardRequest"
            example:
              user_id: 123e4567-e89b-12d3-a456-426614174000
              is_singaporean: false
              card_name: Ops team card
              mailing_address:
                street_name: 71 Robinson Road
                city: Singapore
                neighborhood: Tanjong Pagar
                state: Singapore
                country_code: SG
                zip_code: '068895'
      responses:
        '200':
          description: Physical card application submitted.
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/CardEnvelope"
        '400':
          description: Validation error. `user_id` and a complete `mailing_address`
            are required for physical cards. The end-user must be KYC-approved.
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorEnvelope"
              examples:
                missingUserId:
                  summary: Missing user_id
                  value:
                    request_id: 123e4567-e89b-12d3-a456-426614174000
                    http_status: 400
                    key: INVALID_REQUEST
                    message: INVALID_REQUEST
                    error: null
                    errors:
                      user_i_d: user i d is required
                    trace: null
                missingMailingAddress:
                  summary: Missing mailing_address
                  value:
                    request_id: 123e4567-e89b-12d3-a456-426614174000
                    http_status: 400
                    key: INVALID_REQUEST
                    message: INVALID_REQUEST
                    error: mailing_address is required
                    errors: null
                    trace: mailing_address is required
                incompleteMailingAddress:
                  summary: Incomplete mailing_address
                  value:
                    request_id: 123e4567-e89b-12d3-a456-426614174000
                    http_status: 400
                    key: INVALID_REQUEST
                    message: INVALID_REQUEST
                    error: mailing_address.zip_code is required
                    errors: null
                    trace: mailing_address.zip_code is required
                kycNotApproved:
                  summary: End-user KYC is not approved
                  value:
                    request_id: 123e4567-e89b-12d3-a456-426614174000
                    http_status: 400
                    key: KYC_NOT_APPROVED
                    message: KYC_NOT_APPROVED
                    error: null
                    errors: null
                    trace: null
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '404':
          "$ref": "#/components/responses/NotFound"
        '429':
          "$ref": "#/components/responses/TooManyRequests"
        '500':
          "$ref": "#/components/responses/InternalError"
  "/physical-cards/{card_id}/application":
    get:
      tags:
      - Physical Cards
      operationId: getPhysicalCardApplication
      summary: Retrieve physical card application status
      parameters:
      - "$ref": "#/components/parameters/CardID"
      responses:
        '200':
          description: Card application status retrieved.
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/CardApplicationEnvelope"
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '404':
          "$ref": "#/components/responses/NotFound"
        '429':
          "$ref": "#/components/responses/TooManyRequests"
        '500':
          "$ref": "#/components/responses/InternalError"
  "/physical-cards/{card_id}":
    get:
      tags:
      - Physical Cards
      operationId: getPhysicalCard
      summary: Retrieve physical card details
      parameters:
      - "$ref": "#/components/parameters/CardID"
      responses:
        '200':
          description: Card retrieved.
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/CardEnvelope"
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '404':
          "$ref": "#/components/responses/NotFound"
        '429':
          "$ref": "#/components/responses/TooManyRequests"
        '500':
          "$ref": "#/components/responses/InternalError"
  "/physical-cards/{card_id}/topups":
    post:
      tags:
      - Physical Cards
      operationId: topUpPhysicalCard
      summary: Create top-up for physical card
      parameters:
      - "$ref": "#/components/parameters/CardID"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              "$ref": "#/components/schemas/CreateCardTopUpRequest"
            examples:
              allFields:
                summary: Complete top-up payload template
                value:
                  user_id: 123e4567-e89b-12d3-a456-426614174000
                  mode: hosted_deposit
                  amount: '100.00'
                  currency: '840'
                  from_currency: '840'
                  channel_code: 1
                  deposit_country: SG
                  authentication_code: '123456'
                  payment_method:
                  - card
                  success_url: https://partner.example.com/topup/success
                  failure_url: https://partner.example.com/topup/failure
                  asset: USDT
                  network: TRC20
                  smart_contract_address: TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t
              hostedDeposit:
                summary: Hosted payment session top-up
                value:
                  mode: hosted_deposit
                  amount: '100.00'
                  currency: '840'
                  success_url: https://partner.example.com/topup/success
                  failure_url: https://partner.example.com/topup/failure
              walletAddress:
                summary: Crypto wallet-address top-up
                value:
                  mode: wallet_address
                  amount: '100.00'
                  currency: '840'
                  asset: USDT
                  network: TRC20
      responses:
        '200':
          description: Top-up created.
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/DepositEnvelope"
        '400':
          description: Validation error. `mode`, `amount`, and `currency` are
            required; `mode` must be `hosted_deposit` or `wallet_address`;
            `network` is required for `wallet_address`; `amount` must be a
            positive number. The card account must be provisioned (ready).
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorEnvelope"
              examples:
                missingFields:
                  summary: Missing required fields
                  value:
                    request_id: 123e4567-e89b-12d3-a456-426614174000
                    http_status: 400
                    key: INVALID_REQUEST
                    message: INVALID_REQUEST
                    error: null
                    errors:
                      mode: mode is required
                      amount: amount is required
                      currency: currency is required
                    trace: null
                invalidMode:
                  summary: Unsupported mode value
                  value:
                    request_id: 123e4567-e89b-12d3-a456-426614174000
                    http_status: 400
                    key: INVALID_REQUEST
                    message: INVALID_REQUEST
                    error: null
                    errors:
                      mode: Invalid value for mode
                    trace: null
                missingNetwork:
                  summary: wallet_address mode without network
                  value:
                    request_id: 123e4567-e89b-12d3-a456-426614174000
                    http_status: 400
                    key: INVALID_REQUEST
                    message: INVALID_REQUEST
                    error: network is required for wallet_address mode
                    errors: null
                    trace: network is required for wallet_address mode
                invalidAmount:
                  summary: Non-positive amount (wallet_address mode only)
                  value:
                    request_id: 123e4567-e89b-12d3-a456-426614174000
                    http_status: 400
                    key: INVALID_REQUEST
                    message: INVALID_REQUEST
                    error: amount must be a positive number
                    errors: null
                    trace: amount must be a positive number
                cardNotReady:
                  summary: Card account not yet provisioned
                  value:
                    request_id: 123e4567-e89b-12d3-a456-426614174000
                    http_status: 400
                    key: INVALID_REQUEST
                    message: INVALID_REQUEST
                    error: card account is not ready
                    errors: null
                    trace: card account is not ready
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '404':
          "$ref": "#/components/responses/NotFound"
        '429':
          "$ref": "#/components/responses/TooManyRequests"
        '500':
          "$ref": "#/components/responses/InternalError"
      description: |-
        Creates a top-up for the physical card in the path. The request body follows `POST /deposits`, but `card_id` is resolved from the URL.

        Supported request variants: `hosted_deposit`, authenticated `hosted_deposit`, hosted flow with return URLs, `wallet_address`, and token-specific `wallet_address`.
  "/physical-cards/{card_id}/lock":
    post:
      tags:
      - Physical Cards
      operationId: lockPhysicalCard
      summary: Lock physical card
      parameters:
      - "$ref": "#/components/parameters/CardID"
      responses:
        '200':
          description: Card locked.
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/CardEnvelope"
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '404':
          "$ref": "#/components/responses/NotFound"
        '429':
          "$ref": "#/components/responses/TooManyRequests"
        '500':
          "$ref": "#/components/responses/InternalError"
  "/physical-cards/{card_id}/unlock":
    post:
      tags:
      - Physical Cards
      operationId: unlockPhysicalCard
      summary: Unlock physical card
      parameters:
      - "$ref": "#/components/parameters/CardID"
      responses:
        '200':
          description: Card unlocked.
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/CardEnvelope"
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '404':
          "$ref": "#/components/responses/NotFound"
        '429':
          "$ref": "#/components/responses/TooManyRequests"
        '500':
          "$ref": "#/components/responses/InternalError"
  "/physical-cards/{card_id}/pin-reset-link":
    post:
      tags:
      - Physical Cards
      operationId: createPhysicalCardPINResetLink
      summary: Retrieve PIN reset request link
      parameters:
      - "$ref": "#/components/parameters/CardID"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              "$ref": "#/components/schemas/PINResetLinkRequest"
            example:
              success_url: https://partner.example.com/pin-reset/success
              failure_url: https://partner.example.com/pin-reset/failure
      responses:
        '200':
          description: PIN reset link created.
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/PINResetLinkEnvelope"
        '400':
          description: Validation error. The card account must be provisioned and
            ready, and PIN reset is only available for physical cards.
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorEnvelope"
              examples:
                cardNotReady:
                  summary: Card account is not ready
                  value:
                    request_id: 123e4567-e89b-12d3-a456-426614174000
                    http_status: 400
                    key: INVALID_REQUEST
                    message: INVALID_REQUEST
                    error: card account is not ready
                    errors: null
                    trace: card account is not ready
                wrongCardType:
                  summary: Card is not a physical card
                  value:
                    request_id: 123e4567-e89b-12d3-a456-426614174000
                    http_status: 400
                    key: INVALID_CARD_TYPE
                    message: INVALID_CARD_TYPE
                    error: pin reset is only available for physical cards
                    errors: null
                    trace: pin reset is only available for physical cards
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '404':
          "$ref": "#/components/responses/NotFound"
        '429':
          "$ref": "#/components/responses/TooManyRequests"
        '500':
          "$ref": "#/components/responses/InternalError"
  "/deposits/authentication":
    post:
      tags:
      - Deposits
      operationId: requestDepositAuthentication
      summary: Request deposit OTP
      requestBody:
        required: true
        content:
          application/json:
            schema:
              "$ref": "#/components/schemas/DepositAuthenticationRequest"
            example:
              user_id: 123e4567-e89b-12d3-a456-426614174000
              card_id: 223e4567-e89b-12d3-a456-426614174000
      responses:
        '200':
          description: Deposit authentication result.
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/AuthenticationEnvelope"
        '400':
          description: Validation error. `user_id` is required; when `card_id` is
            supplied the card must belong to the user.
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorEnvelope"
              examples:
                missingUserId:
                  summary: Missing user_id
                  value:
                    request_id: 123e4567-e89b-12d3-a456-426614174000
                    http_status: 400
                    key: INVALID_REQUEST
                    message: INVALID_REQUEST
                    error: null
                    errors:
                      user_i_d: user i d is required
                    trace: null
                cardOwnershipMismatch:
                  summary: card_id not owned by user_id
                  value:
                    request_id: 123e4567-e89b-12d3-a456-426614174000
                    http_status: 400
                    key: INVALID_REQUEST
                    message: INVALID_REQUEST
                    error: user_id does not own card_id
                    errors: null
                    trace: user_id does not own card_id
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '404':
          "$ref": "#/components/responses/NotFound"
        '429':
          "$ref": "#/components/responses/TooManyRequests"
        '500':
          "$ref": "#/components/responses/InternalError"
      description: |-
        Requests a deposit authentication challenge when the selected funding route requires additional verification.

        Supported request variants:
        - User-level challenge: send `user_id` only.
        - Card-bound challenge: send `user_id` and `card_id`; FusionPay checks that the card belongs to the user.

        Use the returned code destination metadata to collect the OTP, then pass `authentication_code` to `POST /deposits` or card top-up endpoints when required.
  "/deposits":
    post:
      tags:
      - Deposits
      operationId: createDeposit
      summary: Create deposit or wallet-address top-up
      requestBody:
        required: true
        content:
          application/json:
            schema:
              "$ref": "#/components/schemas/CreateDepositRequest"
            examples:
              allFields:
                summary: Complete deposit payload template
                value:
                  user_id: 123e4567-e89b-12d3-a456-426614174000
                  card_id: 223e4567-e89b-12d3-a456-426614174000
                  mode: hosted_deposit
                  amount: '100.00'
                  currency: '840'
                  from_currency: '840'
                  channel_code: 1
                  deposit_country: SG
                  authentication_code: '123456'
                  payment_method:
                  - card
                  success_url: https://partner.example.com/deposits/success
                  failure_url: https://partner.example.com/deposits/failure
                  asset: USDT
                  network: TRC20
                  smart_contract_address: TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t
              hostedDeposit:
                summary: Hosted payment session
                value:
                  card_id: 223e4567-e89b-12d3-a456-426614174000
                  mode: hosted_deposit
                  amount: '100.00'
                  currency: '840'
                  success_url: https://partner.example.com/deposits/success
                  failure_url: https://partner.example.com/deposits/failure
              walletAddress:
                summary: Crypto wallet-address instruction
                value:
                  card_id: 223e4567-e89b-12d3-a456-426614174000
                  mode: wallet_address
                  amount: '100.00'
                  currency: '840'
                  asset: USDT
                  network: TRC20
      responses:
        '200':
          description: Deposit created.
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/DepositEnvelope"
        '400':
          description: Validation error. `card_id`, `mode`, `amount`, and
            `currency` are required; `mode` must be `hosted_deposit` or
            `wallet_address`; `network` is required for `wallet_address`; `amount`
            must be a positive number; when `user_id` is supplied it must own
            `card_id`; the card account must be provisioned (ready).
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorEnvelope"
              examples:
                missingCardId:
                  summary: Missing card_id
                  value:
                    request_id: 123e4567-e89b-12d3-a456-426614174000
                    http_status: 400
                    key: INVALID_REQUEST
                    message: INVALID_REQUEST
                    error: card_id is required
                    errors: null
                    trace: card_id is required
                missingFields:
                  summary: Missing required fields
                  value:
                    request_id: 123e4567-e89b-12d3-a456-426614174000
                    http_status: 400
                    key: INVALID_REQUEST
                    message: INVALID_REQUEST
                    error: null
                    errors:
                      mode: mode is required
                      amount: amount is required
                      currency: currency is required
                    trace: null
                cardOwnershipMismatch:
                  summary: card_id not owned by user_id
                  value:
                    request_id: 123e4567-e89b-12d3-a456-426614174000
                    http_status: 400
                    key: INVALID_REQUEST
                    message: INVALID_REQUEST
                    error: user_id does not own card_id
                    errors: null
                    trace: user_id does not own card_id
                missingNetwork:
                  summary: wallet_address mode without network
                  value:
                    request_id: 123e4567-e89b-12d3-a456-426614174000
                    http_status: 400
                    key: INVALID_REQUEST
                    message: INVALID_REQUEST
                    error: network is required for wallet_address mode
                    errors: null
                    trace: network is required for wallet_address mode
                invalidAmount:
                  summary: Non-positive amount (wallet_address mode only)
                  value:
                    request_id: 123e4567-e89b-12d3-a456-426614174000
                    http_status: 400
                    key: INVALID_REQUEST
                    message: INVALID_REQUEST
                    error: amount must be a positive number
                    errors: null
                    trace: amount must be a positive number
                cardNotReady:
                  summary: Card account not yet provisioned
                  value:
                    request_id: 123e4567-e89b-12d3-a456-426614174000
                    http_status: 400
                    key: INVALID_REQUEST
                    message: INVALID_REQUEST
                    error: card account is not ready
                    errors: null
                    trace: card account is not ready
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '404':
          "$ref": "#/components/responses/NotFound"
        '429':
          "$ref": "#/components/responses/TooManyRequests"
        '500':
          "$ref": "#/components/responses/InternalError"
      description: |-
        Creates a public deposit/top-up record.

        Supported request variants:
        - `hosted_deposit`: creates a hosted payment session and returns a FusionPay `redirect_url` when available.
        - `hosted_deposit` with OTP: call `/deposits/authentication` first, then pass `authentication_code`.
        - `hosted_deposit` with return URLs: pass `success_url` and `failure_url`; FusionPay wraps the hosted URL.
        - `wallet_address`: creates a wallet-address instruction and returns `deposit_address`, `asset`, `network`, and `expires_at`.
        - `wallet_address` token routing: include `smart_contract_address` when the selected chain/token requires it.

        Public contract: only `hosted_deposit` and `wallet_address` are public modes. Internal upstream order/session IDs are not returned.
    get:
      tags:
      - Deposits
      operationId: listDeposits
      summary: List deposits
      parameters:
      - "$ref": "#/components/parameters/CardIDQuery"
      - "$ref": "#/components/parameters/Page"
      - "$ref": "#/components/parameters/Limit"
      responses:
        '200':
          description: Deposits retrieved.
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/DepositListEnvelope"
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '429':
          "$ref": "#/components/responses/TooManyRequests"
        '500':
          "$ref": "#/components/responses/InternalError"
  "/deposits/{deposit_id}":
    get:
      tags:
      - Deposits
      operationId: getDeposit
      summary: Retrieve deposit details
      parameters:
      - "$ref": "#/components/parameters/DepositID"
      responses:
        '200':
          description: Deposit retrieved.
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/DepositEnvelope"
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '404':
          "$ref": "#/components/responses/NotFound"
        '429':
          "$ref": "#/components/responses/TooManyRequests"
        '500':
          "$ref": "#/components/responses/InternalError"
  "/deposit-limits":
    get:
      tags:
      - Deposits
      operationId: getDepositLimit
      summary: Retrieve deposit limits
      description: |-
        Retrieves deposit limits for a user or card.

        Supported request variants:
        - Card scoped: send `card_id`; FusionPay resolves the owning user.
        - User scoped: send `user_id` when a specific card is not available.
        - Currency override: send `currency` as an ISO 4217 numeric code; otherwise the gateway defaults to `840` (USD).
      parameters:
      - name: user_id
        in: query
        required: false
        schema:
          type: string
          format: uuid
      - "$ref": "#/components/parameters/CardIDQuery"
      - name: currency
        in: query
        required: false
        description: ISO 4217 numeric currency code. Defaults to `840` (USD).
        schema:
          type: string
        example: '840'
      responses:
        '200':
          description: Deposit limit retrieved.
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/DepositLimitEnvelope"
        '400':
          description: Validation error. Either `user_id` or `card_id` must be
            supplied.
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorEnvelope"
              examples:
                missingScope:
                  summary: Neither user_id nor card_id supplied
                  value:
                    request_id: 123e4567-e89b-12d3-a456-426614174000
                    http_status: 400
                    key: INVALID_REQUEST
                    message: INVALID_REQUEST
                    error: user_id or card_id is required
                    errors: null
                    trace: user_id or card_id is required
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '404':
          "$ref": "#/components/responses/NotFound"
        '429':
          "$ref": "#/components/responses/TooManyRequests"
        '500':
          "$ref": "#/components/responses/InternalError"
  "/transfers/authentication":
    post:
      tags:
      - Transfers
      operationId: requestTransferAuthentication
      summary: Request transfer OTP
      requestBody:
        required: true
        content:
          application/json:
            schema:
              "$ref": "#/components/schemas/TransferAuthenticationRequest"
            example:
              type: p2p
              user_id: 123e4567-e89b-12d3-a456-426614174000
              from_card_id: 223e4567-e89b-12d3-a456-426614174000
              source: '1'
      responses:
        '200':
          description: Transfer authentication result.
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/AuthenticationEnvelope"
        '400':
          description: Validation error. `type`, `user_id`, and `from_card_id` are
            required; `type` must be one of `p2p`, `alipay`, `wechat`, `bank`;
            `from_card_id` must belong to `user_id`.
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorEnvelope"
              examples:
                missingFields:
                  summary: Missing required fields
                  value:
                    request_id: 123e4567-e89b-12d3-a456-426614174000
                    http_status: 400
                    key: INVALID_REQUEST
                    message: INVALID_REQUEST
                    error: null
                    errors:
                      type: type is required
                      user_i_d: user i d is required
                      from_card_i_d: from card i d is required
                    trace: null
                invalidType:
                  summary: Unsupported transfer type
                  value:
                    request_id: 123e4567-e89b-12d3-a456-426614174000
                    http_status: 400
                    key: INVALID_REQUEST
                    message: INVALID_REQUEST
                    error: null
                    errors:
                      type: Invalid value for type
                    trace: null
                cardOwnershipMismatch:
                  summary: from_card_id does not belong to user_id
                  value:
                    request_id: 123e4567-e89b-12d3-a456-426614174000
                    http_status: 400
                    key: INVALID_REQUEST
                    message: INVALID_REQUEST
                    error: card_id does not belong to user_id
                    errors: null
                    trace: card_id does not belong to user_id
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '404':
          "$ref": "#/components/responses/NotFound"
        '429':
          "$ref": "#/components/responses/TooManyRequests"
        '500':
          "$ref": "#/components/responses/InternalError"
      description: |-
        Requests an authentication challenge for a transfer route.

        Supported request variants by `type`:
        - `p2p`: card-to-card transfer challenge.
        - `alipay`: wallet remittance challenge for Alipay recipients.
        - `wechat`: wallet remittance challenge for WeChat recipients.
        - `bank`: bank-account remittance challenge; `source` defaults to `"1"` when omitted.

        Pass the collected `authentication_code` to `POST /transfers` when required by the returned metadata.

        `from_card_id` is required for request consistency and ownership validation, but the authentication challenge itself is generated at the user level.
  "/transfers/quote":
    post:
      tags:
      - Transfers
      operationId: quoteTransfer
      summary: Quote transfer fees and exchange rates
      requestBody:
        required: true
        content:
          application/json:
            schema:
              "$ref": "#/components/schemas/TransferQuoteRequest"
            examples:
              allFields:
                summary: Complete transfer quote payload template
                value:
                  type: bank
                  user_id: 123e4567-e89b-12d3-a456-426614174000
                  card_id: 223e4567-e89b-12d3-a456-426614174000
                  amount: '250.00'
                  currency: '840'
                  to_currency: '360'
                  purpose: family_support
                  recipient:
                    account_no: recipient-wallet-001
                    account_type: savings
                    first_name: Budi
                    last_name: Santoso
                    country: ID
                    payee_id: 12345
                    country_code: ID
                    payee_name: Budi Santoso
                    payee_address:
                    - 1 Jalan Sudirman
                    - Jakarta
                    bank_account: '1234567890'
                    bank_name: Bank Central Asia
                    swift_code: CENAIDJA
                    bank_routing_code: '014'
                    intermediary_swift_code: BOFAUS3N
                    remark: Family support quote
              p2p:
                summary: Card-to-card quote
                value:
                  type: p2p
                  user_id: 123e4567-e89b-12d3-a456-426614174000
                  amount: '50.00'
                  currency: '840'
              alipay:
                summary: Alipay wallet remittance FX quote
                value:
                  type: alipay
                  user_id: 123e4567-e89b-12d3-a456-426614174000
                  amount: '100.00'
                  currency: '840'
                  to_currency: '156'
              wechat:
                summary: WeChat wallet remittance FX quote
                value:
                  type: wechat
                  user_id: 123e4567-e89b-12d3-a456-426614174000
                  amount: '100.00'
                  currency: '840'
                  to_currency: '156'
              bank:
                summary: Bank remittance fee quote (card_id required)
                value:
                  type: bank
                  user_id: 123e4567-e89b-12d3-a456-426614174000
                  card_id: 223e4567-e89b-12d3-a456-426614174000
                  amount: '250.00'
                  currency: '840'
                  to_currency: '360'
                  purpose: family_support
                  recipient:
                    country_code: ID
                    payee_name: Budi Santoso
                    bank_account: '1234567890'
                    bank_name: Bank Central Asia
                    swift_code: CENAIDJA
      responses:
        '200':
          description: Transfer quote retrieved.
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/TransferQuoteEnvelope"
        '400':
          description: Validation error. `type`, `user_id`, `amount`, and
            `currency` are required; `bank` quotes additionally require
            `recipient` and `card_id`; a supplied `card_id` must belong to
            `user_id`.
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorEnvelope"
              examples:
                missingFields:
                  summary: Missing required fields
                  value:
                    request_id: 123e4567-e89b-12d3-a456-426614174000
                    http_status: 400
                    key: INVALID_REQUEST
                    message: INVALID_REQUEST
                    error: null
                    errors:
                      type: type is required
                      user_i_d: user i d is required
                      amount: amount is required
                      currency: currency is required
                    trace: null
                invalidType:
                  summary: Unsupported transfer type
                  value:
                    request_id: 123e4567-e89b-12d3-a456-426614174000
                    http_status: 400
                    key: INVALID_REQUEST
                    message: INVALID_REQUEST
                    error: null
                    errors:
                      type: Invalid value for type
                    trace: null
                missingBankRecipient:
                  summary: Bank quote without recipient
                  value:
                    request_id: 123e4567-e89b-12d3-a456-426614174000
                    http_status: 400
                    key: INVALID_REQUEST
                    message: INVALID_REQUEST
                    error: recipient is required for bank transfer quote
                    errors: null
                    trace: recipient is required for bank transfer quote
                missingBankCardId:
                  summary: Bank quote without card_id
                  value:
                    request_id: 123e4567-e89b-12d3-a456-426614174000
                    http_status: 400
                    key: INVALID_REQUEST
                    message: INVALID_REQUEST
                    error: card_id is required for bank transfer quote
                    errors: null
                    trace: card_id is required for bank transfer quote
                cardOwnershipMismatch:
                  summary: from_card_id does not belong to user_id
                  value:
                    request_id: 123e4567-e89b-12d3-a456-426614174000
                    http_status: 400
                    key: INVALID_REQUEST
                    message: INVALID_REQUEST
                    error: card_id does not belong to user_id
                    errors: null
                    trace: card_id does not belong to user_id
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '404':
          "$ref": "#/components/responses/NotFound"
        '429':
          "$ref": "#/components/responses/TooManyRequests"
        '500':
          "$ref": "#/components/responses/InternalError"
      description: |-
        Quotes transfer fee, FX, or payee metadata before submission.

        Supported request variants by `type`:
        - `p2p`: local 1:1 quote; no recipient object is required.
        - `alipay`: wallet-remittance FX quote for Alipay recipient flows.
        - `wechat`: wallet-remittance FX quote for WeChat recipient flows.
        - `bank`: bank-remittance fee quote; requires `card_id` and bank recipient data, and may return `payee_id`.

        Use returned `exchange_rate_id` or `payee_id` on `POST /transfers` when present.
  "/transfers":
    post:
      tags:
      - Transfers
      operationId: initiateTransfer
      summary: Initiate transfer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              "$ref": "#/components/schemas/InitiateTransferRequest"
            examples:
              allFields:
                summary: Complete transfer payload template
                value:
                  type: bank
                  user_id: 123e4567-e89b-12d3-a456-426614174000
                  from_card_id: 223e4567-e89b-12d3-a456-426614174000
                  to_card_id: 323e4567-e89b-12d3-a456-426614174000
                  to_account_serial_no: account-serial-001
                  amount: '250.00'
                  currency: '840'
                  to_currency: '360'
                  to_amount: '3967500.00'
                  fee: '1.50'
                  purpose: family_support
                  sub_purpose: monthly_support
                  remark: June family support
                  authentication_code: '123456'
                  exchange_rate_id: fx_8Qw3Zr6Td1Mn
                  customer_reference: INV-2026-0001
                  payment_details: Family support June
                  recipient:
                    account_no: recipient-wallet-001
                    account_type: savings
                    first_name: Budi
                    last_name: Santoso
                    country: ID
                    payee_id: 12345
                    country_code: ID
                    payee_name: Budi Santoso
                    payee_address:
                    - 1 Jalan Sudirman
                    - Jakarta
                    bank_account: '1234567890'
                    bank_name: Bank Central Asia
                    swift_code: CENAIDJA
                    bank_routing_code: '014'
                    intermediary_swift_code: BOFAUS3N
                    remark: Family support transfer
              p2p:
                summary: Card-to-card transfer
                value:
                  type: p2p
                  user_id: 123e4567-e89b-12d3-a456-426614174000
                  from_card_id: 223e4567-e89b-12d3-a456-426614174000
                  to_card_id: 323e4567-e89b-12d3-a456-426614174000
                  amount: '50.00'
                  currency: '840'
              alipay:
                summary: Alipay wallet remittance
                value:
                  type: alipay
                  user_id: 123e4567-e89b-12d3-a456-426614174000
                  from_card_id: 223e4567-e89b-12d3-a456-426614174000
                  amount: '100.00'
                  currency: '840'
                  to_currency: '156'
                  exchange_rate_id: fx_8Qw3Zr6Td1Mn
                  recipient:
                    account_no: 'alipay-user-001'
                    first_name: Wei
                    last_name: Chen
                    country: CN
              wechat:
                summary: WeChat wallet remittance
                value:
                  type: wechat
                  user_id: 123e4567-e89b-12d3-a456-426614174000
                  from_card_id: 223e4567-e89b-12d3-a456-426614174000
                  amount: '100.00'
                  currency: '840'
                  to_currency: '156'
                  exchange_rate_id: fx_8Qw3Zr6Td1Mn
                  recipient:
                    account_no: 'wechat-user-001'
                    first_name: Wei
                    last_name: Chen
                    country: CN
              bank:
                summary: Bank-account remittance
                value:
                  type: bank
                  user_id: 123e4567-e89b-12d3-a456-426614174000
                  from_card_id: 223e4567-e89b-12d3-a456-426614174000
                  amount: '250.00'
                  currency: '840'
                  to_currency: '360'
                  purpose: family_support
                  customer_reference: INV-2026-0001
                  payment_details: Family support June
                  recipient:
                    country_code: ID
                    payee_name: Budi Santoso
                    bank_account: '1234567890'
                    bank_name: Bank Central Asia
                    swift_code: CENAIDJA
      responses:
        '200':
          description: Transfer initiated.
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/TransferEnvelope"
        '400':
          description: Validation error. `type`, `user_id`, `from_card_id`,
            `amount`, and `currency` are required. `p2p` requires `to_card_id` or
            `to_account_serial_no`; `alipay`, `wechat`, and `bank` require
            `recipient` (message template `recipient is required for {type}
            transfer`). `from_card_id` must belong to `user_id` and its card
            account must be provisioned (ready).
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorEnvelope"
              examples:
                missingFields:
                  summary: Missing required fields
                  value:
                    request_id: 123e4567-e89b-12d3-a456-426614174000
                    http_status: 400
                    key: INVALID_REQUEST
                    message: INVALID_REQUEST
                    error: null
                    errors:
                      type: type is required
                      user_i_d: user i d is required
                      from_card_i_d: from card i d is required
                      amount: amount is required
                      currency: currency is required
                    trace: null
                missingP2PDestination:
                  summary: p2p without destination
                  value:
                    request_id: 123e4567-e89b-12d3-a456-426614174000
                    http_status: 400
                    key: INVALID_REQUEST
                    message: INVALID_REQUEST
                    error: to_card_id or to_account_serial_no is required for p2p transfer
                    errors: null
                    trace: to_card_id or to_account_serial_no is required for p2p transfer
                missingRecipient:
                  summary: Remittance without recipient (alipay shown; wechat/bank analogous)
                  value:
                    request_id: 123e4567-e89b-12d3-a456-426614174000
                    http_status: 400
                    key: INVALID_REQUEST
                    message: INVALID_REQUEST
                    error: recipient is required for alipay transfer
                    errors: null
                    trace: recipient is required for alipay transfer
                cardOwnershipMismatch:
                  summary: from_card_id does not belong to user_id
                  value:
                    request_id: 123e4567-e89b-12d3-a456-426614174000
                    http_status: 400
                    key: INVALID_REQUEST
                    message: INVALID_REQUEST
                    error: card_id does not belong to user_id
                    errors: null
                    trace: card_id does not belong to user_id
                fromCardNotReady:
                  summary: Source card not yet provisioned
                  value:
                    request_id: 123e4567-e89b-12d3-a456-426614174000
                    http_status: 400
                    key: INVALID_REQUEST
                    message: INVALID_REQUEST
                    error: from card account is not ready
                    errors: null
                    trace: from card account is not ready
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '404':
          "$ref": "#/components/responses/NotFound"
        '429':
          "$ref": "#/components/responses/TooManyRequests"
        '500':
          "$ref": "#/components/responses/InternalError"
      description: |-
        Initiates a transfer or remittance.

        Supported request variants by `type`:
        - `p2p` to public card: use `to_card_id`.
        - `p2p` to controlled account reference: use `to_account_serial_no` only when FusionPay operations has supplied that destination reference.
        - `alipay`: include recipient identity/wallet fields and optional `exchange_rate_id` from quote.
        - `wechat`: include recipient identity/wallet fields and optional `exchange_rate_id` from quote.
        - `bank`: include bank-account recipient data, `customer_reference`, `payment_details`, and optional `payee_id` from quote.

        Public contract: the response returns `transfer_id`, public status, amount, fee, FX fields, and timestamps. Upstream transaction IDs are not returned.
    get:
      tags:
      - Transfers
      operationId: listTransfers
      summary: List transfers
      parameters:
      - "$ref": "#/components/parameters/CardIDQuery"
      - "$ref": "#/components/parameters/Page"
      - "$ref": "#/components/parameters/Limit"
      responses:
        '200':
          description: Transfers retrieved.
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/TransferListEnvelope"
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '429':
          "$ref": "#/components/responses/TooManyRequests"
        '500':
          "$ref": "#/components/responses/InternalError"
  "/transfers/{transfer_id}":
    get:
      tags:
      - Transfers
      operationId: getTransfer
      summary: Retrieve transfer details
      parameters:
      - "$ref": "#/components/parameters/TransferID"
      responses:
        '200':
          description: Transfer retrieved.
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/TransferEnvelope"
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '404':
          "$ref": "#/components/responses/NotFound"
        '429':
          "$ref": "#/components/responses/TooManyRequests"
        '500':
          "$ref": "#/components/responses/InternalError"
  "/webhooks/subscriptions":
    post:
      tags:
      - Webhooks
      operationId: createWebhookSubscription
      summary: Create outbound webhook subscription
      requestBody:
        required: true
        content:
          application/json:
            schema:
              "$ref": "#/components/schemas/CreateWebhookSubscriptionRequest"
            example:
              url: https://partner.example.com/webhooks/fusionpay
              secret: whsec_partner_supplied_secret
              events:
              - kyc.approved
              - deposit.succeeded
              - transfer.succeeded
      responses:
        '200':
          description: Webhook subscription created. The response includes the HMAC
            signing `secret` — this is the only time it is returned.
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/WebhookSubscriptionCreatedEnvelope"
        '400':
          description: Validation error. `url` (valid URL) and at least one entry
            in `events` are required; every event must be a supported filter.
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorEnvelope"
              examples:
                missingFields:
                  summary: Missing url and events
                  value:
                    request_id: 123e4567-e89b-12d3-a456-426614174000
                    http_status: 400
                    key: INVALID_REQUEST
                    message: INVALID_REQUEST
                    error: null
                    errors:
                      u_r_l: u r l is required
                      events: events is required
                    trace: null
                invalidUrl:
                  summary: url is not a valid URL
                  value:
                    request_id: 123e4567-e89b-12d3-a456-426614174000
                    http_status: 400
                    key: INVALID_REQUEST
                    message: INVALID_REQUEST
                    error: null
                    errors:
                      u_r_l: Invalid value for u_r_l
                    trace: null
                unsupportedEvent:
                  summary: Unsupported event filter
                  value:
                    request_id: 123e4567-e89b-12d3-a456-426614174000
                    http_status: 400
                    key: INVALID_REQUEST
                    message: INVALID_REQUEST
                    error: 'unsupported webhook event: card.created'
                    errors: null
                    trace: 'unsupported webhook event: card.created'
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '429':
          "$ref": "#/components/responses/TooManyRequests"
        '500':
          "$ref": "#/components/responses/InternalError"
      description: |-
        Creates an outbound webhook subscription for public API Solutions events.

        Supported event filters: `kyc.initiated`, `kyc.reviewing`, `kyc.approved`, `kyc.rejected`, `kyc.*`, `deposit.pending`, `deposit.succeeded`, `deposit.failed`, `deposit.*`, `transfer.pending`, `transfer.succeeded`, `transfer.failed`, and `transfer.*`.

        KYC event payloads use the public KYC response shape. `kyc.initiated` may include a FusionPay `redirect_url`; later status events omit redirect URLs.

        If `secret` is omitted, FusionPay generates one. The secret is returned only in this creation response (`result.secret`) and cannot be retrieved later — store it securely because it is used for HMAC signature verification. To replace a lost secret, rotate it via `PATCH /webhooks/subscriptions/{subscription_id}`.

        KYC status events can be emitted by the internal callback path or by `GET /users/{user_id}/kyc` when that read refresh detects a newer public KYC status.
    get:
      tags:
      - Webhooks
      operationId: listWebhookSubscriptions
      summary: List outbound webhook subscriptions
      responses:
        '200':
          description: Webhook subscriptions retrieved.
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/WebhookSubscriptionListEnvelope"
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '429':
          "$ref": "#/components/responses/TooManyRequests"
        '500':
          "$ref": "#/components/responses/InternalError"
      description: |-
        Lists outbound webhook subscriptions for the authenticated client.

        Use this for partner operations tooling to confirm URL, status, event filters, and delivery configuration.
  "/webhooks/subscriptions/{subscription_id}":
    patch:
      tags:
      - Webhooks
      operationId: updateWebhookSubscription
      summary: Update outbound webhook subscription
      parameters:
      - "$ref": "#/components/parameters/SubscriptionID"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              "$ref": "#/components/schemas/UpdateWebhookSubscriptionRequest"
            example:
              url: https://partner.example.com/webhooks/fusionpay
              secret: whsec_rotated_partner_secret
              events:
              - kyc.*
              - deposit.*
              - transfer.*
              status: active
      responses:
        '200':
          description: Webhook subscription updated.
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/WebhookSubscriptionEnvelope"
        '400':
          description: Validation error. When supplied, `url` must be a valid URL,
            `status` must be `active` or `disabled`, and every event must be a
            supported filter.
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorEnvelope"
              examples:
                invalidStatus:
                  summary: Unsupported status value
                  value:
                    request_id: 123e4567-e89b-12d3-a456-426614174000
                    http_status: 400
                    key: INVALID_REQUEST
                    message: INVALID_REQUEST
                    error: null
                    errors:
                      status: Invalid value for status
                    trace: null
                unsupportedEvent:
                  summary: Unsupported event filter
                  value:
                    request_id: 123e4567-e89b-12d3-a456-426614174000
                    http_status: 400
                    key: INVALID_REQUEST
                    message: INVALID_REQUEST
                    error: 'unsupported webhook event: card.created'
                    errors: null
                    trace: 'unsupported webhook event: card.created'
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '404':
          "$ref": "#/components/responses/NotFound"
        '429':
          "$ref": "#/components/responses/TooManyRequests"
        '500':
          "$ref": "#/components/responses/InternalError"
      description: |-
        Updates an outbound webhook subscription.

        Supported request variants:
        - URL rotation: send `url`.
        - Secret rotation: send `secret`.
        - Event filter update: send `events`, including KYC, deposit, and transfer filters.
        - Pause/resume delivery: send `status` as `active` or `disabled`.
components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-FusionPay-Api-Key
      description: API Solutions client key generated through admin onboarding.
  parameters:
    UserID:
      name: user_id
      in: path
      required: true
      schema:
        type: string
        format: uuid
    CardID:
      name: card_id
      in: path
      required: true
      schema:
        type: string
        format: uuid
    CardIDQuery:
      name: card_id
      in: query
      required: false
      schema:
        type: string
        format: uuid
    DepositID:
      name: deposit_id
      in: path
      required: true
      schema:
        type: string
        format: uuid
    TransferID:
      name: transfer_id
      in: path
      required: true
      schema:
        type: string
        format: uuid
    SubscriptionID:
      name: subscription_id
      in: path
      required: true
      schema:
        type: string
        format: uuid
    Page:
      name: page
      in: query
      required: false
      schema:
        type: integer
        minimum: 1
        default: 1
    Limit:
      name: limit
      in: query
      required: false
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 20
  responses:
    Unauthorized:
      description: Missing or invalid API key.
      content:
        application/json:
          schema:
            "$ref": "#/components/schemas/ErrorEnvelope"
          example:
            request_id: 123e4567-e89b-12d3-a456-426614174000
            http_status: 401
            key: UNAUTHORIZED
            message: UNAUTHORIZED
            error: Invalid API key
            errors: null
            trace: Invalid API key
    NotFound:
      description: Resource not found in the authenticated client scope.
      content:
        application/json:
          schema:
            "$ref": "#/components/schemas/ErrorEnvelope"
          example:
            request_id: 123e4567-e89b-12d3-a456-426614174000
            http_status: 404
            key: NOT_FOUND
            message: NOT_FOUND
            error: resource not found
            errors: null
            trace: resource not found
    TooManyRequests:
      description: Rate limit or usage quota exceeded.
      headers:
        Retry-After:
          description: Seconds until the client may retry, present for rate-limit
            rejections.
          schema:
            type: string
        X-RateLimit-Limit:
          description: Requests allowed per minute.
          schema:
            type: string
        X-RateLimit-Remaining:
          description: Remaining requests in the current rate-limit window.
          schema:
            type: string
        X-RateLimit-Reset:
          description: Unix timestamp when the current rate-limit window resets.
          schema:
            type: string
      content:
        application/json:
          schema:
            "$ref": "#/components/schemas/ErrorEnvelope"
          example:
            request_id: 123e4567-e89b-12d3-a456-426614174000
            http_status: 429
            key: RATE_LIMIT_EXCEEDED
            message: RATE_LIMIT_EXCEEDED
            error: Rate limit exceeded. Retry after the window resets.
            errors: null
            trace: Rate limit exceeded. Retry after the window resets.
    InternalError:
      description: Internal or upstream service error.
      content:
        application/json:
          schema:
            "$ref": "#/components/schemas/ErrorEnvelope"
          example:
            request_id: 123e4567-e89b-12d3-a456-426614174000
            http_status: 500
            key: INTERNAL_ERROR
            message: INTERNAL_ERROR
            error: unexpected internal error
            errors: null
            trace: unexpected internal error
  schemas:
    SuccessEnvelopeBase:
      type: object
      required:
      - http_status
      - key
      - message
      properties:
        request_id:
          type: string
          format: uuid
        http_status:
          type: integer
          example: 200
        key:
          type: string
          example: success
        message:
          type: string
    ErrorEnvelope:
      type: object
      required:
      - http_status
      - key
      - message
      properties:
        request_id:
          type: string
          format: uuid
        http_status:
          type: integer
          example: 400
        key:
          type: string
          example: INVALID_REQUEST
        message:
          type: string
          example: INVALID_REQUEST
        error:
          nullable: true
          description: Human-readable error detail when available.
        errors:
          type: object
          nullable: true
          additionalProperties:
            type: string
          description: Field-level validation errors.
        trace:
          nullable: true
          description: Present only in local, development, or staging environments.
    TokenCheckEnvelope:
      allOf:
      - "$ref": "#/components/schemas/SuccessEnvelopeBase"
      - type: object
        properties:
          result:
            "$ref": "#/components/schemas/TokenCheckResponse"
    EndUserEnvelope:
      allOf:
      - "$ref": "#/components/schemas/SuccessEnvelopeBase"
      - type: object
        properties:
          result:
            "$ref": "#/components/schemas/EndUserResponse"
    KYCEnvelope:
      allOf:
      - "$ref": "#/components/schemas/SuccessEnvelopeBase"
      - type: object
        properties:
          result:
            "$ref": "#/components/schemas/KYCResponse"
    CardEnvelope:
      allOf:
      - "$ref": "#/components/schemas/SuccessEnvelopeBase"
      - type: object
        properties:
          result:
            "$ref": "#/components/schemas/CardResponse"
    CardApplicationEnvelope:
      allOf:
      - "$ref": "#/components/schemas/SuccessEnvelopeBase"
      - type: object
        properties:
          result:
            "$ref": "#/components/schemas/CardApplicationResponse"
    PINResetLinkEnvelope:
      allOf:
      - "$ref": "#/components/schemas/SuccessEnvelopeBase"
      - type: object
        properties:
          result:
            "$ref": "#/components/schemas/PINResetLinkResponse"
    AuthenticationEnvelope:
      allOf:
      - "$ref": "#/components/schemas/SuccessEnvelopeBase"
      - type: object
        properties:
          result:
            "$ref": "#/components/schemas/AuthenticationResponse"
    DepositEnvelope:
      allOf:
      - "$ref": "#/components/schemas/SuccessEnvelopeBase"
      - type: object
        properties:
          result:
            "$ref": "#/components/schemas/DepositResponse"
    DepositLimitEnvelope:
      allOf:
      - "$ref": "#/components/schemas/SuccessEnvelopeBase"
      - type: object
        properties:
          result:
            "$ref": "#/components/schemas/DepositLimitResponse"
    TransferQuoteEnvelope:
      allOf:
      - "$ref": "#/components/schemas/SuccessEnvelopeBase"
      - type: object
        properties:
          result:
            "$ref": "#/components/schemas/TransferQuoteResponse"
    TransferEnvelope:
      allOf:
      - "$ref": "#/components/schemas/SuccessEnvelopeBase"
      - type: object
        properties:
          result:
            "$ref": "#/components/schemas/TransferResponse"
    WebhookSubscriptionEnvelope:
      allOf:
      - "$ref": "#/components/schemas/SuccessEnvelopeBase"
      - type: object
        properties:
          result:
            "$ref": "#/components/schemas/WebhookSubscriptionResponse"
    WebhookSubscriptionCreatedEnvelope:
      allOf:
      - "$ref": "#/components/schemas/SuccessEnvelopeBase"
      - type: object
        properties:
          result:
            "$ref": "#/components/schemas/WebhookSubscriptionCreatedResponse"
    DepositListEnvelope:
      allOf:
      - "$ref": "#/components/schemas/SuccessEnvelopeBase"
      - type: object
        properties:
          result:
            "$ref": "#/components/schemas/DepositListResponse"
    TransferListEnvelope:
      allOf:
      - "$ref": "#/components/schemas/SuccessEnvelopeBase"
      - type: object
        properties:
          result:
            "$ref": "#/components/schemas/TransferListResponse"
    CardTransactionListEnvelope:
      allOf:
      - "$ref": "#/components/schemas/SuccessEnvelopeBase"
      - type: object
        properties:
          result:
            "$ref": "#/components/schemas/CardTransactionListResponse"
    WebhookSubscriptionListEnvelope:
      allOf:
      - "$ref": "#/components/schemas/SuccessEnvelopeBase"
      - type: object
        properties:
          result:
            type: array
            items:
              "$ref": "#/components/schemas/WebhookSubscriptionResponse"
    TokenCheckResponse:
      type: object
      required:
      - status
      - client_id
      - client_name
      - product
      - message
      properties:
        status:
          type: string
          example: ok
        client_id:
          type: string
          format: uuid
        client_name:
          type: string
        product:
          type: string
          example: API_SOLUTIONS
        message:
          type: string
          example: FusionPay API Solutions authentication succeeded
    CreateEndUserRequest:
      type: object
      required:
      - external_user_id
      description: At least one of `email` or `mobile` must be supplied.
      properties:
        external_user_id:
          type: string
          description: Partner-owned user identifier, unique per authenticated client.
          example: partner-user-001
        email:
          type: string
          example: partner.user@example.com
        mobile:
          type: string
          description: Mobile in `+{countryCode} {number}` format — country code
            (1–3 digits) and number (4–15 digits) separated by a single space.
          pattern: '^\+\d{1,3} \d{4,15}$'
          example: "+62 81234567890"
    EndUserResponse:
      type: object
      required:
      - user_id
      - external_user_id
      - kyc_status
      - created_at
      - updated_at
      properties:
        user_id:
          type: string
          format: uuid
        external_user_id:
          type: string
        kyc_status:
          "$ref": "#/components/schemas/KYCStatus"
        email:
          type: string
          format: email
        mobile:
          type: string
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    InitiateKYCRequest:
      type: object
      required:
      - success_url
      properties:
        type:
          type: string
          enum:
          - SINGPASS
          - REGTANK
          description: Required on first initiation unless already stored for the
            user.
        locale:
          type: string
          description: Canonical locale values are `en_US` and `zh_CN`; aliases `en-US`,
            `zh-CN`, `en`, and `zh` are normalized.
          example: en_US
        success_url:
          type: string
          format: uri
        error_url:
          type: string
          format: uri
    KYCResponse:
      type: object
      required:
      - user_id
      - kyc_status
      properties:
        user_id:
          type: string
          format: uuid
        kyc_status:
          "$ref": "#/components/schemas/KYCStatus"
        type:
          type: string
          enum:
          - SINGPASS
          - REGTANK
        redirect_url:
          type: string
          format: uri
        transaction_reference:
          type: string
        reason:
          type: string
    MailingAddressRequest:
      type: object
      properties:
        street_name:
          type: string
        city:
          type: string
        neighborhood:
          type: string
        state:
          type: string
        country_code:
          type: string
        zip_code:
          type: string
    IssueVirtualCardRequest:
      type: object
      required:
      - user_id
      properties:
        user_id:
          type: string
          format: uuid
        is_singaporean:
          type: boolean
          default: false
        card_name:
          type: string
        mailing_address:
          "$ref": "#/components/schemas/MailingAddressRequest"
    IssuePhysicalCardRequest:
      type: object
      required:
      - user_id
      - mailing_address
      properties:
        user_id:
          type: string
          format: uuid
        is_singaporean:
          type: boolean
          default: false
        card_name:
          type: string
        mailing_address:
          "$ref": "#/components/schemas/MailingAddressRequest"
    CardResponse:
      type: object
      required:
      - card_id
      - user_id
      - type
      - status
      - created_at
      - updated_at
      properties:
        card_id:
          type: string
          format: uuid
        user_id:
          type: string
          format: uuid
        type:
          type: string
          enum:
          - virtual
          - physical
        status:
          "$ref": "#/components/schemas/CardStatus"
        application_status:
          type: string
        delivery_status:
          type: string
          enum:
          - unknown
          - pending
          - in_transit
          - delivered
          - failed
        masked_pan:
          type: string
          description: Masked PAN only. Raw PAN is never returned.
        last4:
          type: string
        cvv:
          type: string
          nullable: true
          description: Masked CVV only (for example `***`). Raw CVV is never returned;
            the field may be null or masked for virtual cards.
        expiry:
          type: string
          description: Card expiry in `MM/YY` display format. Returned for both virtual
            and physical cards.
          example: '12/30'
        currency:
          type: string
        balance:
          type: string
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    CardApplicationResponse:
      type: object
      required:
      - card_id
      - user_id
      - application_status
      properties:
        card_id:
          type: string
          format: uuid
        user_id:
          type: string
          format: uuid
        application_status:
          type: string
    PINResetLinkRequest:
      type: object
      properties:
        success_url:
          type: string
          format: uri
        failure_url:
          type: string
          format: uri
    PINResetLinkResponse:
      type: object
      required:
      - redirect_url
      properties:
        redirect_url:
          type: string
          format: uri
        transaction_reference:
          type: string
        expire_time:
          type: string
    DepositAuthenticationRequest:
      type: object
      required:
      - user_id
      properties:
        user_id:
          type: string
          format: uuid
        card_id:
          type: string
          format: uuid
    TransferAuthenticationRequest:
      type: object
      required:
      - type
      - user_id
      - from_card_id
      properties:
        type:
          "$ref": "#/components/schemas/TransferType"
        user_id:
          type: string
          format: uuid
        from_card_id:
          type: string
          format: uuid
        source:
          type: string
    AuthenticationResponse:
      type: object
      required:
      - required
      properties:
        required:
          type: boolean
        type:
          type: string
        send_to:
          type: string
        expire_time:
          type: string
    CreateDepositRequest:
      allOf:
      - "$ref": "#/components/schemas/CreateCardTopUpRequest"
      - type: object
        required:
        - card_id
        properties:
          card_id:
            type: string
            format: uuid
    CreateCardTopUpRequest:
      type: object
      required:
      - mode
      - amount
      - currency
      properties:
        user_id:
          type: string
          format: uuid
          description: Optional cross-check that the card belongs to this user.
        mode:
          type: string
          enum:
          - hosted_deposit
          - wallet_address
          description: Public deposit mode. Use `hosted_deposit` for hosted payment
            sessions or `wallet_address` for crypto wallet-address instructions.
        amount:
          type: string
          example: '100.00'
        currency:
          type: string
          example: '840'
        from_currency:
          type: string
        channel_code:
          type: integer
        deposit_country:
          type: string
        authentication_code:
          type: string
        payment_method:
          type: array
          items:
            type: string
        success_url:
          type: string
          format: uri
          description: Partner return URL after hosted flow success. FusionPay wraps
            the upstream hosted URL and returns a public redirect URL.
        failure_url:
          type: string
          format: uri
          description: Partner return URL after hosted flow failure. FusionPay wraps
            the upstream hosted URL and returns a public redirect URL.
        asset:
          type: string
          description: Crypto asset for `wallet_address`, for example `USDT` or `USDC`.
        network:
          type: string
          description: Required for `wallet_address`.
        smart_contract_address:
          type: string
    DepositResponse:
      type: object
      required:
      - deposit_id
      - user_id
      - card_id
      - mode
      - status
      - amount
      - currency
      - created_at
      - updated_at
      properties:
        deposit_id:
          type: string
          format: uuid
        user_id:
          type: string
          format: uuid
        card_id:
          type: string
          format: uuid
        mode:
          type: string
          enum:
          - hosted_deposit
          - wallet_address
        status:
          "$ref": "#/components/schemas/ResourceStatus"
        amount:
          type: string
        currency:
          type: string
        from_currency:
          type: string
        fee:
          type: string
        asset:
          type: string
        network:
          type: string
        deposit_address:
          type: string
        redirect_url:
          type: string
          format: uri
        expires_at:
          type: string
          format: date-time
          nullable: true
        completed_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    DepositLimitResponse:
      type: object
      required:
      - user_id
      - currency
      - minimum
      - maximum
      properties:
        user_id:
          type: string
          format: uuid
        card_id:
          type: string
          format: uuid
        currency:
          type: string
        minimum:
          type: string
        maximum:
          type: string
    DepositListResponse:
      type: object
      required:
      - items
      - total
      - page
      - limit
      properties:
        items:
          type: array
          items:
            "$ref": "#/components/schemas/DepositResponse"
        total:
          type: integer
          format: int64
        page:
          type: integer
        limit:
          type: integer
    TransferRecipientRequest:
      type: object
      properties:
        account_no:
          type: string
          description: P2P recipient account number.
        account_type:
          type: string
        first_name:
          type: string
        last_name:
          type: string
        country:
          type: string
        payee_id:
          type: integer
          format: int64
          description: Gateway payee reference returned by a bank transfer quote.
            Optional when submitting complete inline recipient data.
        country_code:
          type: string
        payee_name:
          type: string
        payee_address:
          type: array
          items:
            type: string
        bank_account:
          type: string
        bank_name:
          type: string
        swift_code:
          type: string
        bank_routing_code:
          type: string
        intermediary_swift_code:
          type: string
        remark:
          type: string
    TransferQuoteRequest:
      type: object
      required:
      - type
      - user_id
      - amount
      - currency
      properties:
        type:
          "$ref": "#/components/schemas/TransferType"
        user_id:
          type: string
          format: uuid
        card_id:
          type: string
          format: uuid
          description: Required for bank transfer quote.
        amount:
          type: string
        currency:
          type: string
        to_currency:
          type: string
        purpose:
          type: string
        recipient:
          "$ref": "#/components/schemas/TransferRecipientRequest"
    TransferQuoteResponse:
      type: object
      required:
      - type
      - amount
      - currency
      properties:
        type:
          "$ref": "#/components/schemas/TransferType"
        amount:
          type: string
        currency:
          type: string
        to_amount:
          type: string
        to_currency:
          type: string
        fee:
          type: string
        exchange_rate:
          type: string
        exchange_rate_id:
          type: string
        expires_at:
          type: string
        payee_id:
          type: integer
          format: int64
    InitiateTransferRequest:
      type: object
      required:
      - type
      - user_id
      - from_card_id
      - amount
      - currency
      properties:
        type:
          "$ref": "#/components/schemas/TransferType"
        user_id:
          type: string
          format: uuid
        from_card_id:
          type: string
          format: uuid
        to_card_id:
          type: string
          format: uuid
        to_account_serial_no:
          type: string
          description: Controlled P2P destination account reference. Prefer `to_card_id`
            when the receiver is represented by API Solutions.
        amount:
          type: string
        currency:
          type: string
        to_currency:
          type: string
        to_amount:
          type: string
        fee:
          type: string
        purpose:
          type: string
        sub_purpose:
          type: string
        remark:
          type: string
        authentication_code:
          type: string
        exchange_rate_id:
          type: string
        customer_reference:
          type: string
        payment_details:
          type: string
        recipient:
          "$ref": "#/components/schemas/TransferRecipientRequest"
    TransferResponse:
      type: object
      required:
      - transfer_id
      - type
      - status
      - user_id
      - from_card_id
      - amount
      - currency
      - created_at
      - updated_at
      properties:
        transfer_id:
          type: string
          format: uuid
        type:
          "$ref": "#/components/schemas/TransferType"
        status:
          "$ref": "#/components/schemas/ResourceStatus"
        user_id:
          type: string
          format: uuid
        from_card_id:
          type: string
          format: uuid
        to_card_id:
          type: string
          format: uuid
          nullable: true
        amount:
          type: string
        currency:
          type: string
        to_amount:
          type: string
        to_currency:
          type: string
        fee:
          type: string
        exchange_rate:
          type: string
        exchange_rate_id:
          type: string
        completed_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    TransferListResponse:
      type: object
      required:
      - items
      - total
      - page
      - limit
      properties:
        items:
          type: array
          items:
            "$ref": "#/components/schemas/TransferResponse"
        total:
          type: integer
          format: int64
        page:
          type: integer
        limit:
          type: integer
    CardTransactionResponse:
      type: object
      required:
      - id
      - type
      - status
      - amount
      - currency
      - created_at
      properties:
        id:
          type: string
          format: uuid
        type:
          type: string
          enum:
          - deposit
          - transfer
          description: Public transaction item type. `deposit` covers top-ups; `transfer`
            covers p2p, alipay, wechat, and bank transfer records.
        status:
          "$ref": "#/components/schemas/ResourceStatus"
        amount:
          type: string
        currency:
          type: string
        fee:
          type: string
        created_at:
          type: string
          format: date-time
    CardTransactionListResponse:
      type: object
      required:
      - items
      - total
      - page
      - limit
      properties:
        items:
          type: array
          items:
            "$ref": "#/components/schemas/CardTransactionResponse"
        total:
          type: integer
          format: int64
        page:
          type: integer
        limit:
          type: integer
    CreateWebhookSubscriptionRequest:
      type: object
      required:
      - url
      - events
      properties:
        url:
          type: string
          format: uri
        secret:
          type: string
          description: Optional signing secret. When omitted, the gateway generates
            one.
        events:
          type: array
          minItems: 1
          items:
            "$ref": "#/components/schemas/WebhookEvent"
    UpdateWebhookSubscriptionRequest:
      type: object
      properties:
        url:
          type: string
          format: uri
        secret:
          type: string
        events:
          type: array
          items:
            "$ref": "#/components/schemas/WebhookEvent"
        status:
          type: string
          enum:
          - active
          - disabled
    WebhookSubscriptionResponse:
      type: object
      required:
      - subscription_id
      - url
      - events
      - status
      - created_at
      - updated_at
      properties:
        subscription_id:
          type: string
          format: uuid
        url:
          type: string
          format: uri
        events:
          type: array
          items:
            "$ref": "#/components/schemas/WebhookEvent"
        status:
          type: string
          enum:
          - active
          - disabled
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    WebhookSubscriptionCreatedResponse:
      allOf:
      - "$ref": "#/components/schemas/WebhookSubscriptionResponse"
      - type: object
        required:
        - secret
        properties:
          secret:
            type: string
            description: HMAC signing secret (partner-supplied or gateway-generated).
              Returned only on creation; it cannot be retrieved afterwards. Store
              it securely.
            example: whsec_mock_5Kp2Vn8Qd4Xz7Rt1
    KYCStatus:
      type: string
      enum:
      - not_yet
      - initiated
      - reviewing
      - approved
      - rejected
    CardStatus:
      type: string
      enum:
      - pending
      - active
      - locked
      - closed
      - failed
    ResourceStatus:
      type: string
      enum:
      - pending
      - succeeded
      - failed
      - cancelled
    TransferType:
      type: string
      enum:
      - p2p
      - alipay
      - wechat
      - bank
      description: "`p2p` is card-to-card. `alipay` and `wechat` are wallet remittance
        routes. `bank` is bank-account remittance."
    WebhookEvent:
      type: string
      enum:
      - kyc.initiated
      - kyc.reviewing
      - kyc.approved
      - kyc.rejected
      - kyc.*
      - deposit.pending
      - deposit.succeeded
      - deposit.failed
      - deposit.*
      - transfer.pending
      - transfer.succeeded
      - transfer.failed
      - transfer.*
      description: Supported partner outbound webhook event filter. Wildcards are
        `kyc.*`, `deposit.*`, and `transfer.*`.
