Developers

API Reference

Wallet & Swap Open API

Wallet accounts, balances, transfers, OTC swap, RAA, and stablecoin operations.

1 Introduction

1.1 Overview

The DTC Wallet OpenAPI, a JSON RESTful web service API, is for third-party partners to integrate DTC Wallet solutions with their own business website or services.

This specification defines a set of interfaces and security standards.

1.2 Specification Titles

  • Name: The JSON field name in the request or response.

  • Type: The JSON type of the field.

    • String, Integer, Long, Decimal (a JSON number, not a string — see the note below), Boolean, Enum (an integer ID unless stated otherwise), Object, Array.
  • Required:

    • M — Mandatory. Validated by the server.
    • C — Conditional. Mandatory only under the condition stated in the Description column.
    • O — Optional.
  • Description: The meaning, allowed values, or format of the field.

NOTE (numeric fields): Every amount/balance/fee/rate field in this document (amount, balance, fee, rate, gasFee, transactionFee, sellAmount, buyAmount, etc.) is a JSON number, both in requests and responses — e.g. "amount": 1.25, never "amount": "1.25". All JSON samples in this document follow that rule.

1.3 Security Standards

  • HTTPS: TLS 1.2, mandatory for every connection.
  • Whitelist: Optional IP whitelist, configurable on the API Key Management page.
  • OAuth 2.0: Request session management.
  • Signature: Request integrity checking.

2 Quick Start

2.1 Disclaimer

Use of these APIs is subject to dtcpay's review and approval. dtcpay reserves the right, at its sole discretion, to approve, restrict, suspend, or revoke access at any time.

2.2 Before Integration

  1. Request account registration on the DTC Wallet platform.
  2. Log in with the username/password issued by the DTC team and go to the API Key Management page.
  3. Click + Create to open the dialog, and choose Wallet / Card API (not Payments API — that's a separate credential for the Payment Open API).
  4. Enter the Name and IP Whitelist, then submit.
  5. The API Key, API Secret, and Sign Key are generated and shown once. Keep them safe before closing the dialog.
  6. Request domain names — see Fetch Access Token below.

2.3 OAuth 2.0 Authentication

2.3.1 Usage

Fetch an access token first, then add Authorization: Bearer {access_token} to every other request.

If the token expires, re-fetch it, or use Fetch Access Token by Refresh Token.

NOTE: Only one access token can exist at a time per client_id. Cache it (e.g. in Redis) rather than fetching a fresh one per request.

2.3.2 Fetch Access Token

Host

  • Sbx test environment: https://open-api.sbx.dtcpayment.net
  • Production environment: https://open-api.dtcpay.com

Request Body Sample

curl -X "POST" "https://{host}/auth/v1/oauth2/token" \
     -H "Content-Type: multipart/form-data; charset=utf-8; boundary=__X_PAW_BOUNDARY__" \
     -F "client_id=xxx" \
     -F "client_secret=xxx" \
     -F "grant_type=client_credentials"

The client_id/client_secret are obtained via the web portal at https://business.dtcpay.com/login — navigate to the API menu and create a Wallet / Card API to generate the credentials.

Response Body Sample

{
  "access_token": "CkppLkyiqEUKITGdtCZRUZBl",
  "expires_in": 21600,
  "refresh_token": "CpCAhcCRtLU4kPjs54omWW3A",
  "rt_expires_in": 43200,
  "token_type": "bearer"
}

NOTE: Access denied (errCode 00006) usually means a client_id/client_secret mismatch, or the calling IP does not match the whitelist configured for the API key.

2.3.3 Fetch Access Token by Refresh Token

Request Body Sample

curl -X "POST" "https://{host}/auth/v1/oauth2/token" \
     -H "Content-Type: multipart/form-data; charset=utf-8; boundary=__X_PAW_BOUNDARY__" \
     -F "refresh_token=CpCAhcCRtLU4kPjs54omWW3A" \
     -F "grant_type=refresh_token"

Response Body Sample — same shape as Fetch Access Token, with fresh access_token/refresh_token.

2.4 Request Signature

For every request, a signature is mandatory in the HTTP header — this is enforced unconditionally for every CLIENT-token-authenticated call (WalletOauthInterceptor.checkSignature), not just a "sensitive" subset.

  1. Components of the string to sign:

    • HTTP Method — uppercase.
    • Timestamp — Timestamp number in millis, Singapore timezone. The request will be failed if the timestamp is before or after 2 minutes.
    • URL — the request path, no scheme/host.
    • Querystring — in-URL parameters without the leading ?, un-encoded.
    • Request Body — the exact raw body string, empty if none. Must match byte-for-byte, including invisible characters.
  2. Concatenate in order: Method + Timestamp + URL + ('?' + Querystring if present) + Body.

GET1636360576641/wallet/v1/otc/reference-number/REF123?
POST1636360661729/wallet/v1/otc/get-otc-rate{"query":{"buyCurrency":"USD","sellCurrency":"USDC"}}
  1. Sign the string with HMAC-SHA512 using the Sign Key from Before Integration, then Base64-encode the result.

  2. Attach headers to the request:

    • D-TIMESTAMP — Timestamp number in millis, Singapore timezone. The request will be failed if the timestamp is before or after 2 minutes.
    • D-SIGNATURE — the generated signature.
    • D-SUB-ACCOUNT-ID — the id of sub account, registered under the master account. If a user has not been created through the dtc ekyc open API, this parameter does not need to be passed. This parameter can be understood as the client ID you want to operate on.
    • DTC-MFA-Data — only required by specific endpoints noted per-endpoint below (currently /wallet/v1/crypto-txn/withdraw and /wallet/v1/fiat-txn/withdraw, and only when the caller is an institution with sub-account MFA enabled).

NOTE: The server reads the raw body directly from the HTTP request stream for signature verification. The body used to compute the signature must be byte-identical to the body actually sent — any difference (an extra space, a different key order after re-serialization) breaks verification. Sign the exact same string you send, don't re-serialize a JSON object.

2.4.1 Example: GET Request (Java)

import com.google.common.hash.Hashing;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

public class SignatureTestGuava {
    public static void main(String[] args) {
        String signKey = "4mt0MUecOtOEDI42WjN4BmM0";
        String method = "GET";
        String uri = "/wallet/v1/otc/reference-number/REF20250901001";
        String query = "";
        String body = "";
        String timestamp = String.valueOf(System.currentTimeMillis());

        String stringToSign = method + timestamp + uri;
        if (!query.isEmpty()) {
            stringToSign += "?" + query;
        }
        stringToSign += body;

        String signature = Base64.getEncoder().encodeToString(
                Hashing.hmacSha512(signKey.getBytes(StandardCharsets.UTF_8))
                        .hashBytes(stringToSign.getBytes(StandardCharsets.UTF_8))
                        .asBytes()
        );

        System.out.println("Timestamp   : " + timestamp);
        System.out.println("StringToSign: " + stringToSign);
        System.out.println("Signature   : " + signature);
    }
}

2.4.2 Example: POST Request (Java)

import com.google.common.hash.Hashing;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

public class SignatureTestGuavaPOST {
    public static void main(String[] args) {
        String signKey = "4mt0MUecOtOEDI42WjN4BmM0";
        String method = "POST";
        String uri = "/wallet/v1/otc/get-otc-rate";
        String query = "";
        String body = "{\"query\":{\"buyCurrency\":\"USD\",\"sellCurrency\":\"USDC\"}}";
        String timestamp = String.valueOf(System.currentTimeMillis());

        String stringToSign = method + timestamp + uri;
        if (!query.isEmpty()) {
            stringToSign += "?" + query;
        }
        stringToSign += body;

        String signature = Base64.getEncoder().encodeToString(
                Hashing.hmacSha512(signKey.getBytes(StandardCharsets.UTF_8))
                        .hashBytes(stringToSign.getBytes(StandardCharsets.UTF_8))
                        .asBytes()
        );

        System.out.println("Timestamp    : " + timestamp);   // Value for D-TIMESTAMP
        System.out.println("StringToSign : " + stringToSign);
        System.out.println("Signature    : " + signature);   // Value for D-SIGNATURE
    }
}

3 API

3.1 Message Structure

Every request body:

{
  "query": {}
}

Paginated requests additionally carry:

{
  "query": {},
  "page": {
    "current": 1,
    "size": 10
  }
}

Responses come in three shapes depending on the endpoint:

Single object

{
  "header": {
    "success": true,
    "errCode": null,
    "errMsg": null
  },
  "result": {}
}

Plain list (no pagination — used by a small number of endpoints not covered in this document)

{
  "header": {
    "success": true
  },
  "resultList": []
}

Paginated list

{
  "header": {
    "success": true
  },
  "resultList": [],
  "pagination": {
    "current": 1,
    "size": 10,
    "total": 100
  }
}

Failure:

{
  "header": {
    "success": false,
    "errCode": "31005",
    "errMsg": "Card Information is invalid"
  }
}

3.2 Wallet Account

3.2.1 Get Balance

Endpoint

[GET] /wallet/v1/account/balance/{currency}

Description

Retrieve the balance for a specific currency wallet owned by the caller.

Path Variables

NameTypeRequiredDescription
currencyStringMWallet currency (e.g. USD, SGD, USDC). Refer to Enum List — Available Currency

Response Parameters

NameTypeRequiredDescription
balanceDecimalMCurrent wallet balance for the specified currency

Response Body Sample

{
  "header": {
    "success": true
  },
  "result": {
    "balance": 10.12
  }
}

Possible Error Codes

CodeDescriptionWhen it happens
20002Wallet account does not existNo wallet account for this client + currency
00006Access deniedNo valid authClient on the request (token issue)
00018Currency is invalidcurrency path segment does not map to a known Currency

3.2.2 Get All Wallet Balances

Endpoint

[GET] /wallet/v1/account/balances

Description

Retrieve all of the caller's wallet account balances, one row per currency the caller has a wallet account for.

Response Parameters (array of wallet accounts in resultList)

NameTypeRequiredDescription
idLongMWallet account ID
clientIdLongMClient ID
currencyStringMWallet currency
balanceDecimalMCurrent balance
updatedAtStringMLast update timestamp, yyyy-MM-dd HH:mm:ss

Response Body Sample

{
  "header": {
    "success": true
  },
  "resultList": [
    {
      "id": 988,
      "clientId": 8520111007,
      "currency": "HKD",
      "balance": 70526.27,
      "updatedAt": "2026-09-02 16:13:59"
    },
    {
      "id": 987,
      "clientId": 8520111007,
      "currency": "EUR",
      "balance": 544.39,
      "updatedAt": "2026-09-02 16:13:59"
    }
  ]
}

Possible Error Codes

CodeDescriptionWhen it happens
00006Access deniedNo valid authClient

3.2.3 Wallet Transfer

Endpoint

[POST] /wallet/v1/account/transfer

Description

Transfer funds from the caller's own wallet of currency to another dtcpay client's wallet of the same currency, identified by recipientClientId. There is no currency conversion — the recipient's wallet of that same currency is credited the exact amount debited. This is a pure internal ledger move between two dtcpay-hosted wallets; it never touches a blockchain or a third-party bank, regardless of whether currency is fiat or stablecoin.

Contrast with Business Transfer below: transfer supports any currency the caller has a wallet for, needs no supporting documents, and has no receiver-eligibility whitelist gate. business-transfer is stablecoin-only by default (fiat only for whitelisted callers), requires supporting document fileIds, and additionally accepts a recipientDtcpayTag identifier.

Request Parameters

NameTypeRequiredDescription
currencyStringMCurrency of the caller's own wallet to debit; the recipient's wallet of this same currency is credited
amountDecimalMAmount to debit from the caller and credit to the recipient. Must be > 0
recipientClientIdLongMRecipient dtcpay client ID; must own a wallet of currency
noteString (≤35)OMessage to beneficiary
referenceNoStringOClient reference number, used for idempotency. Auto-generated if omitted

Request Body Sample

{
  "query": {
    "currency": "SGD",
    "amount": 1.25,
    "recipientClientId": 85250630150001070,
    "note": "test1"
  }
}

Response Parameters

A wallet transfer always creates two transaction rows — a TRANSFER_OUT leg on the caller's own wallet and a TRANSFER_IN leg on the recipient's wallet — both of which are their own Inquiry Wallet Balance History row (relatedId = that leg's own transaction id), so both are returned.

NameTypeRequiredDescription
fiatTransferOutObjectOPopulated when currency is a fiat currency — the TRANSFER_OUT leg on the caller's own wallet. Same shape as Get Fiat Transaction
fiatTransferInObjectOPopulated when currency is a fiat currency — the TRANSFER_IN leg credited to the recipient. referenceNo/approver are always null on this leg — only the TRANSFER_OUT leg carries the caller-supplied reference number and approver
cryptoTransferOutObjectOPopulated when currency is a stablecoin currency — the TRANSFER_OUT leg on the caller's own wallet. Same shape as Get Crypto Transaction. Because this is an internal ledger move (not an on-chain transfer), gasFee/transactionFee are always 0 here — they are meaningful non-zero values on the real on-chain paths such as Crypto Withdraw
cryptoTransferInObjectOPopulated when currency is a stablecoin currency — the TRANSFER_IN leg credited to the recipient. referenceNo is always null on this leg, same reason as fiatTransferIn

Exactly one pair (fiatTransferOut+fiatTransferIn, or cryptoTransferOut+cryptoTransferIn) is populated, matching whether currency was fiat or stablecoin.

Response Body Sample — fiat

{
  "header": {
    "success": true
  },
  "result": {
    "fiatTransferOut": {
      "id": 2510202055340218000,
      "clientId": 250507120005941,
      "type": 6,
      "state": 200,
      "currency": "SGD",
      "amount": 1,
      "transactionFee": 0,
      "senderAccountId": 250507120005941,
      "recipientAccountId": 85250630150001070,
      "recipientAmount": 1,
      "referenceNo": "20260428153012345A7K3X9",
      "operator": "[250507120005941] test1",
      "approver": "[250507120005941] test1",
      "createdAt": "2026-09-02 16:13:59",
      "completedAt": "2026-09-02 16:13:59",
      "updatedAt": "2026-09-02 16:13:59"
    },
    "fiatTransferIn": {
      "id": 2510202055340219000,
      "clientId": 85250630150001070,
      "type": 5,
      "state": 200,
      "currency": "SGD",
      "amount": 1,
      "transactionFee": 0,
      "senderAccountId": 250507120005941,
      "recipientAccountId": 85250630150001070,
      "recipientAmount": 1,
      "referenceNo": null,
      "operator": "[250507120005941] test1",
      "approver": null,
      "createdAt": "2026-09-02 16:13:59",
      "completedAt": "2026-09-02 16:13:59",
      "updatedAt": "2026-09-02 16:13:59"
    }
  }
}

Response Body Sample — stablecoin

{
  "header": {
    "success": true
  },
  "result": {
    "cryptoTransferOut": {
      "id": 2510202054090305000,
      "type": 7,
      "state": 200,
      "clientId": 250507120005941,
      "mainNet": null,
      "amount": 1,
      "currency": "USDC",
      "transactionFee": 0,
      "gasFee": 0,
      "recipientAddressId": 85250630150001070,
      "recipientAddress": null,
      "senderAddressId": 250507120005941,
      "senderAddress": null,
      "referenceNo": "20260428153012345A7K3X9",
      "remark": "test",
      "operator": "[250507120005941] test1",
      "createdAt": "2026-09-02 16:13:59",
      "updatedAt": "2026-09-02 16:13:59"
    },
    "cryptoTransferIn": {
      "id": 2510202054090306000,
      "type": 6,
      "state": 200,
      "clientId": 85250630150001070,
      "mainNet": null,
      "amount": 1,
      "currency": "USDC",
      "transactionFee": 0,
      "gasFee": 0,
      "recipientAddressId": 85250630150001070,
      "recipientAddress": null,
      "senderAddressId": 250507120005941,
      "senderAddress": null,
      "referenceNo": null,
      "remark": "test",
      "operator": "[250507120005941] test1",
      "createdAt": "2026-09-02 16:13:59",
      "updatedAt": "2026-09-02 16:13:59"
    }
  }
}

mainNet/recipientAddress/senderAddress are null here because a wallet-transfer recipient is identified by recipientClientId, not by a whitelisted blockchain address — there is no address record to resolve.

Possible Error Codes

CodeDescriptionWhen it happens
00011Your balance is insufficient. Please adjust the amount or select a different wallet.Caller's wallet balance < amount
00006Access deniedNo valid authClient
11004Invalid amountamount missing or ≤ 0
20999Wallet account error (other)WalletValidationException/ValidationException from the transfer pipeline
21999Fiat transaction error (other)Unexpected exception during transfer

3.2.4 Business Transfer

Endpoint

[POST] /wallet/v1/account/business-transfer

Description

Transfer from the caller's own wallet of currency to a recipient identified by either recipientClientId or recipientDtcpayTag (exactly one must be set), with mandatory supporting document fileIds.

By default, only stablecoin currencies are accepted. Fiat is additionally allowed only for callers enrolled in the fiat receiver-constraint beta whitelist (contact dtcpay to enroll); for those callers, a receiver-eligibility check (KYC tier / balance cap) also runs against the recipient when identified by recipientClientId. A recipientDtcpayTag that resolves to an individual (not a business) is always rejected — dtag-addressed business transfers are B2B only.

Request Parameters

NameTypeRequiredDescription
currencyStringMCurrency of the caller's own wallet to debit. Must be a stablecoin unless whitelisted for fiat (see above)
amountDecimalMAmount to debit/credit. Must be > 0
recipientClientIdLongCRecipient client ID. Provide exactly one of recipientClientId/recipientDtcpayTag
recipientDtcpayTagStringCRecipient dtcpay tag. Provide exactly one of recipientClientId/recipientDtcpayTag
fileIdsArrayMSupporting document file IDs, uploaded beforehand (see Get Upload Token / Upload File by Token). Non-empty; each file must belong to the caller and be in NORMAL status
noteString (≤35)OMessage to beneficiary
referenceNoStringOClient reference number, used for idempotency

Request Body Sample — by client ID

{
  "query": {
    "currency": "USDC",
    "amount": 100.0,
    "recipientClientId": 260318130073305,
    "fileIds": [
      998001,
      998002
    ],
    "note": "April KOL commission"
  }
}

Request Body Sample — by dtcpay tag

{
  "query": {
    "currency": "USDC",
    "amount": 100.0,
    "recipientDtcpayTag": "@kolalice",
    "fileIds": [
      998001
    ],
    "note": "April KOL commission"
  }
}

Response Parameters

Same two-leg shape as Wallet Transfer — a business transfer creates a TRANSFER_OUT leg on the caller's own wallet and a TRANSFER_IN leg on the recipient's wallet, each its own Inquiry Wallet Balance History row, so both are returned.

NameTypeRequiredDescription
fiatTransferOutObjectOPopulated when currency is fiat (whitelisted callers only) — the TRANSFER_OUT leg on the caller's own wallet. Same shape as Get Fiat Transaction
fiatTransferInObjectOPopulated when currency is fiat — the TRANSFER_IN leg credited to the recipient. referenceNo/approver are always null on this leg, same as Wallet Transfer
cryptoTransferOutObjectOPopulated when currency is a stablecoin — the TRANSFER_OUT leg on the caller's own wallet. Same shape as Get Crypto Transaction; gasFee/transactionFee are always 0 here for the same reason as Wallet Transfer
cryptoTransferInObjectOPopulated when currency is a stablecoin — the TRANSFER_IN leg credited to the recipient. referenceNo is always null on this leg

Response Body Sample — fiat (whitelisted callers only)

{
  "header": {
    "success": true
  },
  "result": {
    "fiatTransferOut": {
      "id": 2606031457480355794,
      "clientId": 8522120036,
      "type": 6,
      "state": 200,
      "currency": "SGD",
      "amount": 100.0,
      "transactionFee": 0,
      "senderAccountId": 8522120036,
      "recipientAccountId": 260318130073305,
      "recipientAmount": 100.0,
      "referenceNo": "20260603145748abc123",
      "operator": "[8522120036] test",
      "approver": "[8522120036] test",
      "createdAt": "2026-06-03 14:57:48",
      "completedAt": "2026-06-03 14:57:48",
      "updatedAt": "2026-06-03 14:57:48"
    },
    "fiatTransferIn": {
      "id": 2606031457480355795,
      "clientId": 260318130073305,
      "type": 5,
      "state": 200,
      "currency": "SGD",
      "amount": 100.0,
      "transactionFee": 0,
      "senderAccountId": 8522120036,
      "recipientAccountId": 260318130073305,
      "recipientAmount": 100.0,
      "referenceNo": null,
      "operator": "[8522120036] test",
      "approver": null,
      "createdAt": "2026-06-03 14:57:48",
      "completedAt": "2026-06-03 14:57:48",
      "updatedAt": "2026-06-03 14:57:48"
    }
  }
}

Response Body Sample — stablecoin

{
  "header": {
    "success": true
  },
  "result": {
    "cryptoTransferOut": {
      "id": 2606031457480355796,
      "type": 7,
      "state": 200,
      "clientId": 8522120036,
      "amount": 100.0,
      "currency": "USDC",
      "gasFee": 0,
      "transactionFee": 0,
      "recipientAddressId": 260318130073305,
      "recipientAddress": null,
      "senderAddressId": 8522120036,
      "senderAddress": null,
      "referenceNo": "20260603145748abc123",
      "operator": "[8522120036] test",
      "createdAt": "2026-06-03 14:57:48",
      "updatedAt": "2026-06-03 14:57:48"
    },
    "cryptoTransferIn": {
      "id": 2606031457480355797,
      "type": 6,
      "state": 200,
      "clientId": 260318130073305,
      "amount": 100.0,
      "currency": "USDC",
      "gasFee": 0,
      "transactionFee": 0,
      "recipientAddressId": 260318130073305,
      "recipientAddress": null,
      "senderAddressId": 8522120036,
      "senderAddress": null,
      "referenceNo": null,
      "operator": "[8522120036] test",
      "createdAt": "2026-06-03 14:57:48",
      "updatedAt": "2026-06-03 14:57:48"
    }
  }
}

Possible Error Codes

CodeDescriptionWhen it happens
00011Your balance is insufficient. Please adjust the amount or select a different wallet.Caller's wallet balance < amount
00006Access deniedNo valid authClient, or dtag resolved to an individual
00010Invalid parametersCurrency not a stablecoin (and not whitelisted for fiat); both/neither of recipientClientId/recipientDtcpayTag provided; transfer to self; amount ≤ 0; fileIds missing/empty
50014Receiver KYC not completedWhitelisted-fiat path only: recipient's KYC does not clear the receiver-eligibility check
50015Receiver tier does not support stablecoinWhitelisted-fiat path only
50016Receiver tier does not support fiatWhitelisted-fiat path only
50017Receiver balance cap reachedWhitelisted-fiat path only
50018Invalid recipient dtagrecipientDtcpayTag does not resolve, or resolves to an individual
14004File upload failedA fileIds entry does not exist, isn't owned by the caller, or isn't NORMAL
20999/21999Wallet/fiat transaction error (other)Unexpected exception during transfer

3.2.5 Inquiry Wallet Balance History

Endpoint

[POST] /wallet/v1/account/balance/history/inquiry

Description

Paginated read of the caller's own rows in wallet.wallet_balance_history — one row per ledger entry that changed a wallet's balance. This is a plain range/equality query against that table, not a merged view across other business objects: relatedId is returned as-is and its meaning depends on type (e.g. for type=FIAT_WITHDRAWAL it is a FiatTransaction ID, for type=CRYPTO_DEPOSIT a CryptoTransaction ID, for type=OTC an Otc ID — see Enum List — ActivityType for the full mapping). To see the details of a specific activity, look it up through the corresponding endpoint (e.g. GET /wallet/v1/fiat-txn/{txnId}) using relatedId together with type.

Request Parameters

NameTypeRequiredDescription
currencyStringOCurrency of the ledger entry
typeEnumOLedger entry type. Refer to Enum List — ActivityType
createTimeFromStringOStart time, inclusive. Format yyyy-MM-dd HH:mm:ss, truncated to the start of that day
createTimeToStringOEnd time, inclusive. Format yyyy-MM-dd HH:mm:ss, truncated to the end of that day
page.currentLongMCurrent page number
page.sizeLongMPage size, max 500

Request Body Sample

{
  "query": {
    "currency": "USDC"
  },
  "page": {
    "current": 1,
    "size": 10
  }
}

Response Parameters

NameTypeRequiredDescription
idLongMLedger entry ID
walletAccountIdLongMWallet account ID this entry belongs to
currencyStringMCurrency of the ledger entry
typeEnumMLedger entry type. Refer to Enum List — ActivityType
relatedIdLongOID of the underlying record this entry was posted for; which table depends on type (see Description above)
balanceBeforeDecimalMWallet balance immediately before this entry
changeAmountDecimalMSigned amount this entry changed the balance by (positive = credit, negative = debit)
balanceAfterDecimalMWallet balance immediately after this entry
completedAtStringOLedger completion timestamp, yyyy-MM-dd HH:mm:ss
updatedAtStringMLast update timestamp, yyyy-MM-dd HH:mm:ss

Response Body Sample

{
  "header": {
    "success": true
  },
  "resultList": [
    {
      "id": 2509231015230507618,
      "walletAccountId": 988,
      "currency": "USDC",
      "type": 15,
      "relatedId": 240501000012345,
      "balanceBefore": 68304.05,
      "changeAmount": 2222,
      "balanceAfter": 70526.05,
      "completedAt": "2025-09-23 10:15:24",
      "updatedAt": "2025-09-23 10:15:24"
    }
  ],
  "pagination": {
    "current": 1,
    "size": 10,
    "total": 1980
  }
}

Possible Error Codes

CodeDescriptionWhen it happens

This endpoint has no dedicated failure path beyond the common 00006 Access denied (missing authClient — returns an empty resultList, not an error header) — any other failure is a 500 from an unexpected exception.


3.3 OTC (Swap)

3.3.1 Sequence Diagram

Sequence diagram: see the interactive version in the dtcpay developer portal (PDF export available on request).

3.3.2 Get OTC Order

Endpoint

[GET] /wallet/v1/otc/{otcId}

Description

Retrieve an OTC (swap) order by ID. Returns the same object shape as Inquiry OTC Orders / Get OTC Order by Reference Number / Request OTC.

Path Variables

NameTypeRequiredDescription
otcIdLongMOTC order ID

Response Parameters

NameTypeRequiredDescription
idLongMOTC ID
statusEnumMOTC status. Refer to Enum List — OtcStatus
clientIdLongMClient ID
sellCurrencyStringMCurrency sold
buyCurrencyStringMCurrency bought
rateDecimalMExchange rate
dtcQuoteIdLongODTC quote ID that was consumed to create this order
sellAmountDecimalMAmount sold
buyAmountDecimalMAmount bought
operatorStringOOperator
referenceNoStringOReference number
completedAtStringOCompleted time, yyyy-MM-dd HH:mm:ss
updatedAtStringOLast updated time, yyyy-MM-dd HH:mm:ss

Response Body Sample

{
  "header": {
    "success": true
  },
  "result": {
    "id": 2510101421380624067,
    "status": 5,
    "clientId": 8524010424,
    "sellCurrency": "GBP",
    "buyCurrency": "USD",
    "rate": 1.345119262388,
    "dtcQuoteId": 1155389391925733471,
    "sellAmount": 0.75,
    "buyAmount": 1,
    "operator": "[8524010424] openapi",
    "referenceNo": "173561238951",
    "completedAt": "2025-10-10 14:21:39",
    "updatedAt": "2025-10-10 14:21:39"
  }
}

Possible Error Codes

CodeDescriptionWhen it happens
00999Swap Info not existNo OTC order with that ID
00006Access deniedNo valid authClient, or the order belongs to another client

3.3.3 Inquiry OTC Orders

Endpoint

[POST] /wallet/v1/otc/inquiry

Description

Look up the caller's OTC orders by pagination. Same response object shape as Get OTC Order.

Request Parameters

NameTypeRequiredDescription
idLongOOTC ID. When provided, all other filters are ignored and this single order (if owned by the caller) is returned
statusEnumOOTC status
sellCurrencyStringOCurrency sold
buyCurrencyStringOCurrency bought
createTimeFromStringOStart time, inclusive. Format yyyy-MM-dd HH:mm:ss, truncated to start of day. Consistent with the createTimeFrom field used by every other inquiry endpoint in this API
createTimeToStringOEnd time, inclusive. Format yyyy-MM-dd HH:mm:ss, truncated to end of day (was orderTimeEnd)
page.currentIntegerMCurrent page number
page.sizeIntegerMPage size

Request Body Sample

{
  "query": {
    "status": 5,
    "buyCurrency": "USDC",
    "sellCurrency": "USD",
    "createTimeFrom": "2026-01-01 00:00:00",
    "createTimeTo": "2026-01-31 23:59:59"
  },
  "page": {
    "current": 1,
    "size": 50
  }
}

Response Body Sample — array of the same object as Get OTC Order:

{
  "header": {
    "success": true
  },
  "resultList": [
    {
      "id": 2508071556490618367,
      "status": 5,
      "clientId": 8524010424,
      "sellCurrency": "SGD",
      "buyCurrency": "USDC",
      "rate": 0.7702672119830299,
      "dtcQuoteId": 1155389391900000000,
      "sellAmount": 123123,
      "buyAmount": 94837.6,
      "referenceNo": "xsdf34234324",
      "operator": "downey.wang@dtc.top",
      "completedAt": "2025-08-07 15:56:50",
      "updatedAt": "2025-08-07 15:56:50"
    }
  ],
  "pagination": {
    "current": 1,
    "size": 50,
    "total": 1
  }
}

Possible Error Codes

CodeDescriptionWhen it happens
11999Invalid OTC Idid provided but does not resolve to an order owned by the caller

3.3.4 Get OTC Order by Reference Number

Endpoint

[GET] /wallet/v1/otc/reference-number/{referenceNo}

Description

Retrieve an OTC order by reference number. Same object shape as Get OTC Order / Inquiry OTC Orders / Request OTC.

Path Variables

NameTypeRequiredDescription
referenceNoStringMReference number (unique ID from your system)

Response Body Sample — same shape as Get OTC Order.

Possible Error Codes

CodeDescriptionWhen it happens
00006Access deniedNo valid authClient
00999ReferenceNo not existNo order with that reference number for this client

3.3.5 Get OTC Rate

Endpoint

[POST] /wallet/v1/otc/get-otc-rate

Description

Get a locked exchange rate quote for an OTC swap.

Request Parameters

NameTypeRequiredDescription
sellCurrencyStringMCurrency to sell
sellAmountDecimalCSell amount
buyCurrencyStringMCurrency to buy
buyAmountDecimalCBuy amount

At least one of sellAmount/buyAmount must be provided; if both are omitted, only indicative rate information is returned.

Request Body Sample

{
  "query": {
    "buyCurrency": "USD",
    "sellCurrency": "GBP"
  }
}

Response Parameters

NameTypeRequiredDescription
sellCurrencyStringMCurrency to sell
buyCurrencyStringMCurrency to buy
clientIdLongMClient ID
expiresAtStringMQuote expiry time, yyyy-MM-dd HH:mm:ss — see below
rateDecimalMExchange rate
quoteIdLongMQuote ID; pass as dtcQuoteId to Request OTC. The value may be negative.
sellAmountDecimalOSell amount
buyAmountDecimalOBuy amount
sellWalletBalanceDecimalMCaller's wallet balance in sellCurrency
buyWalletBalanceDecimalMCaller's wallet balance in buyCurrency

How long is the quote valid for? rate/sellAmount/buyAmount are only guaranteed until expiresAt:

  • Fiat↔stablecoin pairs (a USD-pegged fiat currency against a stablecoin): 7 days.
  • Every other pair (the default for a standard OTC enquiry): 24 hours.

Call this endpoint again to get a fresh rate once expired; a dtcQuoteId can only be consumed once regardless.

Response Body Sample

{
  "header": {
    "success": true
  },
  "result": {
    "sellCurrency": "GBP",
    "buyCurrency": "USD",
    "clientId": 8524010424,
    "expiresAt": "2025-10-10 14:22:15",
    "rate": 1.345119262388,
    "quoteId": 1155389391925733471,
    "balance": 10.79,
    "sellWalletBalance": 10.79,
    "buyWalletBalance": 4725.06
  }
}

Possible Error Codes

CodeDescriptionWhen it happens
00018Currency is invalidsellCurrency/buyCurrency not supported
11002OTC quote errorEnquiry against the pricing source failed
00001Failed to fetch dataUnexpected exception

3.3.6 Request OTC

Endpoint

[POST] /wallet/v1/otc/request

Description

Submit an OTC swap using a quote obtained from Get OTC Rate. Same response object shape as Get OTC Order / Inquiry OTC Orders / Get OTC Order by Reference Number.

Request Parameters

NameTypeRequiredDescription
buyCurrencyStringMCurrency to buy
sellCurrencyStringMCurrency to sell
sellAmountDecimalCEither sellAmount or buyAmount must be provided
buyAmountDecimalCEither buyAmount or sellAmount must be provided
dtcQuoteIdLongMQuote ID from Get OTC Rate. One-time use — becomes invalid once consumed
referenceNoStringOReference number (unique ID from your system), used for idempotency

Request Body Sample

{
  "query": {
    "buyCurrency": "USD",
    "buyAmount": 1,
    "sellCurrency": "GBP",
    "sellAmount": 0.75,
    "dtcQuoteId": 1155389391925733471,
    "referenceNo": "173561238951"
  }
}

Response Body Sample — same shape as Get OTC Order.

Possible Error Codes

CodeDescriptionWhen it happens
00006Access deniedNo valid authClient
00018Currency is invalidsellCurrency/buyCurrency not supported
20999Reference number already exists / wallet account error (other)Duplicate referenceNo for this client, or a ValidationException from status checks
11999OTC error (other)Swap processing failed (e.g. quote expired, insufficient balance)

3.4 RAA (Risk Awareness Assessment)

MAS requires every DPT (Digital Payment Token) user to pass a Risk Awareness Assessment before their first stablecoin deposit. RAA lets a partner present DTC's risk assessment inside its own user journey instead of redirecting to a DTC-hosted page: the partner retrieves the question bank (including the answer key), presents questionsToDraw questions, grades the attempt itself, and calls dtcpay only once the user has met answersNeededCorrect — dtcpay re-grades the submission against its own key rather than trusting the caller's result.

NOTE

  • Use questionsToDraw to decide how many questions to present and answersNeededCorrect to decide whether the user has passed — do not hard-code either number, the bank can be resized or the pass bar moved independently of this document.
  • Submit the complete set of answers (including any wrong one) only after the user has met answersNeededCorrect. A submission that falls short of the bar is rejected and the account is placed under manual review (50021), it is not silently marked as failed and re-triable.
  • Only raaStatus = 2 (COMPLETED) is a pass. If Query RAA Status is unavailable or errors, treat the user as PENDING (1) and withhold the gated action — never fail open.
  • If you plan to enable crypto stablecoin deposits for a client, RAA must be completed for that client first — see the note at the top of Crypto (Stablecoin) below.

3.4.1 Sequence Diagram

Sequence diagram: see the interactive version in the dtcpay developer portal (PDF export available on request).

3.4.2 Get RAA Question Bank

Endpoint

[GET] /wallet/v1/raa/bank

Description

Returns every enabled RAA question for a locale, including the answer key — the caller presents the questions and grades the attempt itself. A locale with no questions seeded falls back to English; the language actually served is echoed back in locale so the caller renders text consistent with what it will later submit.

Query Parameters

NameTypeRequiredDescription
assessmentCodeStringOIdentifies the assessment. Fixed value DPT_RISK_AWARENESS if provided
localeStringOPreferred language. Refer to Enum List — SupportedLanguage. Falls back to EN if the requested locale has no questions seeded

Query Parameters Sample

GET /wallet/v1/raa/bank?assessmentCode=DPT_RISK_AWARENESS&locale=EN

Response Parameters

NameTypeRequiredDescription
assessmentCodeStringMDPT_RISK_AWARENESS
totalQuestionsIntegerMNumber of questions in questions. May change when the bank is updated — do not hard-code. Observed as 94 in stg at time of testing
questionsToDrawIntegerMHow many questions to present in one attempt
passCriteriaStringMGrading rule type. Currently MIN_CORRECT (grade on answersNeededCorrect, not on getting every question right)
answersNeededCorrectIntegerMMinimum number of presented questions that must be answered correctly to pass
localeStringMLanguage actually served, may differ from the requested locale. Refer to Enum List — SupportedLanguage
questionsArrayMEvery enabled question for the served locale
questions[].questionIdLongMStable question ID. Echo unchanged in the submission
questions[].typeStringMQuestion type. Currently always SINGLE_CHOICE — the server hardcodes this value for every question, there is no multi-select variant implemented
questions[].questionContentStringMQuestion text to display
questions[].optionsArrayMSelectable options
questions[].options[].optionIdEnumMOption ID. Refer to Enum List — QuestionOption
questions[].options[].optionContentStringMOption text to display
questions[].correctOptionIdsArrayMCorrect option ID(s), for the caller to grade the attempt against. Refer to Enum List — QuestionOption. Currently always exactly one element for every question in the bank (confirmed across all 94 stg questions) — the question bank is single-select only (CkaQuestion.answer is a single value, not a collection) and the array shape is reserved for a hypothetical future multi-select question type, not a bug

Response Body Sample — real question shown, remaining 93 questions omitted for brevity (see wallet-execution-report.md item 17 / transcripts/wallet/18_raa_bank.json for the full 94-question capture):

{
  "header": {
    "success": true
  },
  "result": {
    "assessmentCode": "DPT_RISK_AWARENESS",
    "totalQuestions": 94,
    "questionsToDraw": 5,
    "passCriteria": "MIN_CORRECT",
    "answersNeededCorrect": 4,
    "locale": "EN",
    "questions": [
      {
        "questionId": 3,
        "type": "SINGLE_CHOICE",
        "questionContent": "Which statement(s) is/are true about most digital payment tokens(DPTs) such as Bitcoin?",
        "options": [
          {
            "optionId": "A",
            "optionContent": "The prices of DPTs are stable and do not vary greatly over time."
          },
          {
            "optionId": "B",
            "optionContent": "Most DPTs have a minimum guaranteed value."
          },
          {
            "optionId": "C",
            "optionContent": "The prices of DPT can fluctuate greatly."
          },
          {
            "optionId": "D",
            "optionContent": "It is impossible for DPTs to lose all their value."
          }
        ],
        "correctOptionIds": [
          "B"
        ]
      }
      // ...remaining 93 questions omitted from this sample
    ]
  }
}

Possible Error Codes

CodeDescriptionWhen it happens
00006Access deniedNo valid authClient
00010Invalid parametersassessmentCode provided but not DPT_RISK_AWARENESS
59999Internal errorUnexpected server-side failure

3.4.3 Submit RAA Result

Endpoint

[POST] /wallet/v1/raa/submissions

Description

Call once the user has answered at least answersNeededCorrect of the presented questions correctly. Send every answer, including any that were wrong — the question and selected-answer text as displayed to the user must be included, compliance requires the record to preserve exactly what the user saw. Idempotent: replaying a submissionId, or submitting for a user who already completed, returns the original result with recorded = false rather than an error. dtcpay re-grades on the same rule as the question bank; an attempt short of the bar is rejected and routed to manual review rather than accepted or silently dropped. The sub-account the submission is recorded against is resolved the same way as every other endpoint in this document — from the authenticated caller (D-SUB-ACCOUNT-ID), not a body parameter.

Request Parameters

NameTypeRequiredDescription
assessmentCodeStringMFixed value DPT_RISK_AWARENESS
submissionIdStringMIdempotency key, generated by the caller and globally unique. Reuse the same value when retrying the same submission
completedAtStringMTime the user completed the assessment, ISO-8601 with or without an offset. Missing, unparseable, more than 5 minutes in the future, or more than 7 days old is rejected as 50020
localeStringOLanguage the user took the assessment in. Refer to Enum List — SupportedLanguage
answersArrayMOne entry per question answered. No duplicate question IDs; every ID must exist in the current bank for the given locale
answers[].questionIdLongMEcho the questionId served by the question bank
answers[].questionContentStringOQuestion text as displayed to the user — stored as given for the audit record, not checked against the bank; a difference is reported (internally) rather than rejected
answers[].selectedOptionIdsArrayMOption(s) selected. SINGLE_CHOICE requires exactly one
answers[].selectedOptionContentsArrayOSelected option text as displayed to the user — stored as given for the audit record

Request Body Sample

{
  "query": {
    "assessmentCode": "DPT_RISK_AWARENESS",
    "submissionId": "aix-20260911-7f3a9c1e",
    "completedAt": "2026-09-11T10:15:30+08:00",
    "locale": "EN",
    "answers": [
      {
        "questionId": 61,
        "selectedOptionIds": [
          "B"
        ]
      },
      {
        "questionId": 62,
        "selectedOptionIds": [
          "A"
        ]
      },
      {
        "questionId": 63,
        "selectedOptionIds": [
          "C"
        ]
      },
      {
        "questionId": 64,
        "selectedOptionIds": [
          "D"
        ]
      },
      {
        "questionId": 65,
        "selectedOptionIds": [
          "B"
        ]
      }
    ]
  }
}

Response Parameters

NameTypeRequiredDescription
submissionIdStringMThe submissionId of the record being reported — for a replay, the original one
raaStatusIntegerMRAA status after this submission. A successful passing submission returns 2 (COMPLETED). Refer to Enum List — RaaStatus
recordedBooleanMWhether this call created a new record. false means the same submissionId was already stored, or the user had already completed RAA — still a successful, idempotent response
completedAtStringOCompletion time as recorded by dtcpay, yyyy-MM-dd HH:mm:ss

Response Body Sample

{
  "header": {
    "success": true
  },
  "result": {
    "submissionId": "aix-20260911-7f3a9c1e",
    "raaStatus": 2,
    "recorded": true,
    "completedAt": "2026-09-11 10:15:30"
  }
}

Possible Error Codes

CodeDescriptionWhen it happens
00006Access deniedNo valid authClient
00010Invalid parametersRequest body missing query
01038User not foundD-SUB-ACCOUNT-ID (or the master account) does not resolve to a known client
50020Invalid RAA answer setWrong number of answers, duplicate/unknown question IDs, more than one selected option per question, or an invalid completedAt
50021Answer mismatch — below the pass thresholdRe-grading against dtcpay's answer key found fewer than answersNeededCorrect correct; the account is placed UNDER_REVIEW, not silently rejected
59999Internal errorUnexpected server-side failure

3.4.4 Query RAA Status

Endpoint

[GET] /wallet/v1/raa/status

Description

Query a sub-account's current RAA status — the basis for deciding whether to prompt the assessment and for gating crypto deposits. If this endpoint is unavailable or errors, treat the user as PENDING and withhold the gated action; do not fail open.

Query Parameters

NameTypeRequiredDescription
assessmentCodeStringOFixed value DPT_RISK_AWARENESS if provided

Response Parameters

NameTypeRequiredDescription
clientIdLongMThe sub-account clientId this status belongs to
raaStatusIntegerM1=PENDING (prompt the assessment, withhold gated actions), 2=COMPLETED (allow), 3=UNDER_REVIEW (withhold, anomaly under manual review). Refer to Enum List — RaaStatus
completedAtStringOWhen the assessment was completed, yyyy-MM-dd HH:mm:ss. null unless raaStatus is 2

Response Body Sample

{
  "header": {
    "success": true
  },
  "result": {
    "clientId": 250426220002035,
    "raaStatus": 2,
    "completedAt": "2026-09-11 10:15:30"
  }
}

Possible Error Codes

CodeDescriptionWhen it happens
00006Access deniedNo valid authClient
00010Invalid parametersassessmentCode provided but not DPT_RISK_AWARENESS
01038User not foundD-SUB-ACCOUNT-ID (or the master account) does not resolve to a known client
59999Internal errorUnexpected server-side failure

3.5 Crypto (Stablecoin)

Before enabling crypto stablecoin deposits for a client, that client must have completed RAA (Risk Awareness Assessment) (raaStatus = 2 / COMPLETED on Query RAA Status) — this is a MAS requirement, not a dtcpay-specific gate, and it applies before the first deposit rather than being enforced per-transaction after the fact.

3.5.1 Get Supported Networks By Address

Endpoint

[GET] /wallet/v1/crypto/address/network/{address}

Description

Given a wallet address string, detect which of dtcpay's currently-enabled blockchain networks (MainNet) it is a valid format for. Useful when a partner has collected a recipient's raw address and needs to know which mainnet(s) to associate it with before whitelisting it. Detection is purely address-format validation (regex/checksum), not an on-chain existence check — and an EVM-style address (0x + 40 hex chars) matches every EVM-compatible network at once (see Enum List — MainNet), since that address format is chain-agnostic. BITCOIN is never returned by this endpoint (no Bitcoin address format check is implemented here). Matches are further filtered down to only the networks currently marked visible in dtcpay's network configuration — a recognized address format can still be filtered out if that network isn't currently enabled for partners.

Path Variables

NameTypeRequiredDescription
addressStringMThe raw wallet address to check

Response Parameters

NameTypeRequiredDescription
supportMainNetListArrayMMainnets this address format matches, filtered to currently-visible networks. Empty array if none match. Refer to Enum List — MainNet

Response Body Sample

{
  "header": {
    "success": true
  },
  "result": {
    "supportMainNetList": [
      2,
      3,
      7,
      8,
      10
    ]
  }
}

Possible Error Codes

CodeDescriptionWhen it happens
00006Access deniedNo valid authClient

3.5.2 Get Supported Currencies By MainNet

Endpoint

[GET] /wallet/v1/crypto/address/currency/{mainNet}

Description

List the currencies dtcpay currently supports depositing/withdrawing on a given blockchain network (MainNet), ordered by the platform's configured display order. Useful for building a currency picker once a network has already been chosen (e.g. right after Get Supported Networks By Address). An unrecognized mainNet ID, or a recognized network with no currently-visible currencies configured, both return an empty supportCurrencyList rather than an error.

Path Variables

NameTypeRequiredDescription
mainNetIntegerMMainnet ID. Refer to Enum List — MainNet

Response Parameters

NameTypeRequiredDescription
supportCurrencyListArrayMCurrencies currently enabled for this mainnet, in configured display order. Empty array if the mainnet is unrecognized or has none configured. Refer to Enum List — Available Currency

Response Body Sample

{
  "header": {
    "success": true
  },
  "result": {
    "supportCurrencyList": [
      "USDC",
      "USDC",
      "WUSD"
    ]
  }
}

Possible Error Codes

CodeDescriptionWhen it happens
00006Access deniedNo valid authClient

3.5.3 Sequence Diagram (Deposit)

Sequence diagram: see the interactive version in the dtcpay developer portal (PDF export available on request).

Most deposit-address / whitelist-address management endpoints (whitelist/deposit-target lookup, add-client-own, etc.) are out of scope for this document. The two network/currency lookup endpoints (Get Supported Networks By Address / Get Supported Currencies By MainNet) are the one exception and are documented here.

Whitelist and Risk Withheld: a deposit involves a senderAddress (the address funds are sent from) and the destinationAddress DTC assigns to the client as its deposit target (returned by the deposit-target lookup endpoint noted above). Before depositing, the client's senderAddress must be added to the whitelist and enabled — this is managed on the web platform (API Key Management), not by an open API endpoint. If a deposit arrives from a senderAddress that is not on the whitelist, the transaction is created with state = RISK_WITHHELD (102) instead of completing — it is held pending review, not rejected. Once the client adds the same senderAddress to the whitelist and enables it, the held transaction automatically transitions to its normal completed state; no separate retry or resubmission is needed.

如何在 stg 测试环境充值(testnet):目前测试环境仅支持以太坊(Sepolia 测试网)+ USDC 币种,具体流程如下:

  1. 在 MetaMask 插件中创建一个钱包;
  2. 在 MetaMask 中添加 Sepolia USDC 代币,合约地址:0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238;
  3. 通过 https://faucet.circle.com/ 往刚创建的钱包地址领取测试币,大概几分钟后该钱包地址将收到款项;
  4. 用该钱包地址作为 senderAddress,调用 Add Client Own Address (POST /wallet/v1/crypto/address/add-client-own) 添加白名单,再调用 (POST /wallet/v1/crypto/address/set-enabled) 启用;
  5. 在 MetaMask 中选择对应的 USDC 代币,往 dtcpay 分配的充值目标地址(destinationAddress,见上方 deposit-target lookup 接口)转账,即可触发充值流程。

3.5.4 Sequence Diagram (Withdraw)

Users withdraw to a whitelisted address only. A withdraw request returning success means the request was accepted, not that on-chain settlement has completed — poll by ID/hash/referenceNo, or listen for the webhook.

Sequence diagram: see the interactive version in the dtcpay developer portal (PDF export available on request).

3.5.5 Inquiry Crypto Transactions

Endpoint

[POST] /wallet/v1/crypto-txn/inquiry

Description

Paginated, filterable lookup of the caller's crypto transactions, ordered by ID descending (latest first). Each row includes the resolved sender/recipient blockchain addresses (recipientAddress/senderAddress, resolved server-side from the whitelist by recipientAddressId/senderAddressId) plus gasFee and transactionFee. Returns the same object shape as Get Crypto Transaction / Get Crypto Transaction by Reference Number / Get Crypto Transaction by txnHash — one unified CryptoTransactionResp used by all four query paths.

Request Parameters

NameTypeRequiredDescription
stateEnumOTransaction state. Refer to Enum List — CryptoTransactionState
typeEnumOTransaction type. Refer to Enum List — CryptoTransactionType
senderAddressIdLongOSender whitelist address ID filter
recipientAddressIdLongORecipient whitelist address ID filter
currencyStringOCurrency
mainNetEnumOMainnet. Refer to Enum List — MainNet
transactionDateFromStringOStart date, format yyyy-MM-dd HH:mm:ss, truncated to start of day
transactionDateToStringOEnd date, format yyyy-MM-dd HH:mm:ss, truncated to end of day
page.currentIntegerMCurrent page
page.sizeIntegerMPage size

Request Body Sample

{
  "query": {
    "type": 1,
    "mainNet": 3,
    "currency": "USDC",
    "senderAddressId": 2233,
    "transactionDateFrom": "2026-09-01 00:00:00",
    "transactionDateTo": "2026-09-30 23:59:59"
  },
  "page": {
    "current": 1,
    "size": 10
  }
}

Response Parameters — (this shape is shared by inquiry / get-by-id / get-by-hash / get-by-reference-number; clientId is always scrubbed to null on the inquiry result specifically):

NameTypeRequiredDescription
idLongMUnique transaction ID
typeEnumMTransaction type. Refer to Enum List — CryptoTransactionType
stateEnumMTransaction state. Refer to Enum List — CryptoTransactionState
clientIdLongOClient ID (always null on the inquiry result; populated on the by-ID/by-hash/by-referenceNo results)
mainNetEnumMMainnet type
amountDecimalMTransaction amount
currencyStringMCurrency
transactionFeeDecimalOTransaction fee
txnHashStringOBlockchain transaction hash
referenceNoStringOBusiness reference number
remarkStringOTransaction remark
createdAtStringMTransaction request time, yyyy-MM-dd HH:mm:ss
updatedAtStringMLast updated time, yyyy-MM-dd HH:mm:ss
gasFeeDecimalOGas fee
operatorStringOOperator executing the transaction
recipientAddressIdLongORecipient whitelist address ID
recipientAddressStringORecipient blockchain address, resolved from recipientAddressId
senderAddressIdLongOSender whitelist address ID
senderAddressStringOSender blockchain address, resolved from senderAddressId

Response Body Sample

{
  "header": {
    "success": true
  },
  "resultList": [
    {
      "id": 2508261801180309870,
      "type": 1,
      "state": 200,
      "clientId": null,
      "mainNet": 3,
      "amount": 1.0,
      "currency": "USDC",
      "transactionFee": 30.0,
      "txnHash": "0x05207c3f87c77ffbf0e038d1667794d1fde8f0081ad118f8c4a01673f4a88fe1",
      "referenceNo": "REF20250901001",
      "remark": "User deposit",
      "createdAt": "2025-08-26 18:01:18",
      "updatedAt": "2025-08-26 18:01:18",
      "gasFee": 0,
      "operator": null,
      "recipientAddressId": 2851,
      "recipientAddress": "0x39c929CCB23116e39aE1297B0157F38ecf501D6a",
      "senderAddressId": 2233,
      "senderAddress": "0x73868CA4bdeEe665FEF2d62C25D64bfD58B63327"
    }
  ],
  "pagination": {
    "current": 1,
    "size": 10,
    "total": 2
  }
}

Possible Error Codes

CodeDescriptionWhen it happens
00006Access deniedNo valid authClient
00999Query stablecoin transaction failedUnexpected exception

3.5.6 Get Crypto Transaction Withdraw Fee

Endpoint

[POST] /wallet/v1/crypto-txn/withdraw-fee

Description

Get the fee estimate for a crypto withdrawal.

Request Parameters

NameTypeRequiredDescription
amountDecimalOTransaction amount
currencyStringMWithdrawal currency (a stablecoin, e.g. USDC)

Request Body Sample

{
  "query": {
    "amount": 0.01,
    "currency": "USDC"
  }
}

Response Parameters

NameTypeRequiredDescription
feeDecimalMFee for a withdrawal of this amount/currency

Response Body Sample

{
  "header": {
    "success": true
  },
  "result": {
    "fee": 7.0
  }
}

Possible Error Codes

CodeDescriptionWhen it happens
00018Currency is invalidcurrency unsupported
00999Get txn fee failedUnexpected exception

3.5.7 Crypto Withdraw

Endpoint

[POST] /wallet/v1/crypto-txn/withdraw

Description

Withdraw stablecoin from the caller's wallet to a whitelisted external address. Returns a create-style acknowledgement (not the full transaction object — poll Get Crypto Transaction for the final state).

A successful call only means the withdrawal request was accepted, not that funds have moved. Every crypto withdrawal must clear an internal approval step before it settles on-chain; this has a turnaround time and does not complete immediately. The returned state always starts at PENDING (or RISK_WITHHELD if the recipient address fails a whitelist/risk check — see Sequence Diagram (Deposit) above for the equivalent deposit-side behavior). Poll Get Crypto Transaction by cryptoTransactionId, or listen for the CRYPTO_TXN webhook, to learn when it actually settles.

DTC-MFA-Data header: required when the caller is an institution with sub-account MFA enabled (see Request Signature); enforced identically to the legacy /openapi/v1/crypto-txn/withdraw endpoint — both the requirement-gating interceptor (openApiMfaInterceptor) and the service-layer check that the supplied token corresponds to a genuinely completed face-auth session (OpenApiMfaService.isSubAccountAuthTokenValid, same call legacy makes) are wired identically.

Request Parameters

NameTypeRequiredDescription
recipientAddressIdLongMWhitelisted recipient wallet address ID (renamed from kycWalletAddressId in an earlier draft)
amountDecimalMWithdrawal amount
currencyStringMWithdrawal currency (a stablecoin, e.g. USDC)
referenceNoStringOReference number, must be unique if provided

Request Body Sample

{
  "query": {
    "recipientAddressId": 516,
    "amount": 10,
    "currency": "USDC",
    "referenceNo": "REF20260901001"
  }
}

Response Parameters

NameTypeRequiredDescription
cryptoTransactionIdLongMUnique withdrawal transaction ID — use this to poll Get Crypto Transaction or match against the CRYPTO_TXN webhook
stateEnumMTransaction state at creation time — PENDING pending approval, or RISK_WITHHELD if the recipient address failed a whitelist/risk check. Refer to Enum List — CryptoTransactionState

Response Body Sample

{
  "header": {
    "success": true
  },
  "result": {
    "cryptoTransactionId": 2508261801180309870,
    "state": 0
  }
}

Possible Error Codes

CodeDescriptionWhen it happens
00018Currency is invalidcurrency unsupported
00006Access deniedNo valid authClient, or the address does not belong to the caller
00008Token is invalidDTC-MFA-Data header present but its token does not match a genuinely completed sub-account face-auth session
25012Invalid recipient address IDrecipientAddressId missing/not found
25006Withdrawal request unsuccessfulTier-3 cooldown active, or the processing pipeline rejected the request
19999Reference number exists / withdrawal request failed (other)Duplicate referenceNo, or an unexpected exception during withdrawal

3.5.8 Get Crypto Transaction

Endpoint

[GET] /wallet/v1/crypto-txn/{txnId}

Description

Get crypto transaction details by transaction ID. Same object shape as Inquiry Crypto Transactions / Get Crypto Transaction by Reference Number / Get Crypto Transaction by txnHash — clientId is populated (not scrubbed) on this path.

Path Variables

NameTypeRequiredDescription
txnIdLongMUnique transaction ID

Response Body Sample — same fields as Inquiry Crypto Transactions:

{
  "header": {
    "success": true
  },
  "result": {
    "id": 2604081724300340700,
    "type": 2,
    "state": 101,
    "clientId": 8522120036,
    "mainNet": 3,
    "amount": 200,
    "currency": "USDC",
    "transactionFee": 114.22,
    "gasFee": 0,
    "txnHash": null,
    "referenceNo": null,
    "remark": null,
    "operator": "evelyn.kong@dtcpay.com",
    "recipientAddressId": 63911479,
    "recipientAddress": "0x29451adE1244Ce8F6E6fBA87E7A27c06dbc07249",
    "senderAddressId": null,
    "senderAddress": null,
    "createdAt": "2026-04-08 17:24:30",
    "updatedAt": "2026-04-08 17:24:31"
  }
}

Possible Error Codes

CodeDescriptionWhen it happens
00006Access deniedNo valid authClient, or the transaction belongs to another client
25005The transaction hash is invalidNo transaction with that ID (yes, this is the code the service returns for a not-found by-ID lookup too)

3.5.9 Get Crypto Transaction by Reference Number

Endpoint

[GET] /wallet/v1/crypto-txn/reference-number/{referenceNo}

Same object shape and error handling pattern as Get Crypto Transaction. referenceNo is a path variable.

Possible Error Codes

CodeDescriptionWhen it happens
00006Access deniedNo valid authClient
00999ReferenceNo not existedNo transaction with that reference number for this client

3.5.10 Get Crypto Transaction by txnHash

Endpoint

[GET] /wallet/v1/crypto-txn/txnhash/{txnHash}

Same object shape as Get Crypto Transaction. txnHash is a path variable.

Possible Error Codes

CodeDescriptionWhen it happens
00006Access deniedNo valid authClient, or the transaction belongs to another client
25001Transaction not foundNo transaction with that hash

3.6 Fiat

3.6.1 Sequence Diagram (Deposit)

Sequence diagram: see the interactive version in the dtcpay developer portal (PDF export available on request).

3.6.2 Sequence Diagram (Withdraw)

Sequence diagram: see the interactive version in the dtcpay developer portal (PDF export available on request).

3.6.3 Get Fiat Transaction

Endpoint

[GET] /wallet/v1/fiat-txn/{txnId}

Description

Get fiat transaction details by transaction ID. Same object shape as Inquiry Fiat Transactions / Fiat Withdraw / Fiat Deposit / Get Fiat Transaction by Reference Number — one unified FiatTransactionResp used by all five paths.

Path Variables

NameTypeRequiredDescription
txnIdLongMTransaction ID

Response Parameters

NameTypeRequiredDescription
idLongMTransaction ID
clientIdLongMClient ID who owns this transaction
typeEnumMTransaction type. Refer to Enum List — FiatTransactionType
stateEnumMTransaction state. Refer to Enum List — FiatTransactionState
currencyStringMTransaction currency, ISO 4217
amountDecimalMTransaction amount
transactionFeeDecimalOTransaction fee
senderAccountIdLongOSender account ID
recipientAccountIdLongORecipient account ID
recipientAmountDecimalOAmount credited to the recipient (when the sender pays the fee)
referenceNoStringOReference number
operatorStringOOperator who created the transaction
approverStringOApprover who authorized the transaction
remarkStringOTransaction remark
purposeEnumOTransfer purpose. Refer to Enum List — TransferPurpose
sourceOfIncomeEnumOSource of income
createdAtStringMCreated timestamp, yyyy-MM-dd HH:mm:ss
completedAtStringOCompleted timestamp, yyyy-MM-dd HH:mm:ss
updatedAtStringMLast updated timestamp, yyyy-MM-dd HH:mm:ss

Response Body Sample

{
  "header": {
    "success": true
  },
  "result": {
    "id": 2604081409370246000,
    "clientId": 8522120036,
    "type": 8,
    "state": 101,
    "currency": "SGD",
    "amount": 23,
    "transactionFee": 0,
    "senderAccountId": 489,
    "recipientAccountId": 11957,
    "recipientAmount": 23,
    "referenceNo": null,
    "operator": null,
    "approver": null,
    "createdAt": "2026-04-08 14:09:37",
    "updatedAt": "2026-04-08 14:09:38"
  }
}

Possible Error Codes

CodeDescriptionWhen it happens
00006Access deniedNo valid authClient, or the transaction belongs to another client
21999Invalid Transaction IdNo transaction with that ID

3.6.4 Inquiry Fiat Transactions

Endpoint

[POST] /wallet/v1/fiat-txn/inquiry

Same response object shape as Get Fiat Transaction.

Request Parameters

NameTypeRequiredDescription
stateEnumOTransaction state filter
typeEnumOTransaction type filter
currencyStringOCurrency filter, ISO 4217
createTimeFromStringOStart date, format yyyy-MM-dd or yyyy-MM-dd HH:mm:ss, truncated to start of day
createTimeToStringOEnd date, same formats, truncated to end of day
page.currentIntegerMPage number
page.sizeIntegerMPage size, max 100

Request Body Sample

{
  "query": {
    "state": 101,
    "type": 1,
    "currency": "USD",
    "createTimeFrom": "2026-01-01",
    "createTimeTo": "2026-01-31"
  },
  "page": {
    "current": 1,
    "size": 20
  }
}

Response Body Sample — array of the object in Get Fiat Transaction:

{
  "header": {
    "success": true
  },
  "resultList": [
    {
      "id": 2604081409370246000,
      "clientId": 8522120036,
      "type": 8,
      "state": 101,
      "currency": "SGD",
      "amount": 23,
      "transactionFee": 0,
      "senderAccountId": 489,
      "recipientAccountId": 11957,
      "operator": "evelyn.kong@dtcpay.com",
      "approver": "evelyn.kong@dtcpay.com",
      "purpose": 99,
      "createdAt": "2026-04-08 14:09:37",
      "updatedAt": "2026-04-08 14:09:38"
    }
  ],
  "pagination": {
    "current": 1,
    "size": 20,
    "total": 2006
  }
}

Possible Error Codes

CodeDescriptionWhen it happens
00999Query Fiat transaction failedUnexpected exception

3.6.5 Get Fiat Withdrawal Fee

Endpoint

[POST] /wallet/v1/fiat-txn/withdraw-fee

Description

Calculate the applicable fee for a fiat withdrawal to a specific recipient bank account.

Request Parameters

NameTypeRequiredDescription
recipientRemitInfoIdLongMRecipient bank account (remit info) ID
amountDecimalMWithdrawal amount

Request Body Sample

{
  "query": {
    "recipientRemitInfoId": 12,
    "amount": 1.35
  }
}

Response Parameters

NameTypeRequiredDescription
feeDecimalMTransaction fee, denominated in the recipient bank account's currency (recipientRemitInfo.currency) — the same currency as the withdrawal amount, not necessarily the caller's own wallet currency

Response Body Sample

{
  "header": {
    "success": true
  },
  "result": {
    "fee": 25.0
  }
}

Possible Error Codes

CodeDescriptionWhen it happens
21002Invalid Recipient Bank AccountrecipientRemitInfoId not found, disabled, or not owned by the caller

3.6.6 Fiat Withdraw

Endpoint

[POST] /wallet/v1/fiat-txn/withdraw

Description

Withdraw fiat from the caller's wallet to a bank account previously added via Add Bank Account. Same response object shape as Get Fiat Transaction.

DTC-MFA-Data header: required when the caller is an institution with sub-account MFA enabled, enforced identically to the legacy /openapi/v1/fiat/withdraw endpoint — both the requirement-gating interceptor (openApiMfaInterceptor) and the service-layer check that the supplied token corresponds to a genuinely completed face-auth session (OpenApiMfaService.isSubAccountAuthTokenValid, same call legacy makes) are wired identically.

Request Parameters

NameTypeRequiredDescription
amountDecimalMAmount to withdraw
currencyStringMCurrency code (e.g. USD, EUR)
typeEnumMFiat transaction type. Refer to Enum List — FiatTransactionType. 2 = Withdraw to own account (no file needed); 3 = Invoice payment to a third party (fileIds required)
recipientAccountIdLongMRecipient bank account ID (the ID returned by Add Bank Account / the id inside Get Client Own Bank Accounts)
fileIdsArrayCSupporting invoice file IDs from Upload File by Token. Required when type=3 (INVOICE), file status must be NORMAL
referenceNoStringOYour system's unique ID, used for idempotency
recipientAmountDecimalOUse this instead of relying on the default fee split if the sender should pay the fee (recipient receives exactly this amount)

Request Body Sample

{
  "query": {
    "amount": 23.33,
    "currency": "USD",
    "type": 3,
    "recipientAccountId": 2,
    "referenceNo": "test-referenceNo",
    "fileIds": [
      3
    ]
  }
}

Response Body Sample — same shape as Get Fiat Transaction:

{
  "header": {
    "success": true
  },
  "result": {
    "id": 220124173604595,
    "clientId": 1618551970036,
    "type": 3,
    "state": 101,
    "currency": "USD",
    "amount": 23.33,
    "transactionFee": 0,
    "recipientAccountId": 2,
    "referenceNo": "test-referenceNo",
    "operator": "[1618551970036] test",
    "approver": "[1618551970036] test",
    "createdAt": "2026-01-24 17:36:04",
    "updatedAt": "2026-01-24 17:36:04"
  }
}

Possible Error Codes

CodeDescriptionWhen it happens
00011Your balance is insufficient. Please adjust the amount or select a different wallet.Caller's wallet balance < amount
00008Token is invalidDTC-MFA-Data header present but its token does not match a genuinely completed sub-account face-auth session
00999RecipientAccountId is empty / Remit info not exist / Fiat Transaction type incorrectVarious validation failures, see message
20999ReferenceNo existsDuplicate referenceNo for this client
21999Withdrawal request unsuccessfulValidationException/unexpected exception during withdrawal

3.6.7 Fiat Deposit

Endpoint

[POST] /wallet/v1/fiat-txn/deposit

Description

Deposit fiat into the caller's wallet, after transferring funds offline to dtcpay's own remit account (see Get Fiat Deposit Target Bank Account). Same response object shape as Get Fiat Transaction.

Request Parameters

NameTypeRequiredDescription
amountDecimalMAmount to deposit, must be > 0
currencyStringMCurrency code (e.g. USD, EUR)
referenceNoStringOReference number for the transaction, used for idempotency
fileIdsArrayOSupporting receipt file IDs (optional but recommended), file status must be NORMAL

Request Body Sample

{
  "query": {
    "amount": 1.0,
    "referenceNo": "saf5354534",
    "currency": "USD",
    "fileIds": [
      11374
    ]
  }
}

Response Body Sample — same shape as Get Fiat Transaction:

{
  "header": {
    "success": true
  },
  "result": {
    "id": 250321145247611,
    "clientId": 1721717098145,
    "type": 1,
    "state": 101,
    "currency": "USD",
    "amount": 1.0,
    "transactionFee": 0,
    "recipientAccountId": 408,
    "createdAt": "2026-03-21 14:52:47",
    "updatedAt": "2026-03-21 14:52:47"
  }
}

Possible Error Codes

CodeDescriptionWhen it happens
20002Wallet account does not existNo wallet account for this client + currency
20004Wallet account is inactiveCaller's wallet is not ACTIVE
21002Validation erroramount/currency missing, amount ≤ 0, or no valid dtcpay recipient bank account configured for currency
50011Balance have exceed the limitDeposit would push the caller's wallet balance over the institution limit

3.6.8 Get Fiat Transaction by Reference Number

Endpoint

[GET] /wallet/v1/fiat-txn/reference-number/{referenceNo}

Same object shape as Get Fiat Transaction. referenceNo is a path variable.

Possible Error Codes

CodeDescriptionWhen it happens
00006Access deniedNo valid authClient
00999ReferenceNo not existNo transaction with that reference number for this client

3.7 Webhook Service

DTC supports webhook callbacks for FIAT_TXN and CRYPTO_TXN events. Configure the webhook URL on the API Key Management page.

Transport and Payload: HTTP POST, JSON body, UTF-8, Content-Type: application/json; charset=utf-8. Sent to exactly the URL configured in API Key Management — no path suffix is appended.

Acknowledgement: an attempt succeeds only if the request completes, returns HTTP 200, and the response body is exactly the plain-text string OK (no quotes). DTC retries up to 6 times after the initial attempt (7 total), with delays 15s/30s/60s/120s/240s/480s (best-effort, may run later). Webhook endpoints must be idempotent — the same event may be delivered more than once; dedupe on eventId.

Signature Verification: HMAC-SHA512 + Base64, using the Sign Key, but not the same string-to-sign as Request Signature (that scheme is for inbound API calls; this is DTC signing an outbound webhook). The string signed is POST + the exact configured webhook URL + data only (never the whole envelope — event/clientId/signature are not part of the signed string), with data's JSON keys recursively sorted before signing, and no timestamp component. To verify: take the data object exactly as received, recursively sort its own keys, and rebuild the same string — do not sign the raw bytes of the whole request body, since key order there is not guaranteed to match what was actually signed.

import com.google.common.hash.Hashing;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

public class WebhookSignatureVerification {
    public static void main(String[] args) {
        String signKey = "4mt0MUecOtOEDI42WjN4BmM0";
        String method = "POST";
        String webhookUrl = "https://partner.example.com/webhooks/endpoint"; // exactly as configured in API Key Management
        // `dataJson` must be the "data" object only, with its keys recursively sorted
        // (including nested objects) — not the whole envelope, and not raw bytes as received.
        String dataJson = "{\"amount\":1250.75,\"currency\":\"USD\", ... }"; // keys sorted alphabetically

        String stringToSign = method + webhookUrl + dataJson;

        String expectedSignature = Base64.getEncoder().encodeToString(
                Hashing.hmacSha512(signKey.getBytes(StandardCharsets.UTF_8))
                        .hashBytes(stringToSign.getBytes(StandardCharsets.UTF_8))
                        .asBytes()
        );
        // compare expectedSignature against the "signature" field inside the webhook payload
    }
}

Only new/default-configured clients receive this signed format. The per-client flag driving this (webhookFormatVersion) defaults to the signed format described here for clients onboarded through the current flow. A client whose flag was left unset by an older onboarding path (or explicitly reverted) instead receives an unsigned, flat, differently-shaped payload with no signature/data wrapper at all — that legacy shape is out of scope for this document; if your integration is receiving an unsigned payload, contact DTC to confirm your account's webhook configuration.

Webhook fiat transaction request

{
  "event": "FIAT_TXN",
  "clientId": 852042402430001,
  "signature": "MxrYnCm9Q7JOAvOrISf8+T2kuTW1d/w0at8aaPaoiX08VWfun3XPokVlIx1TkHXdcitls09wzfUGtXQZq23xdg==",
  "data": {
    "transactionId": 2510119876543210,
    "referenceNo": "TXN20251109001",
    "amount": 1250.75,
    "currency": "USD",
    "transactionFee": 2.51,
    "type": 1,
    "state": 200,
    "recipientAccountId": 2433434,
    "createdTime": "2025-11-09 10:23:45",
    "lastUpdatedTime": "2025-11-09 10:25:12",
    "eventId": "3f7a9c2e-5b1d-4c2a-a9f3-2d1e6b8c4f90"
  }
}

Request Parameters

NameTypeRequiredDescription
eventStringMFIAT_TXN — this envelope carries a fiat transaction event
clientIdLongMClient ID
signatureStringMSee Signature Verification above — signs data only, keys recursively sorted
data.transactionIdLongMFiat transaction ID
data.referenceNoStringMTransaction reference number, usually customer-provided
data.amountBigDecimalMTransaction amount
data.currencyStringMCurrency code. Refer to Enum List — Available Currency
data.transactionFeeBigDecimalMTransaction fee, already included in amount
data.typeEnumMRefer to Enum List — FiatTransactionType
data.stateEnumMRefer to Enum List — FiatTransactionState
data.recipientAccountIdLongCRecipient account ID; null on a deposit transaction
data.createdTimeStringMCreate time, format yyyy-MM-dd HH:mm:ss
data.lastUpdatedTimeStringMLast update time, format yyyy-MM-dd HH:mm:ss
data.eventIdStringMUnique identifier for this event — dedupe retried/duplicate deliveries on it; the same event always carries the same eventId, a new event always gets a new one

Webhook crypto transaction request

{
  "event": "CRYPTO_TXN",
  "clientId": 852042402430001,
  "signature": "MxrYnCm9Q7JOAvOrISf8+T2kuTW1d/w0at8aaPaoiX08VWfun3XPokVlIx1TkHXdcitls09wzfUGtXQZq23xd==",
  "data": {
    "transactionId": 2510119876543210,
    "referenceNo": "CRYPTO20251109001",
    "amount": 9.56,
    "transactionFee": 5,
    "currency": "USDC",
    "mainNet": 1,
    "txnHash": "0xxxxxxxx",
    "type": 1,
    "state": 200,
    "recipientAddressId": 789456123,
    "senderAddressId": 23246672,
    "createTime": "2025-11-09 14:12:30",
    "lastUpdateTime": "2025-11-09 14:18:45",
    "eventId": "3f7a9c2e-5b1d-4c2a-a9f3-2d1e6b8c4f90"
  }
}

Request Parameters

NameTypeRequiredDescription
eventStringMCRYPTO_TXN — this envelope carries a crypto transaction event
clientIdLongMClient ID
signatureStringMSee Signature Verification above — signs data only, keys recursively sorted
data.transactionIdLongMCrypto transaction ID
data.referenceNoStringMTransaction reference number, usually customer-provided
data.amountBigDecimalMTransaction amount
data.transactionFeeBigDecimalMTransaction fee, already included in amount
data.currencyStringMCurrency code. Refer to Enum List — Available Currency
data.mainNetEnumMRefer to Enum List — MainNet
data.txnHashStringCBlockchain transaction hash
data.typeEnumMRefer to Enum List — CryptoTransactionType
data.stateEnumMRefer to Enum List — CryptoTransactionState
data.recipientAddressIdLongCRecipient whitelist address ID; null on a deposit transaction
data.senderAddressIdLongCSender whitelist address ID
data.createTimeStringMCreate time, format yyyy-MM-dd HH:mm:ss
data.lastUpdateTimeStringMLast update time, format yyyy-MM-dd HH:mm:ss
data.eventIdStringMUnique identifier for this event — dedupe retried/duplicate deliveries on it; the same event always carries the same eventId, a new event always gets a new one

Response: reply with the plain-text body OK (see Acknowledgement above).

Possible Events

NameDescriptor
CRYPTO_TXNCrypto transaction event
FIAT_TXNFiat transaction event

3.8 Swap Order

Convert a wallet balance into a non-wallet payout — a PayNow/SGQR bank transfer, or a dtcpay card top-up — settled at a locked quote rate. This is a different capability from OTC (which only exchanges currency between two of the caller's own wallets) and different from Wallet/Business Transfer (which move value between two dtcpay wallets): a swap order's endpoint is outside the wallet system entirely.

Full flow:

  1. (PAY_NOW_WITHDRAW / SGQR_WITHDRAW only) QR Code Query — scan the payee's QR code to resolve qrCodeRecipientInfo (proxy type/value, or a raw qrStr).
  2. Query Quote — lock an exchange rate and get a quoteId (an alternative to Get OTC Rate when the amount you know is on the processing/wallet side rather than the payout side).
  3. Create Swap Order — submit quoteId + amount + currency + payoutMethod (+ the payout-specific info from step 1, or cardInfo for CARD_TOP_UP). dtcpay debits currency from the caller's wallet at the locked rate and settles the payout asynchronously through the underlying channel.
  4. Get Swap Order Details — poll for the order's final state, or receive it via the callbackUrl webhook supplied at creation.
  5. Cancel Swap Order — cancel while still WAITING_PAYIN (before payin funds are received); not possible once payin has started processing.

3.8.1 Sequence Diagram

Sequence diagram: see the interactive version in the dtcpay developer portal (PDF export available on request).

3.8.2 Query Quote

Endpoint

[POST] /wallet/v1/swap-order/query-quote

Description

Lock an exchange rate quote for a swap order. Provide either amount (the amount to receive on the payout side, in currency) or processingAmount (the amount to debit from the wallet, in processingCurrency) — exactly one of the two. Call this before Create Swap Order — the resulting quoteId is a required input to that endpoint.

Request Parameters

NameTypeRequiredDescription
processingCurrencyStringMCurrency of the caller's wallet to debit (the settlement/processing side)
processingAmountDecimalCAmount to debit, in processingCurrency. Mutually exclusive with amount
currencyStringMCurrency of the payout side
amountDecimalCAmount to be paid out, in currency. Mutually exclusive with processingAmount

Request Body Sample

{
  "query": {
    "processingCurrency": "USDC",
    "currency": "SGD",
    "amount": 5
  }
}

Response Parameters

NameTypeRequiredDescription
sellCurrencyStringMCurrency sold (the processing currency)
buyCurrencyStringMCurrency bought (the target/payout currency)
sellAmountDecimalMAmount of sell currency required
buyAmountDecimalMAmount of buy currency expected
rateDecimalMExchange rate
expiresAtStringMQuote expiry, yyyy-MM-dd HH:mm:ss — pass quoteId to Create Swap Order before this time. Same cache/TTL rule as Get OTC Rate (one shared quote cache): 1 day for PAYMENT-category quotes, 7 days for fiat↔stablecoin pairs, 24 hours otherwise
quoteIdLongMQuote ID, pass into Create Swap Order. The value may be negative.

Response Body Sample

{
  "header": {
    "success": true
  },
  "result": {
    "sellCurrency": "USDC",
    "buyCurrency": "SGD",
    "sellAmount": 3.92,
    "buyAmount": 5,
    "rate": 1.2765744944961372,
    "expiresAt": "2026-09-02 16:14:59",
    "quoteId": -183195842672497984
  }
}

Possible Error Codes

CodeDescriptionWhen it happens
00010Invalid parametersprocessingCurrency/currency missing, or both/neither of processingAmount/amount provided
00004Unknown API errorUnexpected exception, e.g. quote enquiry failure

3.8.3 Create Swap Order

Endpoint

[POST] /wallet/v1/swap-order

Description

Create a swap order that debits the caller's wallet of currency at the quoteId rate and settles the payout via payoutMethod. Requires a quoteId from Query Quote, called first. See the flow above for the full sequence.

Request Parameters

NameTypeRequiredDescription
quoteIdLongOQuote ID from Query Quote. The value may be negative. If it is in the same currency, this value is not required
amountDecimalMAmount to debit, must match the amount used in the quote
currencyStringMCurrency to debit (a stablecoin, e.g. USDC)
payoutMethodEnumM1=PAY_NOW_WITHDRAW, 2=CARD_TOP_UP, 3=SGQR_WITHDRAW
qrCodeRecipientInfoObjectCRequired when payoutMethod is 1 or 3
qrCodeRecipientInfo.referenceNoStringCRequired unless qrStr is provided
qrCodeRecipientInfo.proxyTypeEnumCPayNow proxy type. Refer to Enum List — ProxyType. Required unless qrStr is provided
qrCodeRecipientInfo.proxyValueStringCPayNow proxy value. Required unless qrStr is provided
qrCodeRecipientInfo.qrStrStringOFull QR code string; overrides proxyType/proxyValue when provided
cardInfoObjectCRequired when payoutMethod is 2
cardInfo.cardIdLongCCard ID to top up
callbackUrlStringOURL to POST order status changes to

Request Body Sample

{
  "query": {
    "quoteId": -7803018125589665000,
    "amount": 1,
    "currency": "USDC",
    "payoutMethod": 1,
    "qrCodeRecipientInfo": {
      "referenceNo": "REF123456",
      "proxyType": 1,
      "proxyValue": "91234567"
    },
    "callbackUrl": "https://partner.example.com/webhooks/swap-order"
  }
}

Response Parameters — same shape as Get Swap Order Details minus callbackUrl:

NameTypeRequiredDescription
idLongMSwap order ID
clientIdLongMClient ID who created the order
typeEnumMOrder type. Refer to Enum List — SwapOrderType
stateEnumMOrder state. Refer to Enum List — SwapOrderState. Always WAITING_PAYIN immediately after creation
payinTransactionIdLongOPay-in transaction ID; null until payin actually completes
payinCurrencyStringMPay-in currency
payinAmountDecimalMAmount debited from the caller
otcIdLongOBacking OTC order ID, if applicable
quoteIdLongOQuote ID used to create the order. The value may be negative.
payoutTransactionIdLongOA FiatTransaction ID for PAY_NOW/SGQR, or a CardTransaction ID for CARD_TOP_UP. Confirmed on stg: this is populated immediately at order creation (state 0 WAITING_PAYIN), not only "once settled" — it points to a placeholder transaction created in a pending state, which is then updated in place as the order progresses. Do not treat a non-null value here as proof the payout has actually settled; check state for that.
payoutAmountDecimalMAmount paid out to the recipient
payoutCurrencyStringMPayout currency
operatorStringOOperator handling the order
createdAtStringMCreation timestamp, yyyy-MM-dd HH:mm:ss
expiresAtStringMExpiration timestamp of the order, yyyy-MM-dd HH:mm:ss
updatedAtStringMLast updated timestamp, yyyy-MM-dd HH:mm:ss

Response Body Sample

{
  "header": {
    "success": true
  },
  "result": {
    "id": 2507011357170784,
    "clientId": 1733914979001,
    "type": 2,
    "state": 0,
    "payinTransactionId": null,
    "payinCurrency": "USDC",
    "payinAmount": 1.5,
    "otcId": null,
    "quoteId": 1357924680,
    "payoutTransactionId": 246813579,
    "payoutAmount": 1.89,
    "payoutCurrency": "SGD",
    "operator": "[1733914979001] partner",
    "createdAt": "2026-09-02 16:13:59",
    "expiresAt": "2026-09-02 16:15:59",
    "updatedAt": "2026-09-02 16:13:59"
  }
}

Note payoutTransactionId is already non-null here even though state: 0 (WAITING_PAYIN) — see the field note above.

Possible Error Codes

CodeDescriptionWhen it happens
11008Invalid Quote IDquoteId missing/expired/already consumed
11004Invalid amountamount missing
11020Currency is invalidcurrency missing
39002Invalid payout methodpayoutMethod missing/unrecognized
39004Reference number cannot be emptypayoutMethod 1/3 without qrCodeRecipientInfo.referenceNo (and no qrStr)
39005PayNow proxy type cannot be emptySame, missing proxyType
39006PayNow proxy value cannot be emptySame, missing proxyValue
39007Card info cannot be emptypayoutMethod 2 without cardInfo
39008Card ID cannot be emptycardInfo.cardId missing
00004Unknown API errorUnexpected exception

3.8.4 Cancel Swap Order

Endpoint

[DELETE] /wallet/v1/swap-order/{id}

Description

Cancel a swap order while it is still WAITING_PAYIN. Fails with INVALID_ORDER_STATE once payin funds have started processing.

Path Variables

NameTypeRequiredDescription
idLongMSwap order ID

Response Parameters

NameTypeRequiredDescription
idLongMSwap order ID that was cancelled
stateEnumMSwap order state after the cancel attempt — re-read from storage, so it reflects CANCELLED on success, or the order's current state if it had already moved on (e.g. a payin was received concurrently with the cancel request)

Response Body Sample

{
  "header": {
    "success": true
  },
  "result": {
    "id": 2507011357170784,
    "state": 6
  }
}

Possible Error Codes

CodeDescriptionWhen it happens
00001Failed to fetch dataNo order with that ID
39009The order status does not meet the requirementsOrder is not owned by the caller, or is not WAITING_PAYIN
39999Swap order error (other)Unexpected exception during cancel

3.8.5 Get Swap Order Details

Endpoint

[GET] /wallet/v1/swap-order/{id}

Description

Retrieve the details of a specific swap order. Same object shape as Create Swap Order's result, plus callbackUrl.

Path Variables

NameTypeRequiredDescription
idLongMSwap order ID

Response Parameters — same as Create Swap Order response, plus:

NameTypeRequiredDescription
callbackUrlStringOCallback URL provided at order creation

Response Body Sample

{
  "header": {
    "success": true
  },
  "result": {
    "id": 2507011357170784,
    "clientId": 1733914979001,
    "type": 2,
    "state": 2,
    "payinTransactionId": 123456789,
    "payinCurrency": "USDC",
    "payinAmount": 1.5,
    "otcId": 987654321,
    "quoteId": 1357924680,
    "payoutTransactionId": 246813579,
    "payoutAmount": 1.89,
    "payoutCurrency": "SGD",
    "callbackUrl": "https://partner.example.com/webhooks/swap-order",
    "operator": "[1733914979001] partner",
    "createdAt": "2026-09-02 16:13:59",
    "expiresAt": "2026-09-02 16:15:59",
    "updatedAt": "2026-09-02 16:14:20"
  }
}

Possible Error Codes

CodeDescriptionWhen it happens
00001Failed to fetch dataNo order with that ID
00006Access deniedThe order belongs to another client

3.8.6 QR Code Query

Endpoint

[POST] /wallet/v1/swap-order/qrcode/query

Description

Resolve a scanned QR code payload into recipient + amount details. Supports PayNow, SGQR, and Alipay Plus (channel=13) — the response's Alipay-specific fields (promoDetail, sdkActionPayload, etc., see below) are only populated for that channel. For PayNow/SGQR, use the result to populate qrCodeRecipientInfo on a subsequent Create Swap Order call with payoutMethod=PAY_NOW_WITHDRAW or SGQR_WITHDRAW.

Request Parameters

NameTypeRequiredDescription
qrCodeStringMFull QR code string (PayNow or SGQR format)

Request Body Sample

{
  "query": {
    "qrCode": "00020101021226370009SG.PAYNOW010120210201935231K030105204000053037025402105802SG5925DIGITAL TREASURES CENTER 6009SINGAPORE62280124FPNI-25081213324002212966304E62B"
  }
}

Response Parameters

NameTypeRequiredDescription
channelEnumMResolved payment channel
currencyStringMCurrency, usually SGD
proxyTypeEnumCPayNow proxy type. Refer to Enum List — ProxyType
proxyValueStringCPayNow proxy value
amountModifierBooleanMfalse for a fixed amount, true for a variable amount
amountDecimalOTransaction amount, present for a fixed-amount PayNow QR
noteStringOPayment note, present for PayNow
acceptorNameStringOMerchant/acceptor name
expiredTimeStringOQR code expiration time, present for SGQR
maxAmountDecimalOMaximum transaction amount, present for SGQR
isSupportedBooleanOWhether this QR/merchant is currently payable through this channel
postCodeMatchActionTypeStringOAlipay Plus only — required post-payment action related to postal-code verification, when applicable
userAgentStringOAlipay Plus only — user agent to use when following redirectUrl/paymentRedirectUrl
redirectUrlStringOAlipay Plus only — URL to redirect the payer to in order to complete the scan/authorization step
paymentAmountDecimalOAlipay Plus only — the amount the payer will actually be charged (after any promotion), in paymentAmountCurrency
paymentAmountCurrencyStringOAlipay Plus only — currency of paymentAmount
chargedAmountDecimalOAlipay Plus only — amount actually charged to the payer's Alipay account, in chargedCurrency
chargedCurrencyStringOAlipay Plus only — currency of chargedAmount
crossedAmountDecimalOAlipay Plus only — original (pre-promotion) amount shown as struck-through on the payment page
promoDetailArrayOAlipay Plus only — list of promotion/discount line items applied to this payment
promoTotalAmountDecimalOAlipay Plus only — total promotion discount, in the promotion's own currency
promoTotalAmountInOrderCurrencyDecimalOAlipay Plus only — total promotion discount, converted into the order's currency
merchantNameStringOAlipay Plus only — merchant display name
paymentRequestIdStringOAlipay Plus only — the payment gateway's own request id for this QR resolution
sdkActionPayloadStringOAlipay Plus only — opaque payload for driving the Alipay client-side SDK, when applicable
paymentExpiryTimeStringOAlipay Plus only — expiry time of the resolved payment request
paymentRedirectUrlStringOAlipay Plus only — alternate redirect URL used by some Alipay Plus payment flows
quotePriceDecimalOAlipay Plus only — FX rate applied between baseCurrency and quoteCurrency, when a currency conversion is involved
txnIdLongOAlipay Plus only — the gateway's own transaction id for this QR resolution
baseCurrencyStringOAlipay Plus only — the currency quotePrice converts from
quoteCurrencyStringOAlipay Plus only — the currency quotePrice converts to
isInternalTransferBooleanOWhether this would settle as an internal (same-institution) transfer rather than an external payout

Response Body Sample

{
  "header": {
    "success": true
  },
  "result": {
    "channel": 1,
    "currency": "SGD",
    "proxyType": 1,
    "proxyValue": "91234567",
    "amountModifier": false,
    "amount": 10.0,
    "acceptorName": "DIGITAL TREASURES CENTER"
  }
}

Possible Error Codes

CodeDescriptionWhen it happens
00003Params check failedqrCode missing/blank
38001Invalid QR codeqrCode not a recognized PayNow/SGQR format
00004Unknown API errorUnexpected exception

3.9 Bank Account

3.9.1 Add Bank Account

Endpoint

[POST] /wallet/v1/bank-account

Description

Add a beneficiary bank account (remit info) for the caller. Used to store recipient bank details for fiat withdrawals or invoice payments.

Request Parameters

NameTypeRequiredDescription
ownerIdLongMClient ID who owns this bank account; must match the authenticated client
typeEnumM1=OWN (own account), 2=PAYEE (third party). Refer to Enum List — RemitInfoType
currencyStringMCurrency, ISO 4217
bankTypeEnumMRefer to Enum List — BankType
beneficiaryTypeEnumM1=INDIVIDUAL, 2=CORPORATE
beneficiaryFirstNameStringCRequired when beneficiaryType=INDIVIDUAL
beneficiaryLastNameStringCRequired when beneficiaryType=INDIVIDUAL
beneficiaryNameStringOAuto-generated for INDIVIDUAL; manual for CORPORATE; overwritten by the client's own name when type=OWN
beneficiaryAccountStringMAccount number, strictly alphanumeric (no hyphens/spaces)
beneficiaryBankAccountTypeNumberO1=CAK (Current Account), 2=OAK (Ordinary Account), 3=SAK (Savings Account)
beneficiaryAddressStringOBeneficiary address
beneficiaryBankNameStringMBank name
beneficiaryBankAddressStringMBank address
beneficiaryBankCountryStringMISO 3166-1 alpha-3 code (e.g. SGP, USA, GBR), must be valid
beneficiaryBankSwiftCodeStringCRequired when bankType=SWIFT, must be a valid SWIFT code
ibanStringOFor SEPA transfers
relationshipEnumORefer to Enum List — Relationship
isIntermediaryRequiredBooleanODefault false
intermediaryBankCountryStringCISO 3166-1 alpha-3, required if intermediary bank info is provided
intermediaryBankNameStringCRequired when isIntermediaryRequired=true
intermediaryBankSwiftCodeStringCRequired when isIntermediaryRequired=true
intermediaryBankAddressStringO—
intermediaryBankCityStringO—
enabledBooleanOWhether this bank account is active; defaults to enabled when adding a new one. Same field used by Update Bank Account/Disable Bank Account

Request Body Sample

{
  "query": {
    "ownerId": 1669025274037,
    "type": 1,
    "currency": "SGD",
    "bankType": 1,
    "beneficiaryType": 2,
    "beneficiaryAccount": "1231231123",
    "beneficiaryAddress": "12 Marina Blvd",
    "beneficiaryBankAddress": "12 Marina Blvd, Marina Bay Financial Centre",
    "beneficiaryBankCountry": "SGP",
    "beneficiaryBankName": "DBS Bank Ltd",
    "beneficiaryBankSwiftCode": "DBSSSGSGXXX",
    "beneficiaryFirstName": "Mirya",
    "beneficiaryLastName": "Aa",
    "relationship": 0,
    "isIntermediaryRequired": false
  }
}

Response Parameters

NameTypeRequiredDescription
idLongMID of the newly created bank account record

Response Body Sample

{
  "header": {
    "success": true
  },
  "result": {
    "id": 1234
  }
}

Possible Error Codes

CodeDescriptionWhen it happens
00006Access deniedownerId does not match the authenticated client
00007Failed to add dataPersistence failure
24007Invalid swift codebeneficiaryBankSwiftCode fails validation
24009Invalid country codebeneficiaryBankCountry/intermediaryBankCountry not a valid alpha-3 code
24015Adding this recipient bank account is not supportedBlacklist match
20999Other errorClient status (e.g. suspended) blocks account creation

3.9.2 Update Bank Account

Endpoint

[PUT] /wallet/v1/bank-account/{id}

Description

Update an existing bank account. Cannot update while there are pending transactions (UNPAID/PARTIAL_PAID/PROCESSING) associated with this account.

Id stability: in the common case (the update doesn't change which MatchMove-managed virtual account the record is linked to), the record is updated in place and the response id equals the path {id} you passed in. In two narrower cases — moving a SGD bank account away from a currency backed by a MatchMove virtual account, or updating a bank account whose currency requires re-synchronizing its MatchMove recipient — the underlying record is instead replaced (the old row is disabled and a new one created), and the response id is a new, different id. If you persist this id elsewhere (e.g. as recipientRemitInfoId for Get Fiat Withdrawal Fee or for a later Disable Bank Account call), always use the id returned by the most recent update, not the one you originally created the account with.

Path Variables

NameTypeRequiredDescription
idLongMBank account ID to update

Request Parameters — same body shape as Add Bank Account.

Response Parameters

NameTypeRequiredDescription
idLongMID of the bank account record after the update — see the id-stability note above; usually the same as the path {id}, but a new id in the two MatchMove-resync cases

Response Body Sample

{
  "header": {
    "success": true
  },
  "result": {
    "id": 1234
  }
}

Possible Error Codes

CodeDescriptionWhen it happens
00006Access deniedownerId does not match the authenticated client
00002Failed to update dataPersistence failure
24007Invalid swift code—
24009Invalid country code—
25002Recipient information already existsThe updated fields duplicate another existing bank account
25005The recipient has a transaction in progress and cannot be removed nowA pending transaction blocks the update

3.9.3 Disable Bank Account

Endpoint

[POST] /wallet/v1/bank-account/disable

Description

Disable (soft-delete) a bank account. The record is not physically deleted, only marked enabled=false.

Request Parameters

NameTypeRequiredDescription
idLongMBank account ID to disable

Request Body Sample

{
  "query": {
    "id": 1234
  }
}

Response Parameters

NameTypeRequiredDescription
idLongMID of the bank account that was disabled

Response Body Sample

{
  "header": {
    "success": true
  },
  "result": {
    "id": 1234
  }
}

Possible Error Codes

CodeDescriptionWhen it happens
00006Access deniedid does not belong to the authenticated client
00010Invalid parametersid missing
25999Bank account error (other)Unexpected exception during disable

3.9.4 Get Fiat Deposit Target Bank Account

Endpoint

[GET] /wallet/v1/bank-account/deposit-target/{currency}

Description

Get dtcpay's own remit info (deposit account) for a specific currency — the account clients transfer funds to when depositing fiat. Single object result (there is exactly one target account per currency).

Path Variables

NameTypeRequiredDescription
currencyStringMFiat currency. Supported: SGD, USD, EUR, GBP, HKD, JPY, AUD, CAD, CNH, MYR, AED

Response Parameters — same fields as Get Client Own Bank Accounts below.

Response Body Sample

{
  "header": {
    "success": true
  },
  "result": {
    "id": 5678,
    "ownerId": 1,
    "currency": "USD",
    "beneficiaryType": 2,
    "beneficiaryName": "Digital Treasures Center Pte Ltd",
    "beneficiaryAccount": "7001234567",
    "beneficiaryAddress": "7 Straits View, Marina One East Tower, Singapore 018936",
    "beneficiaryBankName": "DBS Bank Ltd",
    "beneficiaryBankAddress": "12 Marina Boulevard, Singapore 018982",
    "beneficiaryBankCountry": "SGP",
    "beneficiaryBankSwiftCode": "DBSSSGSGXXX",
    "enabled": true,
    "isIntermediaryRequired": false,
    "updatedAt": "2026-01-01 00:00:00"
  }
}

Possible Error Codes

CodeDescriptionWhen it happens
21001Fiat transaction error — no supporting bank account for the specified currencydtcpay has no configured deposit target for that currency

3.9.5 Get Client Own Bank Accounts

Endpoint

[GET] /wallet/v1/bank-account/client-own/{currency}

Description

Get the list of the caller's own bank accounts for a currency. Returns a list (resultList), not a single object — a client can have more than one bank account per currency.

Path Variables

NameTypeRequiredDescription
currencyStringMFiat currency, ISO 4217

Response Parameters (array in resultList)

NameTypeRequiredDescription
idLongMBank account ID
ownerIdLongMOwner client ID
currencyStringMCurrency
typeEnumORefer to Enum List — RemitInfoType. Nullable at the data level — not every stored bank account has this set
bankTypeEnumORefer to Enum List — BankType
beneficiaryTypeEnumMRefer to Enum List — BeneficiaryType
beneficiaryFirstNameStringO—
beneficiaryLastNameStringO—
beneficiaryNameStringMFull beneficiary name
beneficiaryAccountStringMAccount number
beneficiaryAddressStringO—
beneficiaryBankNameStringM—
beneficiaryBankAddressStringO—
beneficiaryBankCountryStringMISO 3166-1 alpha-3
beneficiaryBankSwiftCodeStringO—
ibanStringOFor SEPA transfers
enabledBooleanMWhether this account is enabled
isIntermediaryRequiredBooleanO—
intermediaryBankCountryStringO—
updatedAtStringMLast update timestamp, yyyy-MM-dd HH:mm:ss

Response Body Sample

{
  "header": {
    "success": true
  },
  "resultList": [
    {
      "id": 2,
      "ownerId": 1669025274037,
      "type": 1,
      "currency": "USD",
      "bankType": 1,
      "beneficiaryType": 2,
      "beneficiaryName": "DIGITAL TREASURES CENTER PTE. LTD.",
      "beneficiaryAccount": "2132415",
      "beneficiaryAddress": "7 Straits View, Marina One East Tower #05-01",
      "beneficiaryBankName": "MAYBANK SINGAPORE LIMITED",
      "beneficiaryBankAddress": "MAYBANK TOWER, 2 BATTERY ROAD SINGAPORE 049907",
      "beneficiaryBankCountry": "SGP",
      "beneficiaryBankSwiftCode": "MBBESGS2XXX",
      "enabled": true,
      "isIntermediaryRequired": false,
      "updatedAt": "2022-09-08 19:49:18"
    }
  ]
}

Possible Error Codes

CodeDescriptionWhen it happens
00006Access deniedNo valid authClient
00999Failed to fetch dataUnexpected exception

3.10 File

3.10.1 Get Upload Token

Endpoint

[POST] /wallet/v1/file/upload-token

Description

Generate a temporary token authorizing an upcoming file upload. Use whenever a document needs to be uploaded. The token must be included in the Upload File by Token request, and is single-use — it is consumed on that call.

When a file is required

  • Fiat Deposit — optional but recommended. When uploading, set documentType=200 (Fiat Deposit Receipt).
  • Fiat Withdrawal — type=2 (Withdraw to own account): not required. type=3 (Invoice Payment to a third party): required, documentType=201 (Invoice).
  • Business Transfer — required (fileIds is mandatory on that endpoint regardless of documentType).

Request Parameters

NameTypeRequiredDescription
documentTypeEnumMRefer to Enum List — FileDocumentType

Request Body Sample

{
  "query": {
    "documentType": 200
  }
}

Response Parameters

NameTypeRequiredDescription
tokenStringMUpload token, single-use
expiresAtStringMToken expiry timestamp, yyyy-MM-dd HH:mm:ss — currently 10 minutes from issuance

Response Body Sample

{
  "header": {
    "success": true
  },
  "result": {
    "token": "5e8a0b2afcc24abfa207c0b902d82f00",
    "expiresAt": "2026-09-13 10:15:30"
  }
}

Possible Error Codes

CodeDescriptionWhen it happens
00010Invalid parametersdocumentType missing
00006Access deniedNo valid authClient

3.10.2 Upload File by Token

Endpoint

[POST] /wallet/v1/file/upload-by-token/{token}

Description

Upload a document using a token from Get Upload Token. This endpoint uses token-based auth instead of the usual OAuth bearer token.

Path Variables

NameTypeRequiredDescription
tokenStringMToken obtained from Get Upload Token

Request: multipart/form-data

NameTypeRequiredDescription
fileFileMThe file to upload

Supported MIME types (detected server-side from the file's actual content — not trusted from the client's declared Content-Type):

MIME typeTypical extension
image/jpeg.jpg / .jpeg
image/png.png
image/heif, image/heif-sequence.heif
image/heic, image/heic-sequence.heic
video/mp4.mp4
application/pdf.pdf

Any other detected content type is rejected with MIME_TYPE_NOT_SUPPORT (see error table below).

Response Parameters

NameTypeRequiredDescription
idLongMFile ID
statusEnumMRefer to Enum List — FileStatus. Always 3 (NORMAL) on a successful upload
mimeTypeStringMThe MIME type detected server-side from the uploaded content

Response Body Sample

{
  "header": {
    "success": true
  },
  "result": {
    "id": 11368,
    "status": 3,
    "mimeType": "image/jpeg"
  }
}

Possible Error Codes

CodeDescriptionWhen it happens
00008Token is invalidtoken missing, already consumed, or expired (>10 minutes)
14006File size is larger than 2MBThe token's configured filesizeLimit was exceeded
14005File type not supportThe uploaded content is not one of the MIME types listed above
14004File upload failedAny other upload failure (S3 write failure, etc.)

Token-consumption ordering: the upload token is deleted from its backing store immediately after being looked up — before the file-size check, MIME-type detection, or the actual upload runs. So if the upload fails for any reason (oversized file, unsupported type, a transient S3 error), the token is already gone; retrying with the same token returns 00008 (Token is invalid), and the caller must request a fresh token via Get Upload Token before trying again.


4 Appendix A: Data Structure

Every response object referenced above by name is fully specified inline at its first use (see the Response Parameters table for each endpoint). This appendix only lists shared enum-driven categories not tied to one specific endpoint.

QuoteCategory

IDDescription
1Payment
2Otc
3Payout
4Deposit
5Card Spending
6Account Top Up
7Crypto Purchase
8Card Base Currency Conversion
9Card Withdraw

5 Appendix B: Error Structure

All DTC Pay APIs return errors in a consistent structure, under the header field:

{
  "header": {
    "success": false,
    "errCode": "<unique error code>",
    "errMsg": "<description of the error>"
  }
}

Every endpoint above lists its own specific error codes with the condition that triggers each one; the table below lists client-status codes that can be returned by essentially any endpoint (they gate at the account level, before the endpoint's own business logic runs):

5.1 Client Status Possible Error Codes

CodeDescription
00034Your account is suspended, please contact us at {support email}.
00035Please submit the KYC information first
00036Your account verification is pending. Please complete KYC to unlock full access.
00037Your account is rejected. Please contact at {support email}.
00041Your account is currently inactive. Please contact us at {support email} for assistance.
00042Your account is currently deactivated. Please contact us at {support email} for assistance.
00043Your account is currently restricted. Please contact us at {support email} to resolve this issue.
00044Your account is currently terminated. Please contact us at {support email} for more information.
00045Your account is currently off-board. Please contact us at {support email}.
00060Your account is blocked until {date} for security reasons. Please contact us at {support email} for assistance.

5.2 Common Error Codes (any endpoint)

CodeDescription
00001Failed to fetch data
00002Failed to update data
00003Failed to delete data
00004Unknown API error
00006Access denied
00007Failed to add data
00008Token is invalid
00010Invalid parameters
00011Your balance is insufficient. Please adjust the amount or select a different wallet.
00013Client ID is invalid
00018Currency is invalid
00027Params check failed
00054Duplicate submission detected, please try again later
00055Too many requests, please try again later
00999Other error (message varies by context — see the specific error table for that endpoint)

6 Appendix C: Enum List

ActivityType (used by Inquiry Wallet Balance History's type)

NameIDDescriptor
INIT0Data Init
PAYMENT1Payment Settlement
REMIT2Remittance
RESERVE3Reserve
OTC4Over-the-Counter
FIAT_WITHDRAWAL5Fiat Withdrawal
FIAT_DEPOSIT6Fiat Deposit
OTC_COMMISSION7OTC Commission
POBO8Payment On Behalf Of
CRYPTO_WITHDRAWAL9Stablecoin Withdrawal
CRYPTO_DEPOSIT10Stablecoin Deposit
ADJUSTMENT11Adjustment
OTC_BONUS12OTC Bonus
DTC_WALLET13DTC Wallet Payment
CRYPTO_PAYMENT_REFUND14Stablecoin Payment Refund
TOP_UP_CARD15Top Up Card
SATOSHI_TEST16Satoshi Test
CRYPTO_WITHHELD_REFUND17Stablecoin Withheld Refund
PURCHASE_CRYPTO18Purchase Stablecoin
TOP_UP_WALLET19Top Up Wallet
CARD_PAYMENT_REFUND20Card Payment Refund
TRANSFER_IN21Transfer In
TRANSFER_OUT22Transfer Out
SWAP_CANCEL23Swap Cancel
BOUNCE_BACK24Refund
SCAN_PAY28Scan Pay
CARD_TRANSACTION_REVERSAL29Card Transaction Reversal
CARD_APPLICATION_FEE_DEBIT30Application Fee Debit
CARD_DELIVERY_FEE_DEBIT31Delivery Fee Debit
CARD_APPLICATION_FEE_CREDIT32Application Fee Credit
CARD_DELIVERY_FEE_CREDIT33Delivery Fee Credit
CARD_APPLICATION_FEE_REFUND34Application Fee Refund
CARD_DELIVERY_FEE_REFUND35Delivery Fee Refund

CryptoTransactionType

NameIDDescriptor
DEPOSIT1Deposit
WITHDRAW2Withdraw
SATOSHI3Satoshi Test
PAYMENT4Payment (currently not in use)
SETTLEMENT5Settlement (currently not in use)
TRANSFER_IN6Transfer In
TRANSFER_OUT7Transfer Out
CARD_FEE_DEBIT8Card Fee Debit
CARD_FEE_CREDIT9Card Fee Credit
CARD_FEE_REFUND10Card Fee Refund

CryptoTransactionState

NameIDDescriptor
PENDING0Pending Approval
AUTHORIZED101Authorized
RISK_WITHHELD102Risk Withheld
PROCESSING110Processing
COMPLETED200Completed
REFUNDED401Refunded
REJECTED900Rejected
CLOSED990Closed

FiatTransactionType

NameIDDescriptor
DEPOSIT1Deposit
WITHDRAW2Withdraw
INVOICE_PAYMENT3Invoice Payment
TOP_UP4Top-up
TRANSFER_IN5Transfer In
TRANSFER_OUT6Transfer Out
BOUNCE_BACK7Refund
QR_PAYMENT8QR Payment
CARD_FEE_DEBIT9Card Fee Debit
CARD_FEE_CREDIT10Card Fee Credit
CARD_FEE_REFUND11Card Fee Refund

FiatTransactionState

NameIDDescriptor
PENDING0Pending Approval
AUTHORIZED101Authorized
PROCESSING110Processing
COMPLETED200Completed
REJECTED900Rejected
CLOSED990Closed

OtcStatus

NameIDDescriptor
INITIAL1OTC Order Initial
COMPLETED5Order Completed
EXPIRED10Rate Expired
CANCELLED11Order Cancelled

RaaStatus

NameIDDescriptor
PENDING1Not completed — prompt the assessment and withhold RAA-gated actions
COMPLETED2Completed — RAA-gated actions may proceed
UNDER_REVIEW3Anomaly under manual review — withhold RAA-gated actions

QuestionOption

IDDescriptor
AFirst option
BSecond option
CThird option
DFourth option

SupportedLanguage

NameIDDescriptor
ENENEnglish
ZH_CNZH-CN简体中文 (Simplified Chinese)
ZH_HKZH-HK繁體中文 (Traditional Chinese)
VI_VNVI-VNTiếng Việt (Vietnamese)
FR_FRFR-FRFrançais (French)

MainNet

NameIDDescriptor
BTC1Bitcoin
POLYGON2Polygon
ERC203Ethereum
TRC206Tron
BEP207BNB Smart Chain (BSC)
BASE8Base
SOLANA9Solana
ARBITRUM10Arbitrum One

EVM-compatible group (same 0x + 40-hex-char address format): POLYGON, ERC20 (Ethereum), BEP20 (BSC), BASE, ARBITRUM — see Get Supported Networks By Address.

RemitInfoType

IDDescriptor
1Client's own (OWN)
2Client's payee (PAYEE)

BankType

IDDescriptor
1SWIFT
2ACH
3FPS
4SEPA
5HK_FPS

BeneficiaryType

IDDescriptor
1Individual
2Corporate

Relationship

NameIDDescriptor
PARENT1Parent
CHILD2Child
SPOUSE3Spouse
BUYER4Buyer
SELLER5Seller
SELF6Self
SIBLING7Sibling
EMPLOYEE8Employee
FRIEND9Friend
IMPORTER_EXPORTER10Importer/Exporter
CONSULTANT11Consultant
CONTRACTOR12Contractor

Available Currency

CurrencyDescription
SGDSingapore Dollar
USDUnited States Dollar
GBPPound Sterling
EUREuro
HKDHong Kong Dollar
JPYJapanese Yen
AUDAustralian Dollar
CADCanadian Dollar
CNHChinese Yuan (offshore)
MYRMalaysian Ringgit
AEDUnited Arab Emirates Dirham
USDTUSD Tether
USDCUSD Coin
WUSDWorldwide USD
FDUSDFirst Digital USD

PayoutMethod

NameIDDescriptor
PAY_NOW_WITHDRAW1PayNow withdraw
CARD_TOP_UP2Card top up
SGQR_WITHDRAW3SGQR withdraw

ProxyType

NameIDDescriptor
MOBILE1PayNow / Mobile
UEN2PayNow / UEN
NRIC3PayNow / NRIC/FIN
NETS4SGQR / NETS

SwapOrderType

NameIDDescriptor
SWAP_AND_SEND0Swap and Send
DEPOSIT_AND_SEND1Deposit and Send
DEPOSIT_AND_SWAP2Deposit and Swap
SWAP_AND_TOPUP_CARD3Swap and Topup Card
DEPOSIT_AND_TOP_UP_WALLET4Deposit and Top up Wallet

SwapOrderState

NameIDDescriptor
WAITING_PAYIN0Waiting Payin
PAYOUT_PROCESSING1Payout Processing
COMPLETED2Completed
COMPLETED_WITH_EXCESS_AMOUNT3Completed With Excess Amount
FAILED4Failed
EXPIRED5Expired
CANCELLED6Cancelled

FileDocumentType

NameIDDescriptor
FIAT_DEPOSIT_RECEIPT200Fiat Deposit Receipt
INVOICE201Invoice

FileStatus

NameIDDescriptor
UPLOAD_FAILED2Upload Failed
NORMAL3Normal
DELETED5Deleted

TransferPurpose

NameIDDescriptor
ACCOUNT_TOP_UP0Account Top Up
ADVANCE_PAYMENT1Advance Payment
ALIMONY_PAYMENT2Alimony Payment
CARPARK_CHARGES3Carpark Charges
COLLECTION_PAYMENT4Collection Payment
CREDIT_CARD_PAYMENT5Credit Card Payment
EDUCATION6Education
GOVERNMENT_INSURANCE7Government Insurance
HEALTHCARE_SERVICES8Healthcare Services
INSURANCE_PREMIUM9Insurance Premium
INTEREST10Interest
INVESTMENT11Investment
INVESTMENT_SECURITIES12Investment Securities
LOAN_REPAYMENT13Loan Repayment
PAYMENT_FOR_FEE_AND_CHARGES14Payment for Fee and Charges
PROPERTY_INSURANCE15Property Insurance
PROPERTY_TAX16Property Tax
REBATE17Rebate
RECURRING_INSTALLMENT_PAYMENT18Recurring Installment Payment
RENT19Rent
ROAD_TAX20Road Tax
SAVINGS21Savings
TELECO_BILL22Telco Bill
TRANSPORT_PAYMENT23Transport Payment
UTILITIES_PAYMENT24Utilities Payment
BONUS_PAYMENT25Bonus Payment
BUSINESS_EXPENSES26Business Expenses
CAPITAL_INJECTION27Capital Injection
CASH_DISBURSEMENT28Cash Disbursement
COMMISSION29Commission
DIVIDEND30Dividend
FOREIGN_WORKER_LEVY31Foreign Worker Levy
INVOICE_PAYMENT32Invoice Payment
LICENSE_FEE33License Fee
REFUND34Refund
SALARY_PAYMENT35Salary Payment
SUBSCRIPTION36Subscription
SUPPLIER_PAYMENT37Supplier Payment
TAX_PAYMENT38Tax Payment
OTHERS99Others