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 Managementpage. - 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
- Request account registration on the DTC Wallet platform.
- Log in with the username/password issued by the DTC team and go to the
API Key Managementpage. - Click
+ Createto open the dialog, and choose Wallet / Card API (not Payments API — that's a separate credential for the Payment Open API). - Enter the Name and IP Whitelist, then submit.
- The
API Key,API Secret, andSign Keyare generated and shown once. Keep them safe before closing the dialog. - 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 aclient_id/client_secretmismatch, 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.
-
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.
-
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"}}-
Sign the string with HMAC-SHA512 using the
Sign Keyfrom Before Integration, then Base64-encode the result. -
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/withdrawand/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
| Name | Type | Required | Description |
|---|---|---|---|
| currency | String | M | Wallet currency (e.g. USD, SGD, USDC). Refer to Enum List — Available Currency |
Response Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| balance | Decimal | M | Current wallet balance for the specified currency |
Response Body Sample
{
"header": {
"success": true
},
"result": {
"balance": 10.12
}
}Possible Error Codes
| Code | Description | When it happens |
|---|---|---|
| 20002 | Wallet account does not exist | No wallet account for this client + currency |
| 00006 | Access denied | No valid authClient on the request (token issue) |
| 00018 | Currency is invalid | currency 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)
| Name | Type | Required | Description |
|---|---|---|---|
| id | Long | M | Wallet account ID |
| clientId | Long | M | Client ID |
| currency | String | M | Wallet currency |
| balance | Decimal | M | Current balance |
| updatedAt | String | M | Last 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
| Code | Description | When it happens |
|---|---|---|
| 00006 | Access denied | No 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
| Name | Type | Required | Description |
|---|---|---|---|
| currency | String | M | Currency of the caller's own wallet to debit; the recipient's wallet of this same currency is credited |
| amount | Decimal | M | Amount to debit from the caller and credit to the recipient. Must be > 0 |
| recipientClientId | Long | M | Recipient dtcpay client ID; must own a wallet of currency |
| note | String (≤35) | O | Message to beneficiary |
| referenceNo | String | O | Client 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.
| Name | Type | Required | Description |
|---|---|---|---|
| fiatTransferOut | Object | O | Populated when currency is a fiat currency — the TRANSFER_OUT leg on the caller's own wallet. Same shape as Get Fiat Transaction |
| fiatTransferIn | Object | O | Populated 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 |
| cryptoTransferOut | Object | O | Populated 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 |
| cryptoTransferIn | Object | O | Populated 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/senderAddressarenullhere because a wallet-transfer recipient is identified byrecipientClientId, not by a whitelisted blockchain address — there is no address record to resolve.
Possible Error Codes
| Code | Description | When it happens |
|---|---|---|
| 00011 | Your balance is insufficient. Please adjust the amount or select a different wallet. | Caller's wallet balance < amount |
| 00006 | Access denied | No valid authClient |
| 11004 | Invalid amount | amount missing or ≤ 0 |
| 20999 | Wallet account error (other) | WalletValidationException/ValidationException from the transfer pipeline |
| 21999 | Fiat 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
| Name | Type | Required | Description |
|---|---|---|---|
| currency | String | M | Currency of the caller's own wallet to debit. Must be a stablecoin unless whitelisted for fiat (see above) |
| amount | Decimal | M | Amount to debit/credit. Must be > 0 |
| recipientClientId | Long | C | Recipient client ID. Provide exactly one of recipientClientId/recipientDtcpayTag |
| recipientDtcpayTag | String | C | Recipient dtcpay tag. Provide exactly one of recipientClientId/recipientDtcpayTag |
| fileIds | Array | M | Supporting 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 |
| note | String (≤35) | O | Message to beneficiary |
| referenceNo | String | O | Client 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.
| Name | Type | Required | Description |
|---|---|---|---|
| fiatTransferOut | Object | O | Populated when currency is fiat (whitelisted callers only) — the TRANSFER_OUT leg on the caller's own wallet. Same shape as Get Fiat Transaction |
| fiatTransferIn | Object | O | Populated when currency is fiat — the TRANSFER_IN leg credited to the recipient. referenceNo/approver are always null on this leg, same as Wallet Transfer |
| cryptoTransferOut | Object | O | Populated 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 |
| cryptoTransferIn | Object | O | Populated 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
| Code | Description | When it happens |
|---|---|---|
| 00011 | Your balance is insufficient. Please adjust the amount or select a different wallet. | Caller's wallet balance < amount |
| 00006 | Access denied | No valid authClient, or dtag resolved to an individual |
| 00010 | Invalid parameters | Currency not a stablecoin (and not whitelisted for fiat); both/neither of recipientClientId/recipientDtcpayTag provided; transfer to self; amount ≤ 0; fileIds missing/empty |
| 50014 | Receiver KYC not completed | Whitelisted-fiat path only: recipient's KYC does not clear the receiver-eligibility check |
| 50015 | Receiver tier does not support stablecoin | Whitelisted-fiat path only |
| 50016 | Receiver tier does not support fiat | Whitelisted-fiat path only |
| 50017 | Receiver balance cap reached | Whitelisted-fiat path only |
| 50018 | Invalid recipient dtag | recipientDtcpayTag does not resolve, or resolves to an individual |
| 14004 | File upload failed | A fileIds entry does not exist, isn't owned by the caller, or isn't NORMAL |
| 20999/21999 | Wallet/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
| Name | Type | Required | Description |
|---|---|---|---|
| currency | String | O | Currency of the ledger entry |
| type | Enum | O | Ledger entry type. Refer to Enum List — ActivityType |
| createTimeFrom | String | O | Start time, inclusive. Format yyyy-MM-dd HH:mm:ss, truncated to the start of that day |
| createTimeTo | String | O | End time, inclusive. Format yyyy-MM-dd HH:mm:ss, truncated to the end of that day |
| page.current | Long | M | Current page number |
| page.size | Long | M | Page size, max 500 |
Request Body Sample
{
"query": {
"currency": "USDC"
},
"page": {
"current": 1,
"size": 10
}
}Response Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| id | Long | M | Ledger entry ID |
| walletAccountId | Long | M | Wallet account ID this entry belongs to |
| currency | String | M | Currency of the ledger entry |
| type | Enum | M | Ledger entry type. Refer to Enum List — ActivityType |
| relatedId | Long | O | ID of the underlying record this entry was posted for; which table depends on type (see Description above) |
| balanceBefore | Decimal | M | Wallet balance immediately before this entry |
| changeAmount | Decimal | M | Signed amount this entry changed the balance by (positive = credit, negative = debit) |
| balanceAfter | Decimal | M | Wallet balance immediately after this entry |
| completedAt | String | O | Ledger completion timestamp, yyyy-MM-dd HH:mm:ss |
| updatedAt | String | M | Last 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
| Code | Description | When 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
| Name | Type | Required | Description |
|---|---|---|---|
| otcId | Long | M | OTC order ID |
Response Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| id | Long | M | OTC ID |
| status | Enum | M | OTC status. Refer to Enum List — OtcStatus |
| clientId | Long | M | Client ID |
| sellCurrency | String | M | Currency sold |
| buyCurrency | String | M | Currency bought |
| rate | Decimal | M | Exchange rate |
| dtcQuoteId | Long | O | DTC quote ID that was consumed to create this order |
| sellAmount | Decimal | M | Amount sold |
| buyAmount | Decimal | M | Amount bought |
| operator | String | O | Operator |
| referenceNo | String | O | Reference number |
| completedAt | String | O | Completed time, yyyy-MM-dd HH:mm:ss |
| updatedAt | String | O | Last 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
| Code | Description | When it happens |
|---|---|---|
| 00999 | Swap Info not exist | No OTC order with that ID |
| 00006 | Access denied | No 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
| Name | Type | Required | Description |
|---|---|---|---|
| id | Long | O | OTC ID. When provided, all other filters are ignored and this single order (if owned by the caller) is returned |
| status | Enum | O | OTC status |
| sellCurrency | String | O | Currency sold |
| buyCurrency | String | O | Currency bought |
| createTimeFrom | String | O | Start 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 |
| createTimeTo | String | O | End time, inclusive. Format yyyy-MM-dd HH:mm:ss, truncated to end of day (was orderTimeEnd) |
| page.current | Integer | M | Current page number |
| page.size | Integer | M | Page 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
| Code | Description | When it happens |
|---|---|---|
| 11999 | Invalid OTC Id | id 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
| Name | Type | Required | Description |
|---|---|---|---|
| referenceNo | String | M | Reference number (unique ID from your system) |
Response Body Sample — same shape as Get OTC Order.
Possible Error Codes
| Code | Description | When it happens |
|---|---|---|
| 00006 | Access denied | No valid authClient |
| 00999 | ReferenceNo not exist | No 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
| Name | Type | Required | Description |
|---|---|---|---|
| sellCurrency | String | M | Currency to sell |
| sellAmount | Decimal | C | Sell amount |
| buyCurrency | String | M | Currency to buy |
| buyAmount | Decimal | C | Buy amount |
At least one of
sellAmount/buyAmountmust be provided; if both are omitted, only indicative rate information is returned.
Request Body Sample
{
"query": {
"buyCurrency": "USD",
"sellCurrency": "GBP"
}
}Response Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| sellCurrency | String | M | Currency to sell |
| buyCurrency | String | M | Currency to buy |
| clientId | Long | M | Client ID |
| expiresAt | String | M | Quote expiry time, yyyy-MM-dd HH:mm:ss — see below |
| rate | Decimal | M | Exchange rate |
| quoteId | Long | M | Quote ID; pass as dtcQuoteId to Request OTC. The value may be negative. |
| sellAmount | Decimal | O | Sell amount |
| buyAmount | Decimal | O | Buy amount |
| sellWalletBalance | Decimal | M | Caller's wallet balance in sellCurrency |
| buyWalletBalance | Decimal | M | Caller'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
| Code | Description | When it happens |
|---|---|---|
| 00018 | Currency is invalid | sellCurrency/buyCurrency not supported |
| 11002 | OTC quote error | Enquiry against the pricing source failed |
| 00001 | Failed to fetch data | Unexpected 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
| Name | Type | Required | Description |
|---|---|---|---|
| buyCurrency | String | M | Currency to buy |
| sellCurrency | String | M | Currency to sell |
| sellAmount | Decimal | C | Either sellAmount or buyAmount must be provided |
| buyAmount | Decimal | C | Either buyAmount or sellAmount must be provided |
| dtcQuoteId | Long | M | Quote ID from Get OTC Rate. One-time use — becomes invalid once consumed |
| referenceNo | String | O | Reference 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
| Code | Description | When it happens |
|---|---|---|
| 00006 | Access denied | No valid authClient |
| 00018 | Currency is invalid | sellCurrency/buyCurrency not supported |
| 20999 | Reference number already exists / wallet account error (other) | Duplicate referenceNo for this client, or a ValidationException from status checks |
| 11999 | OTC 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
questionsToDrawto decide how many questions to present andanswersNeededCorrectto 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 asPENDING(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
| Name | Type | Required | Description |
|---|---|---|---|
| assessmentCode | String | O | Identifies the assessment. Fixed value DPT_RISK_AWARENESS if provided |
| locale | String | O | Preferred 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=ENResponse Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| assessmentCode | String | M | DPT_RISK_AWARENESS |
| totalQuestions | Integer | M | Number of questions in questions. May change when the bank is updated — do not hard-code. Observed as 94 in stg at time of testing |
| questionsToDraw | Integer | M | How many questions to present in one attempt |
| passCriteria | String | M | Grading rule type. Currently MIN_CORRECT (grade on answersNeededCorrect, not on getting every question right) |
| answersNeededCorrect | Integer | M | Minimum number of presented questions that must be answered correctly to pass |
| locale | String | M | Language actually served, may differ from the requested locale. Refer to Enum List — SupportedLanguage |
| questions | Array | M | Every enabled question for the served locale |
| questions[].questionId | Long | M | Stable question ID. Echo unchanged in the submission |
| questions[].type | String | M | Question type. Currently always SINGLE_CHOICE — the server hardcodes this value for every question, there is no multi-select variant implemented |
| questions[].questionContent | String | M | Question text to display |
| questions[].options | Array | M | Selectable options |
| questions[].options[].optionId | Enum | M | Option ID. Refer to Enum List — QuestionOption |
| questions[].options[].optionContent | String | M | Option text to display |
| questions[].correctOptionIds | Array | M | Correct 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
| Code | Description | When it happens |
|---|---|---|
| 00006 | Access denied | No valid authClient |
| 00010 | Invalid parameters | assessmentCode provided but not DPT_RISK_AWARENESS |
| 59999 | Internal error | Unexpected 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
| Name | Type | Required | Description |
|---|---|---|---|
| assessmentCode | String | M | Fixed value DPT_RISK_AWARENESS |
| submissionId | String | M | Idempotency key, generated by the caller and globally unique. Reuse the same value when retrying the same submission |
| completedAt | String | M | Time 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 |
| locale | String | O | Language the user took the assessment in. Refer to Enum List — SupportedLanguage |
| answers | Array | M | One entry per question answered. No duplicate question IDs; every ID must exist in the current bank for the given locale |
| answers[].questionId | Long | M | Echo the questionId served by the question bank |
| answers[].questionContent | String | O | Question 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[].selectedOptionIds | Array | M | Option(s) selected. SINGLE_CHOICE requires exactly one |
| answers[].selectedOptionContents | Array | O | Selected 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
| Name | Type | Required | Description |
|---|---|---|---|
| submissionId | String | M | The submissionId of the record being reported — for a replay, the original one |
| raaStatus | Integer | M | RAA status after this submission. A successful passing submission returns 2 (COMPLETED). Refer to Enum List — RaaStatus |
| recorded | Boolean | M | Whether 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 |
| completedAt | String | O | Completion 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
| Code | Description | When it happens |
|---|---|---|
| 00006 | Access denied | No valid authClient |
| 00010 | Invalid parameters | Request body missing query |
| 01038 | User not found | D-SUB-ACCOUNT-ID (or the master account) does not resolve to a known client |
| 50020 | Invalid RAA answer set | Wrong number of answers, duplicate/unknown question IDs, more than one selected option per question, or an invalid completedAt |
| 50021 | Answer mismatch — below the pass threshold | Re-grading against dtcpay's answer key found fewer than answersNeededCorrect correct; the account is placed UNDER_REVIEW, not silently rejected |
| 59999 | Internal error | Unexpected 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
| Name | Type | Required | Description |
|---|---|---|---|
| assessmentCode | String | O | Fixed value DPT_RISK_AWARENESS if provided |
Response Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| clientId | Long | M | The sub-account clientId this status belongs to |
| raaStatus | Integer | M | 1=PENDING (prompt the assessment, withhold gated actions), 2=COMPLETED (allow), 3=UNDER_REVIEW (withhold, anomaly under manual review). Refer to Enum List — RaaStatus |
| completedAt | String | O | When 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
| Code | Description | When it happens |
|---|---|---|
| 00006 | Access denied | No valid authClient |
| 00010 | Invalid parameters | assessmentCode provided but not DPT_RISK_AWARENESS |
| 01038 | User not found | D-SUB-ACCOUNT-ID (or the master account) does not resolve to a known client |
| 59999 | Internal error | Unexpected 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/COMPLETEDon 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
| Name | Type | Required | Description |
|---|---|---|---|
| address | String | M | The raw wallet address to check |
Response Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| supportMainNetList | Array | M | Mainnets 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
| Code | Description | When it happens |
|---|---|---|
| 00006 | Access denied | No 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
| Name | Type | Required | Description |
|---|---|---|---|
| mainNet | Integer | M | Mainnet ID. Refer to Enum List — MainNet |
Response Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| supportCurrencyList | Array | M | Currencies 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
| Code | Description | When it happens |
|---|---|---|
| 00006 | Access denied | No 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 币种,具体流程如下:
- 在 MetaMask 插件中创建一个钱包;
- 在 MetaMask 中添加 Sepolia USDC 代币,合约地址:
0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238; - 通过 https://faucet.circle.com/ 往刚创建的钱包地址领取测试币,大概几分钟后该钱包地址将收到款项;
- 用该钱包地址作为
senderAddress,调用 Add Client Own Address (POST /wallet/v1/crypto/address/add-client-own) 添加白名单,再调用 (POST /wallet/v1/crypto/address/set-enabled) 启用; - 在 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
| Name | Type | Required | Description |
|---|---|---|---|
| state | Enum | O | Transaction state. Refer to Enum List — CryptoTransactionState |
| type | Enum | O | Transaction type. Refer to Enum List — CryptoTransactionType |
| senderAddressId | Long | O | Sender whitelist address ID filter |
| recipientAddressId | Long | O | Recipient whitelist address ID filter |
| currency | String | O | Currency |
| mainNet | Enum | O | Mainnet. Refer to Enum List — MainNet |
| transactionDateFrom | String | O | Start date, format yyyy-MM-dd HH:mm:ss, truncated to start of day |
| transactionDateTo | String | O | End date, format yyyy-MM-dd HH:mm:ss, truncated to end of day |
| page.current | Integer | M | Current page |
| page.size | Integer | M | Page 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):
| Name | Type | Required | Description |
|---|---|---|---|
| id | Long | M | Unique transaction ID |
| type | Enum | M | Transaction type. Refer to Enum List — CryptoTransactionType |
| state | Enum | M | Transaction state. Refer to Enum List — CryptoTransactionState |
| clientId | Long | O | Client ID (always null on the inquiry result; populated on the by-ID/by-hash/by-referenceNo results) |
| mainNet | Enum | M | Mainnet type |
| amount | Decimal | M | Transaction amount |
| currency | String | M | Currency |
| transactionFee | Decimal | O | Transaction fee |
| txnHash | String | O | Blockchain transaction hash |
| referenceNo | String | O | Business reference number |
| remark | String | O | Transaction remark |
| createdAt | String | M | Transaction request time, yyyy-MM-dd HH:mm:ss |
| updatedAt | String | M | Last updated time, yyyy-MM-dd HH:mm:ss |
| gasFee | Decimal | O | Gas fee |
| operator | String | O | Operator executing the transaction |
| recipientAddressId | Long | O | Recipient whitelist address ID |
| recipientAddress | String | O | Recipient blockchain address, resolved from recipientAddressId |
| senderAddressId | Long | O | Sender whitelist address ID |
| senderAddress | String | O | Sender 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
| Code | Description | When it happens |
|---|---|---|
| 00006 | Access denied | No valid authClient |
| 00999 | Query stablecoin transaction failed | Unexpected 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
| Name | Type | Required | Description |
|---|---|---|---|
| amount | Decimal | O | Transaction amount |
| currency | String | M | Withdrawal currency (a stablecoin, e.g. USDC) |
Request Body Sample
{
"query": {
"amount": 0.01,
"currency": "USDC"
}
}Response Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| fee | Decimal | M | Fee for a withdrawal of this amount/currency |
Response Body Sample
{
"header": {
"success": true
},
"result": {
"fee": 7.0
}
}Possible Error Codes
| Code | Description | When it happens |
|---|---|---|
| 00018 | Currency is invalid | currency unsupported |
| 00999 | Get txn fee failed | Unexpected 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-Dataheader: required when the caller is an institution with sub-account MFA enabled (see Request Signature); enforced identically to the legacy/openapi/v1/crypto-txn/withdrawendpoint — 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
| Name | Type | Required | Description |
|---|---|---|---|
| recipientAddressId | Long | M | Whitelisted recipient wallet address ID (renamed from kycWalletAddressId in an earlier draft) |
| amount | Decimal | M | Withdrawal amount |
| currency | String | M | Withdrawal currency (a stablecoin, e.g. USDC) |
| referenceNo | String | O | Reference number, must be unique if provided |
Request Body Sample
{
"query": {
"recipientAddressId": 516,
"amount": 10,
"currency": "USDC",
"referenceNo": "REF20260901001"
}
}Response Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| cryptoTransactionId | Long | M | Unique withdrawal transaction ID — use this to poll Get Crypto Transaction or match against the CRYPTO_TXN webhook |
| state | Enum | M | Transaction 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
| Code | Description | When it happens |
|---|---|---|
| 00018 | Currency is invalid | currency unsupported |
| 00006 | Access denied | No valid authClient, or the address does not belong to the caller |
| 00008 | Token is invalid | DTC-MFA-Data header present but its token does not match a genuinely completed sub-account face-auth session |
| 25012 | Invalid recipient address ID | recipientAddressId missing/not found |
| 25006 | Withdrawal request unsuccessful | Tier-3 cooldown active, or the processing pipeline rejected the request |
| 19999 | Reference 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
| Name | Type | Required | Description |
|---|---|---|---|
| txnId | Long | M | Unique 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
| Code | Description | When it happens |
|---|---|---|
| 00006 | Access denied | No valid authClient, or the transaction belongs to another client |
| 25005 | The transaction hash is invalid | No 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
| Code | Description | When it happens |
|---|---|---|
| 00006 | Access denied | No valid authClient |
| 00999 | ReferenceNo not existed | No 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
| Code | Description | When it happens |
|---|---|---|
| 00006 | Access denied | No valid authClient, or the transaction belongs to another client |
| 25001 | Transaction not found | No 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
| Name | Type | Required | Description |
|---|---|---|---|
| txnId | Long | M | Transaction ID |
Response Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| id | Long | M | Transaction ID |
| clientId | Long | M | Client ID who owns this transaction |
| type | Enum | M | Transaction type. Refer to Enum List — FiatTransactionType |
| state | Enum | M | Transaction state. Refer to Enum List — FiatTransactionState |
| currency | String | M | Transaction currency, ISO 4217 |
| amount | Decimal | M | Transaction amount |
| transactionFee | Decimal | O | Transaction fee |
| senderAccountId | Long | O | Sender account ID |
| recipientAccountId | Long | O | Recipient account ID |
| recipientAmount | Decimal | O | Amount credited to the recipient (when the sender pays the fee) |
| referenceNo | String | O | Reference number |
| operator | String | O | Operator who created the transaction |
| approver | String | O | Approver who authorized the transaction |
| remark | String | O | Transaction remark |
| purpose | Enum | O | Transfer purpose. Refer to Enum List — TransferPurpose |
| sourceOfIncome | Enum | O | Source of income |
| createdAt | String | M | Created timestamp, yyyy-MM-dd HH:mm:ss |
| completedAt | String | O | Completed timestamp, yyyy-MM-dd HH:mm:ss |
| updatedAt | String | M | Last 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
| Code | Description | When it happens |
|---|---|---|
| 00006 | Access denied | No valid authClient, or the transaction belongs to another client |
| 21999 | Invalid Transaction Id | No 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
| Name | Type | Required | Description |
|---|---|---|---|
| state | Enum | O | Transaction state filter |
| type | Enum | O | Transaction type filter |
| currency | String | O | Currency filter, ISO 4217 |
| createTimeFrom | String | O | Start date, format yyyy-MM-dd or yyyy-MM-dd HH:mm:ss, truncated to start of day |
| createTimeTo | String | O | End date, same formats, truncated to end of day |
| page.current | Integer | M | Page number |
| page.size | Integer | M | Page 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
| Code | Description | When it happens |
|---|---|---|
| 00999 | Query Fiat transaction failed | Unexpected 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
| Name | Type | Required | Description |
|---|---|---|---|
| recipientRemitInfoId | Long | M | Recipient bank account (remit info) ID |
| amount | Decimal | M | Withdrawal amount |
Request Body Sample
{
"query": {
"recipientRemitInfoId": 12,
"amount": 1.35
}
}Response Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| fee | Decimal | M | Transaction 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
| Code | Description | When it happens |
|---|---|---|
| 21002 | Invalid Recipient Bank Account | recipientRemitInfoId 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-Dataheader: required when the caller is an institution with sub-account MFA enabled, enforced identically to the legacy/openapi/v1/fiat/withdrawendpoint — 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
| Name | Type | Required | Description |
|---|---|---|---|
| amount | Decimal | M | Amount to withdraw |
| currency | String | M | Currency code (e.g. USD, EUR) |
| type | Enum | M | Fiat transaction type. Refer to Enum List — FiatTransactionType. 2 = Withdraw to own account (no file needed); 3 = Invoice payment to a third party (fileIds required) |
| recipientAccountId | Long | M | Recipient bank account ID (the ID returned by Add Bank Account / the id inside Get Client Own Bank Accounts) |
| fileIds | Array | C | Supporting invoice file IDs from Upload File by Token. Required when type=3 (INVOICE), file status must be NORMAL |
| referenceNo | String | O | Your system's unique ID, used for idempotency |
| recipientAmount | Decimal | O | Use 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
| Code | Description | When it happens |
|---|---|---|
| 00011 | Your balance is insufficient. Please adjust the amount or select a different wallet. | Caller's wallet balance < amount |
| 00008 | Token is invalid | DTC-MFA-Data header present but its token does not match a genuinely completed sub-account face-auth session |
| 00999 | RecipientAccountId is empty / Remit info not exist / Fiat Transaction type incorrect | Various validation failures, see message |
| 20999 | ReferenceNo exists | Duplicate referenceNo for this client |
| 21999 | Withdrawal request unsuccessful | ValidationException/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
| Name | Type | Required | Description |
|---|---|---|---|
| amount | Decimal | M | Amount to deposit, must be > 0 |
| currency | String | M | Currency code (e.g. USD, EUR) |
| referenceNo | String | O | Reference number for the transaction, used for idempotency |
| fileIds | Array | O | Supporting 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
| Code | Description | When it happens |
|---|---|---|
| 20002 | Wallet account does not exist | No wallet account for this client + currency |
| 20004 | Wallet account is inactive | Caller's wallet is not ACTIVE |
| 21002 | Validation error | amount/currency missing, amount ≤ 0, or no valid dtcpay recipient bank account configured for currency |
| 50011 | Balance have exceed the limit | Deposit 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
| Code | Description | When it happens |
|---|---|---|
| 00006 | Access denied | No valid authClient |
| 00999 | ReferenceNo not exist | No 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 nosignature/datawrapper 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
| Name | Type | Required | Description |
|---|---|---|---|
| event | String | M | FIAT_TXN — this envelope carries a fiat transaction event |
| clientId | Long | M | Client ID |
| signature | String | M | See Signature Verification above — signs data only, keys recursively sorted |
| data.transactionId | Long | M | Fiat transaction ID |
| data.referenceNo | String | M | Transaction reference number, usually customer-provided |
| data.amount | BigDecimal | M | Transaction amount |
| data.currency | String | M | Currency code. Refer to Enum List — Available Currency |
| data.transactionFee | BigDecimal | M | Transaction fee, already included in amount |
| data.type | Enum | M | Refer to Enum List — FiatTransactionType |
| data.state | Enum | M | Refer to Enum List — FiatTransactionState |
| data.recipientAccountId | Long | C | Recipient account ID; null on a deposit transaction |
| data.createdTime | String | M | Create time, format yyyy-MM-dd HH:mm:ss |
| data.lastUpdatedTime | String | M | Last update time, format yyyy-MM-dd HH:mm:ss |
| data.eventId | String | M | Unique 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
| Name | Type | Required | Description |
|---|---|---|---|
| event | String | M | CRYPTO_TXN — this envelope carries a crypto transaction event |
| clientId | Long | M | Client ID |
| signature | String | M | See Signature Verification above — signs data only, keys recursively sorted |
| data.transactionId | Long | M | Crypto transaction ID |
| data.referenceNo | String | M | Transaction reference number, usually customer-provided |
| data.amount | BigDecimal | M | Transaction amount |
| data.transactionFee | BigDecimal | M | Transaction fee, already included in amount |
| data.currency | String | M | Currency code. Refer to Enum List — Available Currency |
| data.mainNet | Enum | M | Refer to Enum List — MainNet |
| data.txnHash | String | C | Blockchain transaction hash |
| data.type | Enum | M | Refer to Enum List — CryptoTransactionType |
| data.state | Enum | M | Refer to Enum List — CryptoTransactionState |
| data.recipientAddressId | Long | C | Recipient whitelist address ID; null on a deposit transaction |
| data.senderAddressId | Long | C | Sender whitelist address ID |
| data.createTime | String | M | Create time, format yyyy-MM-dd HH:mm:ss |
| data.lastUpdateTime | String | M | Last update time, format yyyy-MM-dd HH:mm:ss |
| data.eventId | String | M | Unique 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
| Name | Descriptor |
|---|---|
| CRYPTO_TXN | Crypto transaction event |
| FIAT_TXN | Fiat 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:
- (PAY_NOW_WITHDRAW / SGQR_WITHDRAW only) QR Code Query — scan the payee's QR code to resolve
qrCodeRecipientInfo(proxy type/value, or a rawqrStr). - 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). - Create Swap Order — submit
quoteId+amount+currency+payoutMethod(+ the payout-specific info from step 1, orcardInfoforCARD_TOP_UP). dtcpay debitscurrencyfrom the caller's wallet at the locked rate and settles the payout asynchronously through the underlying channel. - Get Swap Order Details — poll for the order's final state, or receive it via the
callbackUrlwebhook supplied at creation. - 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
| Name | Type | Required | Description |
|---|---|---|---|
| processingCurrency | String | M | Currency of the caller's wallet to debit (the settlement/processing side) |
| processingAmount | Decimal | C | Amount to debit, in processingCurrency. Mutually exclusive with amount |
| currency | String | M | Currency of the payout side |
| amount | Decimal | C | Amount to be paid out, in currency. Mutually exclusive with processingAmount |
Request Body Sample
{
"query": {
"processingCurrency": "USDC",
"currency": "SGD",
"amount": 5
}
}Response Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| sellCurrency | String | M | Currency sold (the processing currency) |
| buyCurrency | String | M | Currency bought (the target/payout currency) |
| sellAmount | Decimal | M | Amount of sell currency required |
| buyAmount | Decimal | M | Amount of buy currency expected |
| rate | Decimal | M | Exchange rate |
| expiresAt | String | M | Quote 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 |
| quoteId | Long | M | Quote 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
| Code | Description | When it happens |
|---|---|---|
| 00010 | Invalid parameters | processingCurrency/currency missing, or both/neither of processingAmount/amount provided |
| 00004 | Unknown API error | Unexpected 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
| Name | Type | Required | Description |
|---|---|---|---|
| quoteId | Long | O | Quote ID from Query Quote. The value may be negative. If it is in the same currency, this value is not required |
| amount | Decimal | M | Amount to debit, must match the amount used in the quote |
| currency | String | M | Currency to debit (a stablecoin, e.g. USDC) |
| payoutMethod | Enum | M | 1=PAY_NOW_WITHDRAW, 2=CARD_TOP_UP, 3=SGQR_WITHDRAW |
| qrCodeRecipientInfo | Object | C | Required when payoutMethod is 1 or 3 |
| qrCodeRecipientInfo.referenceNo | String | C | Required unless qrStr is provided |
| qrCodeRecipientInfo.proxyType | Enum | C | PayNow proxy type. Refer to Enum List — ProxyType. Required unless qrStr is provided |
| qrCodeRecipientInfo.proxyValue | String | C | PayNow proxy value. Required unless qrStr is provided |
| qrCodeRecipientInfo.qrStr | String | O | Full QR code string; overrides proxyType/proxyValue when provided |
| cardInfo | Object | C | Required when payoutMethod is 2 |
| cardInfo.cardId | Long | C | Card ID to top up |
| callbackUrl | String | O | URL 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:
| Name | Type | Required | Description |
|---|---|---|---|
| id | Long | M | Swap order ID |
| clientId | Long | M | Client ID who created the order |
| type | Enum | M | Order type. Refer to Enum List — SwapOrderType |
| state | Enum | M | Order state. Refer to Enum List — SwapOrderState. Always WAITING_PAYIN immediately after creation |
| payinTransactionId | Long | O | Pay-in transaction ID; null until payin actually completes |
| payinCurrency | String | M | Pay-in currency |
| payinAmount | Decimal | M | Amount debited from the caller |
| otcId | Long | O | Backing OTC order ID, if applicable |
| quoteId | Long | O | Quote ID used to create the order. The value may be negative. |
| payoutTransactionId | Long | O | A 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. |
| payoutAmount | Decimal | M | Amount paid out to the recipient |
| payoutCurrency | String | M | Payout currency |
| operator | String | O | Operator handling the order |
| createdAt | String | M | Creation timestamp, yyyy-MM-dd HH:mm:ss |
| expiresAt | String | M | Expiration timestamp of the order, yyyy-MM-dd HH:mm:ss |
| updatedAt | String | M | Last 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
| Code | Description | When it happens |
|---|---|---|
| 11008 | Invalid Quote ID | quoteId missing/expired/already consumed |
| 11004 | Invalid amount | amount missing |
| 11020 | Currency is invalid | currency missing |
| 39002 | Invalid payout method | payoutMethod missing/unrecognized |
| 39004 | Reference number cannot be empty | payoutMethod 1/3 without qrCodeRecipientInfo.referenceNo (and no qrStr) |
| 39005 | PayNow proxy type cannot be empty | Same, missing proxyType |
| 39006 | PayNow proxy value cannot be empty | Same, missing proxyValue |
| 39007 | Card info cannot be empty | payoutMethod 2 without cardInfo |
| 39008 | Card ID cannot be empty | cardInfo.cardId missing |
| 00004 | Unknown API error | Unexpected 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
| Name | Type | Required | Description |
|---|---|---|---|
| id | Long | M | Swap order ID |
Response Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| id | Long | M | Swap order ID that was cancelled |
| state | Enum | M | Swap 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
| Code | Description | When it happens |
|---|---|---|
| 00001 | Failed to fetch data | No order with that ID |
| 39009 | The order status does not meet the requirements | Order is not owned by the caller, or is not WAITING_PAYIN |
| 39999 | Swap 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
| Name | Type | Required | Description |
|---|---|---|---|
| id | Long | M | Swap order ID |
Response Parameters — same as Create Swap Order response, plus:
| Name | Type | Required | Description |
|---|---|---|---|
| callbackUrl | String | O | Callback 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
| Code | Description | When it happens |
|---|---|---|
| 00001 | Failed to fetch data | No order with that ID |
| 00006 | Access denied | The 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
| Name | Type | Required | Description |
|---|---|---|---|
| qrCode | String | M | Full QR code string (PayNow or SGQR format) |
Request Body Sample
{
"query": {
"qrCode": "00020101021226370009SG.PAYNOW010120210201935231K030105204000053037025402105802SG5925DIGITAL TREASURES CENTER 6009SINGAPORE62280124FPNI-25081213324002212966304E62B"
}
}Response Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| channel | Enum | M | Resolved payment channel |
| currency | String | M | Currency, usually SGD |
| proxyType | Enum | C | PayNow proxy type. Refer to Enum List — ProxyType |
| proxyValue | String | C | PayNow proxy value |
| amountModifier | Boolean | M | false for a fixed amount, true for a variable amount |
| amount | Decimal | O | Transaction amount, present for a fixed-amount PayNow QR |
| note | String | O | Payment note, present for PayNow |
| acceptorName | String | O | Merchant/acceptor name |
| expiredTime | String | O | QR code expiration time, present for SGQR |
| maxAmount | Decimal | O | Maximum transaction amount, present for SGQR |
| isSupported | Boolean | O | Whether this QR/merchant is currently payable through this channel |
| postCodeMatchActionType | String | O | Alipay Plus only — required post-payment action related to postal-code verification, when applicable |
| userAgent | String | O | Alipay Plus only — user agent to use when following redirectUrl/paymentRedirectUrl |
| redirectUrl | String | O | Alipay Plus only — URL to redirect the payer to in order to complete the scan/authorization step |
| paymentAmount | Decimal | O | Alipay Plus only — the amount the payer will actually be charged (after any promotion), in paymentAmountCurrency |
| paymentAmountCurrency | String | O | Alipay Plus only — currency of paymentAmount |
| chargedAmount | Decimal | O | Alipay Plus only — amount actually charged to the payer's Alipay account, in chargedCurrency |
| chargedCurrency | String | O | Alipay Plus only — currency of chargedAmount |
| crossedAmount | Decimal | O | Alipay Plus only — original (pre-promotion) amount shown as struck-through on the payment page |
| promoDetail | Array | O | Alipay Plus only — list of promotion/discount line items applied to this payment |
| promoTotalAmount | Decimal | O | Alipay Plus only — total promotion discount, in the promotion's own currency |
| promoTotalAmountInOrderCurrency | Decimal | O | Alipay Plus only — total promotion discount, converted into the order's currency |
| merchantName | String | O | Alipay Plus only — merchant display name |
| paymentRequestId | String | O | Alipay Plus only — the payment gateway's own request id for this QR resolution |
| sdkActionPayload | String | O | Alipay Plus only — opaque payload for driving the Alipay client-side SDK, when applicable |
| paymentExpiryTime | String | O | Alipay Plus only — expiry time of the resolved payment request |
| paymentRedirectUrl | String | O | Alipay Plus only — alternate redirect URL used by some Alipay Plus payment flows |
| quotePrice | Decimal | O | Alipay Plus only — FX rate applied between baseCurrency and quoteCurrency, when a currency conversion is involved |
| txnId | Long | O | Alipay Plus only — the gateway's own transaction id for this QR resolution |
| baseCurrency | String | O | Alipay Plus only — the currency quotePrice converts from |
| quoteCurrency | String | O | Alipay Plus only — the currency quotePrice converts to |
| isInternalTransfer | Boolean | O | Whether 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
| Code | Description | When it happens |
|---|---|---|
| 00003 | Params check failed | qrCode missing/blank |
| 38001 | Invalid QR code | qrCode not a recognized PayNow/SGQR format |
| 00004 | Unknown API error | Unexpected 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
| Name | Type | Required | Description |
|---|---|---|---|
| ownerId | Long | M | Client ID who owns this bank account; must match the authenticated client |
| type | Enum | M | 1=OWN (own account), 2=PAYEE (third party). Refer to Enum List — RemitInfoType |
| currency | String | M | Currency, ISO 4217 |
| bankType | Enum | M | Refer to Enum List — BankType |
| beneficiaryType | Enum | M | 1=INDIVIDUAL, 2=CORPORATE |
| beneficiaryFirstName | String | C | Required when beneficiaryType=INDIVIDUAL |
| beneficiaryLastName | String | C | Required when beneficiaryType=INDIVIDUAL |
| beneficiaryName | String | O | Auto-generated for INDIVIDUAL; manual for CORPORATE; overwritten by the client's own name when type=OWN |
| beneficiaryAccount | String | M | Account number, strictly alphanumeric (no hyphens/spaces) |
| beneficiaryBankAccountType | Number | O | 1=CAK (Current Account), 2=OAK (Ordinary Account), 3=SAK (Savings Account) |
| beneficiaryAddress | String | O | Beneficiary address |
| beneficiaryBankName | String | M | Bank name |
| beneficiaryBankAddress | String | M | Bank address |
| beneficiaryBankCountry | String | M | ISO 3166-1 alpha-3 code (e.g. SGP, USA, GBR), must be valid |
| beneficiaryBankSwiftCode | String | C | Required when bankType=SWIFT, must be a valid SWIFT code |
| iban | String | O | For SEPA transfers |
| relationship | Enum | O | Refer to Enum List — Relationship |
| isIntermediaryRequired | Boolean | O | Default false |
| intermediaryBankCountry | String | C | ISO 3166-1 alpha-3, required if intermediary bank info is provided |
| intermediaryBankName | String | C | Required when isIntermediaryRequired=true |
| intermediaryBankSwiftCode | String | C | Required when isIntermediaryRequired=true |
| intermediaryBankAddress | String | O | — |
| intermediaryBankCity | String | O | — |
| enabled | Boolean | O | Whether 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
| Name | Type | Required | Description |
|---|---|---|---|
| id | Long | M | ID of the newly created bank account record |
Response Body Sample
{
"header": {
"success": true
},
"result": {
"id": 1234
}
}Possible Error Codes
| Code | Description | When it happens |
|---|---|---|
| 00006 | Access denied | ownerId does not match the authenticated client |
| 00007 | Failed to add data | Persistence failure |
| 24007 | Invalid swift code | beneficiaryBankSwiftCode fails validation |
| 24009 | Invalid country code | beneficiaryBankCountry/intermediaryBankCountry not a valid alpha-3 code |
| 24015 | Adding this recipient bank account is not supported | Blacklist match |
| 20999 | Other error | Client 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
| Name | Type | Required | Description |
|---|---|---|---|
| id | Long | M | Bank account ID to update |
Request Parameters — same body shape as Add Bank Account.
Response Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| id | Long | M | ID 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
| Code | Description | When it happens |
|---|---|---|
| 00006 | Access denied | ownerId does not match the authenticated client |
| 00002 | Failed to update data | Persistence failure |
| 24007 | Invalid swift code | — |
| 24009 | Invalid country code | — |
| 25002 | Recipient information already exists | The updated fields duplicate another existing bank account |
| 25005 | The recipient has a transaction in progress and cannot be removed now | A 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
| Name | Type | Required | Description |
|---|---|---|---|
| id | Long | M | Bank account ID to disable |
Request Body Sample
{
"query": {
"id": 1234
}
}Response Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| id | Long | M | ID of the bank account that was disabled |
Response Body Sample
{
"header": {
"success": true
},
"result": {
"id": 1234
}
}Possible Error Codes
| Code | Description | When it happens |
|---|---|---|
| 00006 | Access denied | id does not belong to the authenticated client |
| 00010 | Invalid parameters | id missing |
| 25999 | Bank 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
| Name | Type | Required | Description |
|---|---|---|---|
| currency | String | M | Fiat 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
| Code | Description | When it happens |
|---|---|---|
| 21001 | Fiat transaction error — no supporting bank account for the specified currency | dtcpay 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
| Name | Type | Required | Description |
|---|---|---|---|
| currency | String | M | Fiat currency, ISO 4217 |
Response Parameters (array in resultList)
| Name | Type | Required | Description |
|---|---|---|---|
| id | Long | M | Bank account ID |
| ownerId | Long | M | Owner client ID |
| currency | String | M | Currency |
| type | Enum | O | Refer to Enum List — RemitInfoType. Nullable at the data level — not every stored bank account has this set |
| bankType | Enum | O | Refer to Enum List — BankType |
| beneficiaryType | Enum | M | Refer to Enum List — BeneficiaryType |
| beneficiaryFirstName | String | O | — |
| beneficiaryLastName | String | O | — |
| beneficiaryName | String | M | Full beneficiary name |
| beneficiaryAccount | String | M | Account number |
| beneficiaryAddress | String | O | — |
| beneficiaryBankName | String | M | — |
| beneficiaryBankAddress | String | O | — |
| beneficiaryBankCountry | String | M | ISO 3166-1 alpha-3 |
| beneficiaryBankSwiftCode | String | O | — |
| iban | String | O | For SEPA transfers |
| enabled | Boolean | M | Whether this account is enabled |
| isIntermediaryRequired | Boolean | O | — |
| intermediaryBankCountry | String | O | — |
| updatedAt | String | M | Last 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
| Code | Description | When it happens |
|---|---|---|
| 00006 | Access denied | No valid authClient |
| 00999 | Failed to fetch data | Unexpected 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 (
fileIdsis mandatory on that endpoint regardless ofdocumentType).
Request Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| documentType | Enum | M | Refer to Enum List — FileDocumentType |
Request Body Sample
{
"query": {
"documentType": 200
}
}Response Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| token | String | M | Upload token, single-use |
| expiresAt | String | M | Token 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
| Code | Description | When it happens |
|---|---|---|
| 00010 | Invalid parameters | documentType missing |
| 00006 | Access denied | No 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
| Name | Type | Required | Description |
|---|---|---|---|
| token | String | M | Token obtained from Get Upload Token |
Request: multipart/form-data
| Name | Type | Required | Description |
|---|---|---|---|
| file | File | M | The 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 type | Typical 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 |
Any other detected content type is rejected with MIME_TYPE_NOT_SUPPORT (see error table below).
Response Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| id | Long | M | File ID |
| status | Enum | M | Refer to Enum List — FileStatus. Always 3 (NORMAL) on a successful upload |
| mimeType | String | M | The 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
| Code | Description | When it happens |
|---|---|---|
| 00008 | Token is invalid | token missing, already consumed, or expired (>10 minutes) |
| 14006 | File size is larger than 2MB | The token's configured filesizeLimit was exceeded |
| 14005 | File type not support | The uploaded content is not one of the MIME types listed above |
| 14004 | File upload failed | Any 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
| ID | Description |
|---|---|
| 1 | Payment |
| 2 | Otc |
| 3 | Payout |
| 4 | Deposit |
| 5 | Card Spending |
| 6 | Account Top Up |
| 7 | Crypto Purchase |
| 8 | Card Base Currency Conversion |
| 9 | Card 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
| Code | Description |
|---|---|
| 00034 | Your account is suspended, please contact us at {support email}. |
| 00035 | Please submit the KYC information first |
| 00036 | Your account verification is pending. Please complete KYC to unlock full access. |
| 00037 | Your account is rejected. Please contact at {support email}. |
| 00041 | Your account is currently inactive. Please contact us at {support email} for assistance. |
| 00042 | Your account is currently deactivated. Please contact us at {support email} for assistance. |
| 00043 | Your account is currently restricted. Please contact us at {support email} to resolve this issue. |
| 00044 | Your account is currently terminated. Please contact us at {support email} for more information. |
| 00045 | Your account is currently off-board. Please contact us at {support email}. |
| 00060 | Your account is blocked until {date} for security reasons. Please contact us at {support email} for assistance. |
5.2 Common Error Codes (any endpoint)
| Code | Description |
|---|---|
| 00001 | Failed to fetch data |
| 00002 | Failed to update data |
| 00003 | Failed to delete data |
| 00004 | Unknown API error |
| 00006 | Access denied |
| 00007 | Failed to add data |
| 00008 | Token is invalid |
| 00010 | Invalid parameters |
| 00011 | Your balance is insufficient. Please adjust the amount or select a different wallet. |
| 00013 | Client ID is invalid |
| 00018 | Currency is invalid |
| 00027 | Params check failed |
| 00054 | Duplicate submission detected, please try again later |
| 00055 | Too many requests, please try again later |
| 00999 | Other 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)
| Name | ID | Descriptor |
|---|---|---|
| INIT | 0 | Data Init |
| PAYMENT | 1 | Payment Settlement |
| REMIT | 2 | Remittance |
| RESERVE | 3 | Reserve |
| OTC | 4 | Over-the-Counter |
| FIAT_WITHDRAWAL | 5 | Fiat Withdrawal |
| FIAT_DEPOSIT | 6 | Fiat Deposit |
| OTC_COMMISSION | 7 | OTC Commission |
| POBO | 8 | Payment On Behalf Of |
| CRYPTO_WITHDRAWAL | 9 | Stablecoin Withdrawal |
| CRYPTO_DEPOSIT | 10 | Stablecoin Deposit |
| ADJUSTMENT | 11 | Adjustment |
| OTC_BONUS | 12 | OTC Bonus |
| DTC_WALLET | 13 | DTC Wallet Payment |
| CRYPTO_PAYMENT_REFUND | 14 | Stablecoin Payment Refund |
| TOP_UP_CARD | 15 | Top Up Card |
| SATOSHI_TEST | 16 | Satoshi Test |
| CRYPTO_WITHHELD_REFUND | 17 | Stablecoin Withheld Refund |
| PURCHASE_CRYPTO | 18 | Purchase Stablecoin |
| TOP_UP_WALLET | 19 | Top Up Wallet |
| CARD_PAYMENT_REFUND | 20 | Card Payment Refund |
| TRANSFER_IN | 21 | Transfer In |
| TRANSFER_OUT | 22 | Transfer Out |
| SWAP_CANCEL | 23 | Swap Cancel |
| BOUNCE_BACK | 24 | Refund |
| SCAN_PAY | 28 | Scan Pay |
| CARD_TRANSACTION_REVERSAL | 29 | Card Transaction Reversal |
| CARD_APPLICATION_FEE_DEBIT | 30 | Application Fee Debit |
| CARD_DELIVERY_FEE_DEBIT | 31 | Delivery Fee Debit |
| CARD_APPLICATION_FEE_CREDIT | 32 | Application Fee Credit |
| CARD_DELIVERY_FEE_CREDIT | 33 | Delivery Fee Credit |
| CARD_APPLICATION_FEE_REFUND | 34 | Application Fee Refund |
| CARD_DELIVERY_FEE_REFUND | 35 | Delivery Fee Refund |
CryptoTransactionType
| Name | ID | Descriptor |
|---|---|---|
| DEPOSIT | 1 | Deposit |
| WITHDRAW | 2 | Withdraw |
| SATOSHI | 3 | Satoshi Test |
| PAYMENT | 4 | Payment (currently not in use) |
| SETTLEMENT | 5 | Settlement (currently not in use) |
| TRANSFER_IN | 6 | Transfer In |
| TRANSFER_OUT | 7 | Transfer Out |
| CARD_FEE_DEBIT | 8 | Card Fee Debit |
| CARD_FEE_CREDIT | 9 | Card Fee Credit |
| CARD_FEE_REFUND | 10 | Card Fee Refund |
CryptoTransactionState
| Name | ID | Descriptor |
|---|---|---|
| PENDING | 0 | Pending Approval |
| AUTHORIZED | 101 | Authorized |
| RISK_WITHHELD | 102 | Risk Withheld |
| PROCESSING | 110 | Processing |
| COMPLETED | 200 | Completed |
| REFUNDED | 401 | Refunded |
| REJECTED | 900 | Rejected |
| CLOSED | 990 | Closed |
FiatTransactionType
| Name | ID | Descriptor |
|---|---|---|
| DEPOSIT | 1 | Deposit |
| WITHDRAW | 2 | Withdraw |
| INVOICE_PAYMENT | 3 | Invoice Payment |
| TOP_UP | 4 | Top-up |
| TRANSFER_IN | 5 | Transfer In |
| TRANSFER_OUT | 6 | Transfer Out |
| BOUNCE_BACK | 7 | Refund |
| QR_PAYMENT | 8 | QR Payment |
| CARD_FEE_DEBIT | 9 | Card Fee Debit |
| CARD_FEE_CREDIT | 10 | Card Fee Credit |
| CARD_FEE_REFUND | 11 | Card Fee Refund |
FiatTransactionState
| Name | ID | Descriptor |
|---|---|---|
| PENDING | 0 | Pending Approval |
| AUTHORIZED | 101 | Authorized |
| PROCESSING | 110 | Processing |
| COMPLETED | 200 | Completed |
| REJECTED | 900 | Rejected |
| CLOSED | 990 | Closed |
OtcStatus
| Name | ID | Descriptor |
|---|---|---|
| INITIAL | 1 | OTC Order Initial |
| COMPLETED | 5 | Order Completed |
| EXPIRED | 10 | Rate Expired |
| CANCELLED | 11 | Order Cancelled |
RaaStatus
| Name | ID | Descriptor |
|---|---|---|
| PENDING | 1 | Not completed — prompt the assessment and withhold RAA-gated actions |
| COMPLETED | 2 | Completed — RAA-gated actions may proceed |
| UNDER_REVIEW | 3 | Anomaly under manual review — withhold RAA-gated actions |
QuestionOption
| ID | Descriptor |
|---|---|
| A | First option |
| B | Second option |
| C | Third option |
| D | Fourth option |
SupportedLanguage
| Name | ID | Descriptor |
|---|---|---|
| EN | EN | English |
| ZH_CN | ZH-CN | 简体中文 (Simplified Chinese) |
| ZH_HK | ZH-HK | 繁體中文 (Traditional Chinese) |
| VI_VN | VI-VN | Tiếng Việt (Vietnamese) |
| FR_FR | FR-FR | Français (French) |
MainNet
| Name | ID | Descriptor |
|---|---|---|
| BTC | 1 | Bitcoin |
| POLYGON | 2 | Polygon |
| ERC20 | 3 | Ethereum |
| TRC20 | 6 | Tron |
| BEP20 | 7 | BNB Smart Chain (BSC) |
| BASE | 8 | Base |
| SOLANA | 9 | Solana |
| ARBITRUM | 10 | Arbitrum 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
| ID | Descriptor |
|---|---|
| 1 | Client's own (OWN) |
| 2 | Client's payee (PAYEE) |
BankType
| ID | Descriptor |
|---|---|
| 1 | SWIFT |
| 2 | ACH |
| 3 | FPS |
| 4 | SEPA |
| 5 | HK_FPS |
BeneficiaryType
| ID | Descriptor |
|---|---|
| 1 | Individual |
| 2 | Corporate |
Relationship
| Name | ID | Descriptor |
|---|---|---|
| PARENT | 1 | Parent |
| CHILD | 2 | Child |
| SPOUSE | 3 | Spouse |
| BUYER | 4 | Buyer |
| SELLER | 5 | Seller |
| SELF | 6 | Self |
| SIBLING | 7 | Sibling |
| EMPLOYEE | 8 | Employee |
| FRIEND | 9 | Friend |
| IMPORTER_EXPORTER | 10 | Importer/Exporter |
| CONSULTANT | 11 | Consultant |
| CONTRACTOR | 12 | Contractor |
Available Currency
| Currency | Description |
|---|---|
| SGD | Singapore Dollar |
| USD | United States Dollar |
| GBP | Pound Sterling |
| EUR | Euro |
| HKD | Hong Kong Dollar |
| JPY | Japanese Yen |
| AUD | Australian Dollar |
| CAD | Canadian Dollar |
| CNH | Chinese Yuan (offshore) |
| MYR | Malaysian Ringgit |
| AED | United Arab Emirates Dirham |
| USDT | USD Tether |
| USDC | USD Coin |
| WUSD | Worldwide USD |
| FDUSD | First Digital USD |
PayoutMethod
| Name | ID | Descriptor |
|---|---|---|
| PAY_NOW_WITHDRAW | 1 | PayNow withdraw |
| CARD_TOP_UP | 2 | Card top up |
| SGQR_WITHDRAW | 3 | SGQR withdraw |
ProxyType
| Name | ID | Descriptor |
|---|---|---|
| MOBILE | 1 | PayNow / Mobile |
| UEN | 2 | PayNow / UEN |
| NRIC | 3 | PayNow / NRIC/FIN |
| NETS | 4 | SGQR / NETS |
SwapOrderType
| Name | ID | Descriptor |
|---|---|---|
| SWAP_AND_SEND | 0 | Swap and Send |
| DEPOSIT_AND_SEND | 1 | Deposit and Send |
| DEPOSIT_AND_SWAP | 2 | Deposit and Swap |
| SWAP_AND_TOPUP_CARD | 3 | Swap and Topup Card |
| DEPOSIT_AND_TOP_UP_WALLET | 4 | Deposit and Top up Wallet |
SwapOrderState
| Name | ID | Descriptor |
|---|---|---|
| WAITING_PAYIN | 0 | Waiting Payin |
| PAYOUT_PROCESSING | 1 | Payout Processing |
| COMPLETED | 2 | Completed |
| COMPLETED_WITH_EXCESS_AMOUNT | 3 | Completed With Excess Amount |
| FAILED | 4 | Failed |
| EXPIRED | 5 | Expired |
| CANCELLED | 6 | Cancelled |
FileDocumentType
| Name | ID | Descriptor |
|---|---|---|
| FIAT_DEPOSIT_RECEIPT | 200 | Fiat Deposit Receipt |
| INVOICE | 201 | Invoice |
FileStatus
| Name | ID | Descriptor |
|---|---|---|
| UPLOAD_FAILED | 2 | Upload Failed |
| NORMAL | 3 | Normal |
| DELETED | 5 | Deleted |
TransferPurpose
| Name | ID | Descriptor |
|---|---|---|
| ACCOUNT_TOP_UP | 0 | Account Top Up |
| ADVANCE_PAYMENT | 1 | Advance Payment |
| ALIMONY_PAYMENT | 2 | Alimony Payment |
| CARPARK_CHARGES | 3 | Carpark Charges |
| COLLECTION_PAYMENT | 4 | Collection Payment |
| CREDIT_CARD_PAYMENT | 5 | Credit Card Payment |
| EDUCATION | 6 | Education |
| GOVERNMENT_INSURANCE | 7 | Government Insurance |
| HEALTHCARE_SERVICES | 8 | Healthcare Services |
| INSURANCE_PREMIUM | 9 | Insurance Premium |
| INTEREST | 10 | Interest |
| INVESTMENT | 11 | Investment |
| INVESTMENT_SECURITIES | 12 | Investment Securities |
| LOAN_REPAYMENT | 13 | Loan Repayment |
| PAYMENT_FOR_FEE_AND_CHARGES | 14 | Payment for Fee and Charges |
| PROPERTY_INSURANCE | 15 | Property Insurance |
| PROPERTY_TAX | 16 | Property Tax |
| REBATE | 17 | Rebate |
| RECURRING_INSTALLMENT_PAYMENT | 18 | Recurring Installment Payment |
| RENT | 19 | Rent |
| ROAD_TAX | 20 | Road Tax |
| SAVINGS | 21 | Savings |
| TELECO_BILL | 22 | Telco Bill |
| TRANSPORT_PAYMENT | 23 | Transport Payment |
| UTILITIES_PAYMENT | 24 | Utilities Payment |
| BONUS_PAYMENT | 25 | Bonus Payment |
| BUSINESS_EXPENSES | 26 | Business Expenses |
| CAPITAL_INJECTION | 27 | Capital Injection |
| CASH_DISBURSEMENT | 28 | Cash Disbursement |
| COMMISSION | 29 | Commission |
| DIVIDEND | 30 | Dividend |
| FOREIGN_WORKER_LEVY | 31 | Foreign Worker Levy |
| INVOICE_PAYMENT | 32 | Invoice Payment |
| LICENSE_FEE | 33 | License Fee |
| REFUND | 34 | Refund |
| SALARY_PAYMENT | 35 | Salary Payment |
| SUBSCRIPTION | 36 | Subscription |
| SUPPLIER_PAYMENT | 37 | Supplier Payment |
| TAX_PAYMENT | 38 | Tax Payment |
| OTHERS | 99 | Others |