API Reference
Card Open API
Card application, activation, PIN management, controls, top up, and card transactions.
1 Introduction
1.1 Overview
The DTC Card Issuing Open API is a JSON RESTful web service for third parties to integrate DTC's card issuing solution into their own products.
1.2 Specification Conventions
- Type: the JSON type of the field. Amount/balance/fee fields are always JSON numbers (not quoted strings).
- Required: Mandatory (validated server-side), Conditional (mandatory only under a stated condition), Optional.
- Description: field meaning, enum reference, or the condition under which it is populated.
1.3 Security Standards
- HTTPS / TLS 1.2 — compulsory for every connection.
- IP Whitelist — optional, configurable in the
API Key Managementpage. - OAuth 2.0 — bearer-token session management (see below).
- Request Signature — HMAC-SHA512 request signing (see below).
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 and go to the
API Key Managementpage. - Click
+ Create, choose Wallet / Card API (not Payments API — that's a separate credential for the Payment Open API), and fill in the Name and IP Whitelist. - The
API Key,API SecretandSign Keyare generated and shown once — copy them before closing the dialog.
2.3 OAuth 2.0 Authentication
Fetch an access token first, then add Authorization: Bearer {access_token} to every other request. Re-fetch (or refresh) before the token expires — only one access token can exist per client id at a time; cache it (e.g. in Redis).
2.3.1 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"Response Body Sample
{
"access_token": "CkppLkyiqEUKITGdtCZRUZBl",
"expires_in": 21600,
"refresh_token": "CpCAhcCRtLU4kPjs54omWW3A",
"rt_expires_in": 43200,
"token_type": "bearer"
}2.3.2 Fetch Access Token by Refresh Token
curl -X "POST" "https://{host}/openapi/auth/oauth2/token" \
-H 'Content-Type: multipart/form-data; charset=utf-8; boundary=__X_PAW_BOUNDARY__' \
-F "refresh_token=CpCAhcCRtLU4kPjs54omWW3A" \
-F "grant_type=refresh_token"2.4 Request Signature
Mandatory for key-function requests; may be applied to all requests.
- Components of the string to sign:
HTTP Method(uppercase) +Timestamp(millis, SGT, request rejected if outside ±2 minutes of server time) +URL(no scheme/host) +Querystring(raw, no leading?) +Request Body(exact bytes, empty string if none). - Concatenate in that order, e.g.
POST1636360661729/openapi/test{"a":"124"}. - Sign with HMAC-SHA512 + Base64, using the
Sign Keyfrom Before Integration. - Add headers to the request:
| Name | R/O/C | Description |
|---|---|---|
| Authorization | R | Bearer {access_token} |
| D-TIMESTAMP | R | Timestamp number in millis, Singapore timezone. The request will be failed if the timestamp is before or after 2 minutes. |
| D-SIGNATURE | R | The generated signature |
| D-SUB-ACCOUNT-ID | C | 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. |
| Content-Type | R | application/json |
| DTC-MFA-Data | O | MFA verification payload — see each endpoint's MFA note |
3 API List
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 Card Issuing
3.2.1 Card Application
Endpoint
[POST] /card/v1/apply
Creates a virtual or physical card request. Idempotent on referenceNo: replaying a request with a referenceNo that already resolved to a card returns that card's current cardId/status instead of creating a duplicate.
Sequence diagram: see the interactive version in the dtcpay developer portal (PDF export available on request).
Request Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| referenceNo | String | M | Unique reference number for this card request (idempotency key) |
| productCode | String | M | Card product code, assigned by DTC |
| cardMaterial | Number | M | CardMaterial enum — see Appendix B |
| currency | String | M | National currency code (ISO 4217), e.g. SGD |
| firstName | String | M | Cardholder first name, ≤25 chars |
| lastName | String | M | Cardholder last name, ≤25 chars |
| preferredPrintedName | String | C | Printed name on card, ≤25 chars; required when cardMaterial is METAL/PLASTIC (physical) |
| String | M | Cardholder email | |
| mobile | Object | M | Cardholder mobile |
| mobile.countryCode | String | M | Country calling code, e.g. 65 |
| mobile.number | String | M | Mobile number |
| deliveryAddress | Object | C | Required when cardMaterial is METAL/PLASTIC (physical) — omitting it entirely is rejected with 31006; ignored for VIRTUAL |
| deliveryAddress.country | String | M | ISO-3166 alpha-3 country code. China Mainland is rejected (31057) |
| deliveryAddress.state | String | O | State/Province, ≤40 chars |
| deliveryAddress.city | String | M | City, ≤40 chars |
| deliveryAddress.district | String | O | District, ≤40 chars |
| deliveryAddress.address1 | String | M | Address line 1, ≤40 chars |
| deliveryAddress.address2 | String | O | Address line 2, ≤40 chars |
| deliveryAddress.address3 | String | O | Address line 3 |
| deliveryAddress.postal | String | M | Postal code, ≤10 chars |
| deliveryAddress.fullName | String | O | Recipient name, ≤60 chars; defaults to cardholder's lastName firstName |
| deliveryAddress.phoneNumber | String | O | Recipient mobile; defaults to cardholder's mobile |
| cardFeeDetails | Array | O | Fees to deduct from the client's wallet for this application. Omit the array entirely when no card fee applies to this request |
| cardFeeDetails[].type | Number | O | 1=application fee, 2=delivery fee |
| cardFeeDetails[].amount | Number | O | Fee amount |
| cardFeeDetails[].currency | String | O | Fee currency (ISO 4217) |
| settlementMode | String | O | MSA_DELEGATED to settle against an existing master settlement account (a separate pre-funded settlement facility, not covered by this document) instead of standard wallet-balance deduction; omit for standard deduction |
| autoDebitEnabled | Number | O | 0=OFF (default), 1=ON — see Setup Card Auto Debit |
Response Parameters
| Name | Type | Description |
|---|---|---|
| cardId | Long | Newly created (or, if referenceNo already existed, the existing) card id |
| status | Number | CardStatus enum — see Appendix B. Confirmed on stg: only physical cards (cardMaterial=METAL/PLASTIC) come back 99 (PENDING, awaiting manual approval); virtual cards (cardMaterial=VIRTUAL) auto-activate and come back 1 (ACTIVATED) immediately — there is no PENDING intermediate state for virtual cards. |
Request Body Sample
{
"query": {
"referenceNo": "123456789",
"productCode": "4202",
"cardMaterial": 3,
"currency": "SGD",
"firstName": "John",
"lastName": "James",
"email": "james@gmail.com",
"mobile": {
"countryCode": "65",
"number": "91234567"
},
"cardFeeDetails": [
{
"type": 1,
"amount": 5.0,
"currency": "SGD"
}
]
}
}Response Body Sample
{
"header": {
"success": true
},
"result": {
"cardId": 3232452,
"status": 1
}
}This sample is for a virtual card (cardMaterial: 3), hence status: 1 (ACTIVATED, immediately) — see the note on the status field above. A physical card (cardMaterial=METAL/PLASTIC) would instead come back status: 99 (PENDING).
Possible Error Codes
| Code | Description | When it happens |
|---|---|---|
| 00006 | Access denied | No valid authClient |
| 00008 | Token is invalid | DTC-MFA-Data present but the sub-account face-auth token is missing/expired |
| 01048 | Invalid mobile number | mobile fails phone-format validation, or (physical card) deliveryAddress.phoneNumber is present but invalid |
| 31001 | Validation error | No card product config matches clientId/currency/cardMaterial/productCode, even after checking the institution's own config for a sub-account caller |
| 31006 | Parameters entered is invalid | query missing; or productCode/currency/referenceNo missing; or (base) firstName/lastName/mobile/email missing/too long; or (physical, cardMaterial != VIRTUAL) deliveryAddress omitted entirely, or present but preferredPrintedName/its own sub-fields missing/too long |
| 31024 | Failed to apply for virtual card | cardMaterial=VIRTUAL and card creation failed, or an unexpected error occurred while applying |
| 31025 | Failed to apply for physical card | cardMaterial is METAL/PLASTIC and card creation failed, or an unexpected error occurred while applying |
| 31055 | Insufficient wallet balance. Card application fee cannot be deducted. | cardFeeDetails given but the client's wallet balance for the fee currency is insufficient |
| 31057 | Sorry, we are no longer supporting the card delivery address to China Mainland | deliveryAddress.country resolves to China |
| 20999 | (dynamic message) | A wallet-side validation error occurred while deducting the card fee |
| 31999 | (dynamic message) | An unexpected error occurred while deducting the card fee |
3.2.2 Inquiry Card Info
Two endpoints return the exact same response object — this doc documents them once.
Endpoints
[GET] /card/v1/inquiry-info/{referenceNo}— look up by thereferenceNoused at application time.[GET] /card/v1/inquiry-info/by-id/{cardId}— look up bycardId.
Path Variables
| Name | Type | Required | Description |
|---|---|---|---|
| referenceNo | String | M | Only for the by-reference-number variant |
| cardId | Long | M | Only for the by-id variant |
Response Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| cardId | Long | M | Card id |
| referenceNo | String | O | Reference number used at application |
| status | Number | M | CardStatus enum |
| clientId | Long | M | Owning client id |
| cardholderName | String | M | Cardholder full name |
| truncatedCardNumber | String | C | Masked PAN, e.g. ****8022; present once status is not Pending |
| currency | String | O | Card's own settlement currency |
| balance | Number | O | Available balance |
| pendingBalance | Number | O | Balance held for pending authorizations |
| cardBrand | Number | M | Brand enum |
| cardMaterial | Number | M | CardMaterial enum |
| preferredPrintedName | String | O | Printed name; present for physical cards |
| mobileNumber | String | M | Cardholder mobile |
| String | M | Cardholder email | |
| pinEnabled | Boolean | M | Whether a PIN has been set |
| autoDebitEnabled | Boolean | M | Whether auto-debit top-up is enabled |
| cardLimitEnabled | Boolean | M | Whether spending limits are enabled |
| transactionLimit | Number | C | Per-transaction limit; present when cardLimitEnabled=true |
| monthlyLimit | Number | C | Monthly limit; present when cardLimitEnabled=true |
| productCode | String | M | Card product code |
| trackingNo | String | O | Delivery tracking number; present once dispatched |
| createdAt | String | M | yyyy-MM-dd HH:mm:ss |
| issuedDate | String | O | YYYY-MM-DD; present once status is not Pending |
| activationDate | String | O | yyyy-MM-dd HH:mm:ss; present once status is not Pending |
| deliveryAddress | Object | O | Present for physical cards |
| deliveryAddress.country / state / city / district / address1 / address2 / address3 / postal | String | O | Same shape as the request's deliveryAddress, minus fullName/phoneNumber |
Response Body Sample
{
"header": {
"success": true
},
"result": {
"cardId": 2323123,
"referenceNo": "123456789",
"status": 1,
"clientId": 1695955465650,
"cardholderName": "John James",
"truncatedCardNumber": "****8022",
"currency": "SGD",
"balance": 422.0,
"pendingBalance": 0.0,
"cardBrand": 1,
"cardMaterial": 1,
"preferredPrintedName": "John",
"mobileNumber": "65 91234567",
"email": "james@gmail.com",
"pinEnabled": true,
"autoDebitEnabled": true,
"cardLimitEnabled": true,
"transactionLimit": 1000.0,
"monthlyLimit": 50000.0,
"productCode": "4001",
"trackingNo": "20241122005",
"createdAt": "2024-04-15 14:02:10",
"issuedDate": "2024-04-15",
"activationDate": "2024-04-15 15:06:39",
"deliveryAddress": {
"country": "SGP",
"state": "Singapore",
"city": "Singapore",
"address1": "123, Clementi Ave 2",
"address2": "Blk 123",
"postal": "123456"
}
}
}Possible Error Codes
| Code | Description | When it happens |
|---|---|---|
| 00006 | Access denied | No valid authClient |
| 00010 | Invalid parameters | referenceNo path segment blank (by-reference-number variant) |
| 31005 | Card Information is invalid | Card not found, or found but not owned by the caller |
3.2.3 Cancel Card Application (physical card only)
Endpoint
[POST] /card/v1/cancel
Sequence diagram: see the interactive version in the dtcpay developer portal (PDF export available on request).
Request Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| cardId | Long | M | Card id |
Possible Error Codes
| Code | Description | When it happens |
|---|---|---|
| 00006 | Access denied | No valid authClient |
| 31005 | Card Information is invalid | Card not found or not owned by the caller |
| 31006 | Parameters entered is invalid | query missing |
| 31027 | Failed to cancel card | Cancellation or fee-refund failed unexpectedly |
| 31999 | (dynamic message) | Client-status validation rejected the cancellation (e.g. suspended account) — errMsg carries the specific reason |
Request Body Sample
{
"query": {
"cardId": 123
}
}Response Body Sample
{
"header": {
"success": true
}
}3.2.4 Send Activate Card OTP
Endpoint
[POST] /card/v1/otp/activate-card
Description
The OTP is sent to the email/mobile number captured on the card itself at application time (the email/mobile submitted with Card Application), not to whatever the client's current account-level KYC/profile contact happens to be — the two can differ (e.g. a different email was used for this specific card). If the cardholder's card-level contact has since been changed via Edit Mobile Phone Number, the OTP goes to the updated value.
Request Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| cardId | Long | M | Card id |
| otpType | Number | O | 1=Email, 2=SMS; must select one |
Request Body Sample
{
"query": {
"cardId": 123,
"otpType": 1
}
}Response Body Sample
{
"header": {
"success": true
}
}Possible Error Codes
| Code | Description | When it happens |
|---|---|---|
| 00006 | Access denied | No valid authClient |
| 31005 | Card Information is invalid | Card not found/not owned, or status is not INACTIVE/DISPATCH |
| 31006 | Parameters entered is invalid | query missing |
| 01006 | Too many OTP requests. Please wait 60 seconds. | OTP requested again inside the cooldown window |
3.2.5 Card Activation
Endpoint
[POST] /card/v1/activate
MFA: this endpoint is gated by institution sub-account MFA — if the caller's institution has apiMfaEnabled and D-SUB-ACCOUNT-ID is present, a valid DTC-MFA-Data header (face-auth token) is required or the request is rejected before it reaches this logic (see WebMvcConfig interceptor coverage).
Sequence diagram: see the interactive version in the dtcpay developer portal (PDF export available on request).
Request Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| cardId | Long | M | Card id |
| lastFourDigitCardNo | String | M | Last 4 digits of the card number, exactly 4 chars |
| otpCode | String | C | Required unless the request carries a valid DTC-MFA-Data face-auth token, or the client's institution is a GATE partner (OTP bypassed) |
Request Body Sample
{
"query": {
"cardId": 123,
"lastFourDigitCardNo": "5868",
"otpCode": "123456"
}
}Response Body Sample
{
"header": {
"success": true
}
}Possible Error Codes
| Code | Description | When it happens |
|---|---|---|
| 00006 | Access denied | No valid authClient |
| 00008 | Token is invalid | DTC-MFA-Data present but sub-account face-auth token invalid/expired |
| 00017 | Incorrect OTP | otpCode does not match (non-face-auth, non-GATE path) |
| 31005 | Card Information is invalid | Card not found/not owned |
| 31006 | Parameters entered is invalid | query missing; lastFourDigitCardNo missing/not 4 chars; or no face-auth and otpCode blank |
| 31008 | The last 4 digits entered are invalid | lastFourDigitCardNo does not match the card's truncated number |
| 31017 | Activate failed | Activation failed for another reason |
3.2.6 Generate Card Public Key
Endpoint
[POST] /card/v1/public-key
Used before Set Card PIN / Reset Card PIN (cardPublicKeyType=PIN) or Edit Mobile Phone Number (cardPublicKeyType=CARD_PHONE), to obtain an RSA public key for client-side encryption.
Sequence diagram: see the interactive version in the dtcpay developer portal (PDF export available on request).
Request Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| cardId | Long | M | Card id |
| cardPublicKeyType | Int | M | Numeric enum: 0=PIN, 1=CARD_PHONE (see CardPublicKeyType below). Not a string — passing "PIN"/"CARD_PHONE" fails validation with 31006. |
Request Body Sample
{
"query": {
"cardId": 1232323,
"cardPublicKeyType": 0
}
}Response Parameters
| Name | Type | Description |
|---|---|---|
| publicKey | String | Base64-encoded RSA public key |
Response Body Sample
{
"header": {
"success": true
},
"result": {
"publicKey": "MIIBIjANBgkq..."
}
}Possible Error Codes
| Code | Description | When it happens |
|---|---|---|
| 00006 | Access denied | No valid authClient |
| 31005 | Card Information is invalid | Card not found/not owned, or not ACTIVATED |
| 31006 | Parameters entered is invalid | cardPublicKeyType is neither 0 nor 1, or query missing. Confirmed on stg: passing the string values "PIN"/"CARD_PHONE" also fails with this exact code — the field only accepts the numeric enum. |
Encryption sample (unchanged from legacy — same RSA/ECB/PKCS1Padding scheme):
// Web:
const encryptor = new JSEncrypt();
encryptor.setPublicKey(publicKey);
const encryptedPIN = encryptor.encrypt(inputPIN);
// Java backend:
public static String encryptUsingPublicKey(String publicKey, String content, String transformation) throws Exception {
PublicKey pubKey = getPublicKey(publicKey, "RSA");
Cipher cipher = Cipher.getInstance(transformation);
cipher.init(Cipher.ENCRYPT_MODE, pubKey);
return Base64.getEncoder().encodeToString(cipher.doFinal(content.getBytes()));
}3.2.7 Set Card PIN
Endpoint
[POST] /card/v1/set-pin
MFA: gated by institution sub-account MFA, same as Card Activation.
Sequence diagram: see the interactive version in the dtcpay developer portal (PDF export available on request).
Request Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| cardId | Long | M | Card id |
| encryptedPin | String | M | PIN block encrypted with the PIN public key |
Request Body Sample
{
"query": {
"cardId": 2323123,
"encryptedPin": "<base64 ciphertext>"
}
}Response Body Sample
{
"header": {
"success": true
}
}Possible Error Codes
| Code | Description | When it happens |
|---|---|---|
| 00006 | Access denied | No valid authClient |
| 00008 | Token is invalid | DTC-MFA-Data present but sub-account face-auth token invalid/expired |
| 31005 | Card Information is invalid | Card not found/not owned, or not ACTIVATED |
| 31006 | Parameters entered is invalid | query missing |
| 31014 | Failed to set PIN | encryptedPin missing/blank; the card's cached PIN public/private key pair (from Generate Card Public Key) not found or expired; decrypting encryptedPin with that private key failed; the decrypted PIN fails format validation (must be exactly 6 digits, not fully consecutive/repeating past half its length); or the downstream processor call otherwise failed |
| 31031 | Overly simple PIN | PIN is a consecutive/repeating sequence |
| 31052 | This card already has a PIN set | Card already has a PIN |
3.2.8 OTP For Reset PIN
Endpoint
[POST] /card/v1/otp/reset-pin
Request Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| cardId | Long | M | Card id |
| otpType | Number | O | 1=Email, 2=SMS; omit to send both |
Request Body Sample
{
"query": {
"cardId": 123,
"otpType": 2
}
}Response Body Sample
{
"header": {
"success": true
}
}Possible Error Codes
| Code | Description | When it happens |
|---|---|---|
| 00006 | Access denied | No valid authClient |
| 31005 | Card Information is invalid | Card not found/not owned, or not ACTIVATED |
| 31006 | Parameters entered is invalid | query missing |
| 01006 | Too many OTP requests. Please wait 60 seconds. | OTP requested again inside the cooldown window |
3.2.9 Reset Card PIN
Endpoint
[POST] /card/v1/reset-pin
MFA: gated by institution sub-account MFA, same as Card Activation — otpCode is not required when a valid DTC-MFA-Data face-auth token is present.
Sequence diagram: see the interactive version in the dtcpay developer portal (PDF export available on request).
Request Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| cardId | Long | M | Card id |
| encryptedPin | String | M | PIN block encrypted with the PIN public key |
| otpCode | String | C | Required unless the request carries a valid DTC-MFA-Data face-auth token, or the client's institution is a GATE partner |
Request Body Sample
{
"query": {
"cardId": 123,
"encryptedPin": "<base64 ciphertext>",
"otpCode": "123456"
}
}Response Body Sample
{
"header": {
"success": true
}
}Possible Error Codes
| Code | Description | When it happens |
|---|---|---|
| 00006 | Access denied | No valid authClient |
| 00008 | Token is invalid | DTC-MFA-Data present but sub-account face-auth token invalid/expired |
| 31005 | Card Information is invalid | Card not found/not owned, or not ACTIVATED |
| 31006 | Parameters entered is invalid | query missing |
| 31014 | Failed to set PIN | Same conditions as Set Card PIN's 31014 row — encryptedPin missing/blank, key/decrypt failure, or invalid PIN format |
| 31999 | Invalid OTP, please try again | otpCode does not match (non-face-auth, non-GATE path) — note this path returns the generic 31999 code with a fixed message, not 00017 |
3.2.10 Freeze Card
Endpoint
[POST] /card/v1/freeze
Request Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| cardId | Long | M | Card id |
Request Body Sample
{
"query": {
"cardId": 2323123
}
}Response Body Sample
{
"header": {
"success": true
}
}Possible Error Codes
| Code | Description | When it happens |
|---|---|---|
| 00006 | Access denied | No valid authClient |
| 31005 | Card Information is invalid | Card not found/not owned |
| 31006 | Parameters entered is invalid | query missing |
| 31011 | Freeze failed | Freeze failed for another reason |
| 31999 | (dynamic message) | Client-status validation rejected the request. Unlike most mutating endpoints, freezing is explicitly allowed even while the client is suspended (errMsg describes the actual reason if rejected) |
3.2.11 Unfreeze Card
Endpoint
[POST] /card/v1/unfreeze
Request Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| cardId | Long | M | Card id |
Request Body Sample
{
"query": {
"cardId": 2323123
}
}Response Body Sample
{
"header": {
"success": true
}
}Possible Error Codes
| Code | Description | When it happens |
|---|---|---|
| 00006 | Access denied | No valid authClient |
| 31005 | Card Information is invalid | Card not found/not owned |
| 31006 | Parameters entered is invalid | query missing |
| 31012 | Unfreeze failed | Unfreeze failed for another reason |
| 31999 | (dynamic message) | Client-status validation rejected the request (suspended clients cannot unfreeze) |
3.2.12 Terminate Card
Endpoint
[POST] /card/v1/terminate
Permanent — a terminated card cannot be reactivated.
Request Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| cardId | Long | M | Card id |
| reason | String | O | Free-text termination reason |
Request Body Sample
{
"query": {
"cardId": 2323123,
"reason": "Lost card"
}
}Response Body Sample
{
"header": {
"success": true
}
}Possible Error Codes
| Code | Description | When it happens |
|---|---|---|
| 00006 | Access denied | No valid authClient |
| 31005 | Card Information is invalid | Card not found/not owned |
| 31006 | Parameters entered is invalid | query missing |
| 31027 | Failed to cancel card | Termination failed for another reason (same code family as Cancel Card Application) |
| 31999 | (dynamic message) | Client-status validation rejected the request |
3.2.13 Setup Card Auto Debit
When the cardholder spends and the card balance is insufficient, the shortfall is auto-debited from the client's dtcpay wallet if this setting is enabled.
Endpoint
[POST] /card/v1/set-up
Request Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| cardId | Long | M | Card id |
| autoDebitEnabled | Number | M | 0=OFF, 1=ON |
Request Body Sample
{
"query": {
"cardId": 2323123,
"autoDebitEnabled": 1
}
}Response Body Sample
{
"header": {
"success": true
}
}Possible Error Codes
| Code | Description | When it happens |
|---|---|---|
| 00006 | Access denied | No valid authClient |
| 31005 | Card Information is invalid | Card not found/not owned |
| 31006 | Parameters entered is invalid | query missing |
| 31034 | Update card settings failed | Setting failed for another reason |
| 31999 | (dynamic message) | Client-status validation rejected the request |
3.2.14 Setup Card Limit
When enabled, every card transaction is checked against these limits and auto-declined if exceeded.
Endpoint
[POST] /card/v1/set-limit
Request Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| cardId | Long | M | Card id |
| cardLimitEnabled | Number | M | 0=OFF, 1=ON |
| perTransactionLimit | Number | C | Maximum per-transaction amount; required when cardLimitEnabled=1, and must not exceed monthlyLimit |
| monthlyLimit | Number | C | Maximum cumulative amount per calendar month; required when cardLimitEnabled=1 |
Request Body Sample
{
"query": {
"cardId": 2323123,
"cardLimitEnabled": 1,
"perTransactionLimit": 80.0,
"monthlyLimit": 5000.0
}
}Response Body Sample
{
"header": {
"success": true
}
}Possible Error Codes
| Code | Description | When it happens |
|---|---|---|
| 00006 | Access denied | No valid authClient |
| 31005 | Card Information is invalid | Card not found/not owned; or cardLimitEnabled=1 and perTransactionLimit/monthlyLimit are missing, zero, or perTransactionLimit > monthlyLimit (note: this validation reuses the "card info invalid" code rather than a parameter-specific one) |
| 31006 | Parameters entered is invalid | query missing |
| 31034 | Update card settings failed | Setting failed for another reason |
| 31999 | (dynamic message) | Client-status validation rejected the request |
3.2.15 Get Card Sensitive Info
Endpoint
[POST] /card/v1/inquiry-card-sensitive-info
MFA: gated by institution sub-account MFA, same as Card Activation.
Sequence diagram: see the interactive version in the dtcpay developer portal (PDF export available on request).
Request Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| cardId | Long | M | Card id |
Response Parameters
| Name | Type | Description |
|---|---|---|
| cardNumber | String | Full PAN. Masked in logs/audit trails; only returned in this response body |
| cvc | String | Card CVC |
| expiryDate | String | Card expiry |
Request Body Sample
{
"query": {
"cardId": 2323123
}
}Response Body Sample
{
"header": {
"success": true
},
"result": {
"cardNumber": "1234567890123456",
"cvc": "123",
"expiryDate": "2024-04"
}
}Possible Error Codes
| Code | Description | When it happens |
|---|---|---|
| 00006 | Access denied | No valid authClient |
| 00008 | Token is invalid | DTC-MFA-Data present but sub-account face-auth token invalid/expired |
| 31005 | Card Information is invalid | Card not found/not owned |
| 31006 | Parameters entered is invalid | query missing |
| 31016 | View card details failed | Retrieval failed for another reason |
3.2.16 Edit Mobile Phone Number
Endpoint
[PUT] /card/v1/edit-mobile
Encrypt the new mobile number with the CARD_PHONE public key from Generate Card Public Key before submitting.
Request Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| cardId | Long | M | Card id |
| encryptedMobile | String | M | New mobile number, encrypted with the CARD_PHONE public key |
Possible Error Codes
| Code | Description | When it happens |
|---|---|---|
| 00006 | Access denied | No valid authClient, or the card does not belong to the caller |
| 31006 | Parameters entered is invalid | query missing, or cardId missing |
| 31010 | Wallet Card not exist | cardId does not resolve to a card |
| 31999 | (dynamic message) | Client-status validation rejected the request |
| CARD_PROCESSOR_ERROR (31002) | Card processor error | Decryption or downstream processor call failed |
Request Body Sample
{
"query": {
"cardId": 2323123,
"encryptedMobile": "<base64 ciphertext>"
}
}Response Body Sample
{
"header": {
"success": true
}
}3.3 Card Transaction
3.3.1 Get OTC Rate
Obtains the quoteId used by Top Up Card when the pay-in currency differs from the card's own settlement currency.
Endpoint
[POST] /wallet/v1/otc/get-otc-rate
Description
Get a locked exchange rate quote for a currency conversion.
Request Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| sellCurrency | String | M | Currency to sell (the pay-in currency for a card top-up) |
| sellAmount | Decimal | C | Sell amount |
| buyCurrency | String | M | Currency to buy (the card's own settlement currency for a card top-up) |
| 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 quoteId to Top Up Card. 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. Call this endpoint again to get a fresh rate once expired; a quoteId 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,
"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.2 Top Up Card
Loads value onto an already-ACTIVATED card by depositing a virtual (crypto) currency amount — this endpoint does not accept fiat wallet balances as the pay-in side. If the card's own settlement currency differs from the pay-in currency, a quoteId (obtained from Get OTC Rate above) is required to lock the conversion rate.
Endpoint
[POST] /card/v1/top-up
Sequence diagram: see the interactive version in the dtcpay developer portal (PDF export available on request).
Request Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| cardId | Long | M | Card id, must be ACTIVATED |
| currency | String | M | Pay-in currency — must be a supported virtual (crypto) currency, e.g. USDT |
| amount | Number | M | Pay-in amount, > 0 |
| quoteId | Long | C | Required when currency differs from the card's own settlement currency. The value may be negative. |
| referenceNo | String | O | Idempotency key; auto-generated if omitted |
Response Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| cardTransactionId | Long | M | Card transaction created for this top-up (starts PENDING) |
| cardTransactionState | Number | C | CardTransactionState enum |
| swapOrderId | Long | M | Underlying swap order id (starts WAITING_PAYIN) |
| swapOrderState | Number | M | SwapOrderState enum |
| amount | Number | M | Requested pay-in amount (echo of the request) |
| currency | String | M | Requested pay-in currency (echo of the request) |
| creditedAmount | Number | C | Amount actually credited to the card, after FX conversion if applicable — not known at request time; only meaningful once the deposit completes and the transaction reaches a terminal state (delivered via the Card Transaction Notify webhook or a follow-up transaction detail query) |
| creditedCurrency | String | C | Currency of creditedAmount — the card's own settlement currency |
| createdAt | String | M | yyyy-MM-dd HH:mm:ss |
| updatedAt | String | M | yyyy-MM-dd HH:mm:ss |
Request Body Sample
{
"query": {
"cardId": 2323123,
"currency": "USDT",
"amount": 1532.25,
"quoteId": -6952162430331323076
}
}Response Body Sample
{
"header": {
"success": true
},
"result": {
"cardTransactionId": 2523456789012345,
"cardTransactionState": 0,
"swapOrderId": 3242452,
"swapOrderState": 0,
"amount": 1532.25,
"currency": "USDT",
"creditedAmount": null,
"creditedCurrency": null,
"createdAt": "2026-09-03 12:16:00",
"updatedAt": "2026-09-03 12:16:00"
}
}Possible Error Codes
| Code | Description | When it happens |
|---|---|---|
| 00006 | Access denied | No valid authClient |
| 31006 | Parameters entered is invalid | query missing; cardId/amount/currency missing; or no wallet account exists for the pay-in currency |
| 11004 | Invalid amount | amount ≤ 0 |
| 31005 | Card Information is invalid | Card not found or not owned |
| 11008 | Invalid Quote ID | Card currency differs from currency and no quoteId given |
| 50011 | Balance have exceed the limit. | The institution's aggregate card-balance limit would be exceeded |
| 31013 | Top Up Failed | Top-up failed for another reason |
| 31999 | (dynamic message) | Client-status validation rejected the request |
3.3.3 Transfer to Card
Moves part or all of one Active card's balance to another of the caller's own dtcpay cards in the same currency.
This creates two card-transaction records, both of type CardTransactionType.BALANCE_TRANSFER — a DEBIT leg on the sender card and a CREDIT leg on the recipient card, both returned to the caller (see Response Parameters below). Each leg also produces its own balance-history row (type=1 CARD_TRANSACTION) with relatedId pointing back to that leg's own transaction id. Retrying the same referenceNo is idempotent: it returns the same pair of ids rather than creating a second transfer.
Endpoint
[POST] /card/v1/transfer-to-card
Sequence diagram: see the interactive version in the dtcpay developer portal (PDF export available on request).
Request Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| senderCardId | Long | M | Sender card id |
| recipientCardId | Long | M | Recipient card id (must be the same currency as the sender) |
| amount | Number | M | Transfer amount, > 0 |
| referenceNo | String | O | Idempotency key; auto-generated if omitted |
Response Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| debitCardTransaction | Object | O | The DEBIT BALANCE_TRANSFER transaction created on the sender card |
| debitCardTransaction.id | Long | M | Card transaction id |
| debitCardTransaction.status | Number | M | CardTransactionState enum |
| debitCardTransaction.type | Number | M | CardTransactionType enum — always BALANCE_TRANSFER for this endpoint |
| debitCardTransaction.cardId | Long | M | The sender card id |
| creditCardTransaction | Object | O | The CREDIT BALANCE_TRANSFER transaction created on the recipient card |
| creditCardTransaction.id | Long | M | Card transaction id |
| creditCardTransaction.status | Number | M | CardTransactionState enum |
| creditCardTransaction.type | Number | M | CardTransactionType enum — always BALANCE_TRANSFER for this endpoint |
| creditCardTransaction.cardId | Long | M | The recipient card id |
Possible Error Codes
| Code | Description | When it happens |
|---|---|---|
| 00006 | Access denied | No valid authClient |
| 00010 | Invalid parameters | query missing, or sender/recipient card not found |
| 11004 | Invalid amount | amount missing or ≤ 0 |
| 31022 | Fund Transfer Failed, Please try again. | Transfer failed for another reason |
| 31033 | The input amount exceeds the allowable. | Confirmed on stg: amount exceeds the sender card's available balance (i.e. insufficient balance) — despite the wording, this is not a limit/quota error |
| 31999 | (dynamic message) | Client-status validation rejected the request |
Request Body Sample
{
"query": {
"senderCardId": 3424222123,
"recipientCardId": 2324323456,
"amount": 1532.25
}
}Response Body Sample
{
"header": {
"success": true
},
"result": {
"debitCardTransaction": {
"id": 2523456789012345,
"status": 200,
"type": 17,
"cardId": 3424222123
},
"creditCardTransaction": {
"id": 2523456789012346,
"status": 200,
"type": 17,
"cardId": 2324323456
}
}
}3.3.4 Transfer to Wallet
Moves part or all of one Active card's balance back to the caller's dtcpay wallet, in the card's own currency (or another wallet currency, via a quote).
This creates one card-transaction record of type CardTransactionType.REVERSAL_TO_ACCOUNT with subType=WALLET_TRANSFER_IN, indicator=DEBIT on the card, returned to the caller as cardTransaction. It also produces its own balance-history row (type=1 CARD_TRANSACTION) with relatedId pointing back to that transaction id, and credits the destination dtcpay wallet account. If the destination currency differs from the card's currency, an OTC swap order is additionally created and submitted to settle the conversion (compensated/rolled back automatically if that step fails after the card has already been debited).
Endpoint
[POST] /card/v1/transfer-to-wallet
Sequence diagram: see the interactive version in the dtcpay developer portal (PDF export available on request).
Request Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| cardId | Long | M | Card id |
| amount | Number | M | Transfer amount, > 0 |
| referenceNo | String | O | Idempotency key; auto-generated if omitted |
Response Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| cardTransaction | Object | O | The REVERSAL_TO_ACCOUNT transaction created on the card |
| cardTransaction.id | Long | M | Card transaction id |
| cardTransaction.status | Number | M | CardTransactionState enum |
| cardTransaction.type | Number | M | CardTransactionType enum — always REVERSAL_TO_ACCOUNT for this endpoint |
| cardTransaction.cardId | Long | M | The card id transferred from |
Possible Error Codes
| Code | Description | When it happens |
|---|---|---|
| 00006 | Access denied | No valid authClient, or the card does not belong to the caller |
| 00010 | Invalid parameters | query missing |
| 11004 | Invalid amount | amount missing or ≤ 0 |
| 31010 | Wallet Card not exist | cardId does not resolve to a card |
| 31022 | Fund Transfer Failed, Please try again. | Transfer failed for another reason |
| 31033 | The input amount exceeds the allowable. | Confirmed on stg: amount exceeds the card's available balance (i.e. insufficient balance) — despite the wording, this is not a limit/quota error |
| 31999 | (dynamic message) | Client-status validation rejected the request |
Request Body Sample
{
"query": {
"cardId": 2323123,
"amount": 1532.25
}
}Response Body Sample
{
"header": {
"success": true
},
"result": {
"cardTransaction": {
"id": 2523456789012347,
"status": 200,
"type": 28,
"cardId": 2323123
}
}
}3.3.5 Transaction History Of Card
Endpoint
[POST] /card/v1/inquiry-transaction
Request Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| cardId | Long | M | Card id |
| startDate | String | O | YYYYMMDD, inclusive |
| endDate | String | O | YYYYMMDD, inclusive (internally treated as inclusive-of-day, i.e. up to but not including the following day) |
| page.current | Number | M | Page number |
| page.size | Number | M | Page size, capped at 500 server-side |
Response Parameters — identical object to Detail Of Card Transaction; see that section for the full field table.
Request Body Sample
{
"query": {
"cardId": 1384,
"startDate": "20240105",
"endDate": "20240110"
},
"page": {
"current": 1,
"size": 20
}
}Response Body Sample
{
"header": {
"success": true
},
"resultList": [
{
"id": 2507071503290548745,
"type": 2,
"state": 200,
"clientId": 250613150009465,
"cardId": 1384,
"truncatedCardNumber": "****6729",
"originalId": 25070715032932348785,
"indicator": "DEBIT",
"amount": 10.0,
"currency": "SGD",
"requestAmount": 9.0,
"requestCurrency": "SGD",
"merchantName": "Test Ecom Php",
"referenceNo": "234242312325565",
"transactionDate": "20251028",
"transactionTime": "095500",
"transactionDesc": "Visa Domestic Purchase POS",
"mcc": "5814",
"merchantCity": "SINGAPORE 819",
"merchantId": "001584054110002",
"posEntryMode": "0501",
"acquirerReferenceNo": "635298214632541",
"exchangeRate": 0.7342037,
"createdAt": "2024-01-10 15:19:31",
"updatedAt": "2024-01-10 15:19:31"
}
],
"pagination": {
"current": 1,
"size": 20,
"total": 1
}
}Possible Error Codes
| Code | Description | When it happens |
|---|---|---|
| 00006 | Access denied | No valid authClient |
| 00010 | Invalid parameters | cardId does not resolve to a card |
| 31006 | Parameters entered is invalid | query missing, or page/page.current/page.size missing |
This endpoint has no dedicated catch-all failure path — an unexpected internal error surfaces as a generic 500, not a CARD.* code.
3.3.6 Detail Of Card Transaction
Endpoint
[GET] /card/v1/inquiry-transaction-detail/{transactionId}
Returns the same object as Transaction History Of Card — a single row instead of a page.
Path Variable
| Name | Type | Required | Description |
|---|---|---|---|
| transactionId (path) | Long | M | Card transaction id |
Response Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| id | Long | M | Transaction id |
| type | Number | M | CardTransactionType enum |
| state | Number | M | CardTransactionState enum |
| clientId | Long | M | Owning client id |
| cardId | Long | M | Card id |
| truncatedCardNumber | String | M | Masked PAN |
| originalId | Long | O | Original transaction id (for reversals/refunds) |
| indicator | String | M | DEBIT or CREDIT |
| amount | Number | M | Settlement amount, in the card's currency |
| currency | String | M | Settlement currency |
| cardCurrency | String | O | Card's own base currency, when different from currency |
| requestAmount | Number | M | Amount as originally requested/authorized |
| requestCurrency | String | M | Currency as originally requested/authorized |
| merchantName | String | M | Merchant descriptor |
| referenceNo | String | O | Reference number, when applicable (e.g. top-up/transfer originated transactions) |
| transactionDate | String | O | YYYYMMDD |
| transactionTime | String | O | HHMMSS |
| transactionDesc | String | O | Free-text description |
| mcc | String | O | Merchant category code |
| merchantCity | String | O | Merchant city |
| merchantId | String | O | Merchant id, parsed from the processor's raw auth payload when available |
| posEntryMode | String | O | ISO 8583 field 22 — first 2 digits PAN entry mode, last 2 PIN entry mode |
| acquirerReferenceNo | String | O | Acquirer reference number |
| exchangeRate | Number | O | FX rate applied between requestAmount/requestCurrency and amount/currency. When requestCurrency is directly quotable by DTC (the card's own currency, its pegged currency, or one of DTC's account-supported currencies), this is that quote's rate. Otherwise DTC quotes from Visa's own settlement/billing currency for the transaction to the card's currency instead, and derives this rate as requestAmount ÷ amount from that quote |
| createdAt | String | M | yyyy-MM-dd HH:mm:ss |
| updatedAt | String | M | yyyy-MM-dd HH:mm:ss |
Response Body Sample
{
"header": {
"success": true
},
"result": {
"id": 2507071503290548745,
"type": 2,
"state": 200,
"clientId": 250613150009465,
"cardId": 1384,
"amount": 10.0,
"currency": "SGD",
"originalId": 25070715032932348785,
"truncatedCardNumber": "****6729",
"merchantName": "Test Ecom Php",
"merchantId": "001584054110002",
"posEntryMode": "0501",
"indicator": "DEBIT",
"requestAmount": 9.0,
"requestCurrency": "SGD",
"referenceNo": "234242312325565",
"transactionDate": "20251028",
"transactionTime": "095500",
"transactionDesc": "Visa Domestic Purchase POS",
"mcc": "5814",
"merchantCity": "SINGAPORE 819",
"acquirerReferenceNo": "635298214632541",
"exchangeRate": 0.7342037,
"createdAt": "2024-01-10 15:19:31",
"updatedAt": "2024-01-10 15:19:31"
}
}Possible Error Codes
| Code | Description | When it happens |
|---|---|---|
| 00006 | Access denied | No valid authClient |
| 00010 | Invalid parameters | transactionId not found, or the resolved card does not belong to the caller |
This endpoint has no dedicated catch-all failure path — an unexpected internal error surfaces as a generic 500, not a CARD.* code.
3.3.7 Monthly Card Statement
Requests generation of a monthly statement. The success response only confirms the request was accepted — statement generation and delivery both happen asynchronously afterwards. The finished statement is emailed by the reporting engine to the cardholder's email on file (the email captured at card application, i.e. the same address returned by Inquiry Card Info) — it is not returned in this API's response body, and there is no webhook for statement completion.
Endpoint
[POST] /card/v1/statement
sequenceDiagram
participant Partner
participant dtcpay
participant ReportEngine as dtcpay report engine
participant Cardholder
Partner->>dtcpay: 1. POST /card/v1/statement { cardId, yearMonth }
dtcpay->>dtcpay: 2. Validate card ownership
dtcpay->>ReportEngine: 3. Submit report request
ReportEngine-->>dtcpay: 4. Accepted
dtcpay-->>Partner: 5. response() — accepted, not yet delivered
ReportEngine->>ReportEngine: 6. Generate statement (async)
ReportEngine-->>Cardholder: 7. Email the finished statement to the cardholder's registered emailRequest Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| cardId | Long | M | Card id |
| yearMonth | String | M | YYYYMM |
Request Body Sample
{
"query": {
"cardId": 2323123,
"yearMonth": "202505"
}
}Response Body Sample
{
"header": {
"success": true
}
}Possible Error Codes
| Code | Description | When it happens |
|---|---|---|
| 00006 | Access denied | No valid authClient |
| 31005 | Card Information is invalid | Card not found or not owned |
| 31006 | Parameters entered is invalid | query missing, or yearMonth blank |
| 31999 | (dynamic message) | The reporting engine rejected or failed the request, or client-status validation rejected it — errMsg carries the specific reason |
3.3.8 Inquiry Card Balance History
Returns a paginated ledger of balance-changing events on a card (top-ups, card-to-card/wallet transfers, currency conversions).
Endpoint
[POST] /card/v1/inquiry-balance-history
Request Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| cardId | Long | O | Filter to one card; omit to query across all of the caller's cards |
| relatedId | Long | O | Filter to one quote id / card transaction id |
| dateFrom | String | O | yyyy-MM-dd HH:mm:ss, start of updatedAt range |
| dateTo | String | O | yyyy-MM-dd HH:mm:ss, end of updatedAt range |
| page.current | Number | M | Page number |
| page.size | Number | M | Page size |
Response Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| id | Long | M | Balance history id |
| type | Number | M | CardBalanceHistoryType enum |
| clientId | Long | M | Owning client id |
| cardId | Long | M | Card id |
| balanceBefore | Number | M | Balance before this change |
| changeAmount | Number | M | Signed change amount (positive = credit, negative = debit) |
| balanceAfter | Number | M | Balance after this change |
| currency | String | M | Currency |
| relatedId | Long | M | What this points to depends on type: when type=1 (CARD_TRANSACTION — the common case, e.g. top-up, transfer-to-card, transfer-to-wallet), this is a card transaction id. When type=2 (CARD_CONVERSION — a whole-card currency-conversion event), this is instead a quote id, not a card transaction id |
| updatedAt | String | M | yyyy-MM-dd HH:mm:ss |
Request Body Sample
{
"query": {
"cardId": 410,
"relatedId": 250225115135959,
"dateFrom": "2025-02-20 00:00:00",
"dateTo": "2026-01-03 23:59:59"
},
"page": {
"current": 1,
"size": 20
}
}Response Body Sample
{
"header": {
"success": true
},
"resultList": [
{
"id": 653317167,
"clientId": 1669025274037,
"cardId": 410,
"type": 1,
"balanceBefore": 136.0,
"changeAmount": 10.0,
"balanceAfter": 146.0,
"currency": "SGD",
"relatedId": 250225115135959,
"updatedAt": "2025-02-25 03:51:37"
}
],
"pagination": {
"current": 1,
"size": 20,
"total": 1
}
}Possible Error Codes
| Code | Description | When it happens |
|---|---|---|
| 00006 | Access denied | No valid authClient |
| 31006 | Parameters entered is invalid | page missing |
This endpoint has no dedicated catch-all failure path — an unexpected internal error surfaces as a generic 500, not a CARD.* code.
3.4 WebHook
DTC supports webhook callbacks for card issuing events. Configure the webhook URL in API Key Management.
Transport: HTTP POST, Content-Type: application/json; charset=utf-8. Sent to exactly the URL configured in API Key Management — no path suffix is appended.
Acknowledgement: a delivery attempt succeeds only if the request completes, returns HTTP 200, and the body is exactly the plain-text string OK (no quotes). Anything else — failure, timeout, non-200, or a different body — is a failed attempt.
Retry: up to 6 automatic retries after the first attempt (7 total), at 15s/30s/60s/120s/240s/480s (best-effort, may run later). A notification is FAILED only once every attempt is exhausted; endpoints must be idempotent since the same event may be delivered more than once — dedupe on eventId. The submission response only confirms the notification was accepted for processing — not that the partner's endpoint has handled it.
3.4.1 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 webhook URL configured in API Key Management + 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 = "{\"id\":2507071503290548745,\"type\":2, ... }"; // 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
}
}Request Envelope
[POST] {webhook_url} — exactly the URL configured in API Key Management; no path suffix is appended.
| Name | Type | Required | Description |
|---|---|---|---|
| event | String | M | CARD_STATUS_CHANGE / CARD_DELIVERY / CARD_SETTING / CARD_TRANSACTION |
| clientId | Long | M | Owning client id |
| signature | String | M | See Signature Verification above |
| data | Object | M | Event payload, shape depends on event |
3.4.2 Card Status Change Notify
| Name | Type | Required | Description |
|---|---|---|---|
| cardId | Long | M | Card id |
| referenceNo | String | M | Reference number |
| truncatedCardNumber | String | M | Masked PAN |
| previousCardStatus | Number | M | CardStatus enum, before the change |
| newCardStatus | Number | M | CardStatus enum, after the change |
| eventId | String | M | Unique per event — dedupe retried deliveries of the same event by this value |
3.4.3 Card Delivery Notify
| Name | Type | Required | Description |
|---|---|---|---|
| cardId | Long | M | Card id |
| referenceNo | String | M | Reference number |
| truncatedCardNumber | String | M | Masked PAN |
| trackingNo | String | M | Delivery tracking number |
| eventId | String | M | Dedupe key |
3.4.4 Card Setting Notify
This one response shape is shared by two independent settings-change triggers (Setup Card Auto Debit and Set Card Limit --- section numbers per this doc's ToC), and only the fields for whichever one triggered this notification are populated — the two groups below are mutually exclusive on the wire, never both present at once. Confirmed on stg with two real captured payloads:
set-limittriggered:{"cardId":...,"cardLimitEnabled":1,"currency":"SGD","monthlyLimit":6000,"perTransactionLimit":90,"referenceNo":...,"truncatedCardNumber":...}— noautoDebitEnabledkey at all.set-up(auto-debit) triggered:{"autoDebitEnabled":1,"cardId":...,"referenceNo":...,"truncatedCardNumber":...}— nocardLimitEnabled/perTransactionLimit/monthlyLimit/currencykeys at all.
| Name | Type | Required | Description |
|---|---|---|---|
| cardId | Long | M | Card id |
| referenceNo | String | M | Reference number |
| truncatedCardNumber | String | M | Masked PAN |
| autoDebitEnabled | Number | C | 0/1 — present only when this notification was triggered by Setup Card Auto Debit |
| cardLimitEnabled | Number | C | 0/1 — present only when this notification was triggered by Set Card Limit |
| perTransactionLimit | Number | C | Present when triggered by Set Card Limit and cardLimitEnabled=1 |
| monthlyLimit | Number | C | Present when triggered by Set Card Limit and cardLimitEnabled=1 |
| currency | String | C | Present when triggered by Set Card Limit and cardLimitEnabled=1 |
| eventId | String | M | Dedupe key |
3.4.5 Card Transaction Notify
| Name | Type | Required | Description |
|---|---|---|---|
| id | Long | M | Transaction id |
| type | Number | M | CardTransactionType enum |
| state | Number | M | CardTransactionState enum |
| clientId | Long | O | Always null — this field exists on the payload class but is never assigned in code (confirmed: the constructor and every place that mutates this object skip it). Use the envelope's own clientId (a sibling of data, see the envelope table above) instead — that one is always populated. |
| cardId | Long | M | Card id |
| truncatedCardNumber | String | M | Masked PAN |
| originalId | Long | O | Original transaction id |
| cardToken | String | M | Processor card token |
| processorTransactionId | String | O | Processor transaction id |
| indicator | String | M | debit/credit |
| amount | Number | M | Settlement amount |
| currency | String | M | Settlement currency |
| requestAmount | Number | M | Requested amount |
| requestCurrency | String | M | Requested currency |
| merchantName | String | M | Merchant descriptor |
| referenceNo | String | O | Reference number |
| transactionDate | String | O | YYYYMMDD |
| transactionTime | String | O | HHMMSS |
| transactionDesc | String | O | Free-text description |
| mcc | String | O | Merchant category code |
| merchantCity | String | O | Merchant city |
| merchantId | String | O | Merchant ID, resolved from the processor's raw auth request payload; null if unavailable or unparseable |
| posEntryMode | String | O | POS entry mode, concatenation of pan-entry-mode + pin-entry-mode; null if both are absent |
| acquirerReferenceNo | String | O | Acquirer reference number |
| deniedReason | String | O | Present only for declined transactions |
| exchangeRate | Number | O | FX rate applied |
| confirmedTime | String | O | yyyy-MM-dd HH:mm:ss when confirmed |
| createdDate | String | M | yyyy-MM-dd HH:mm:ss |
| lastUpdatedDate | String | M | yyyy-MM-dd HH:mm:ss |
| eventId | String | M | Dedupe key |
Body Sample
{
"event": "CARD_TRANSACTION",
"clientId": 1695955465650,
"signature": "MxrYnCm9Q7JOAvOrISf8+T2kuTW1d/w0at8aaPaoiX08VWfun3XPokVlIx1TkHXdcitls09wzfUGtXQZq23xdg==",
"data": {
"id": 2507071503290548745,
"type": 2,
"state": 200,
"clientId": null,
"cardId": 123,
"amount": 9.0,
"currency": "SGD",
"originalId": 25070715032932348785,
"cardToken": "4937244492000010377",
"truncatedCardNumber": "****6729",
"processorTransactionId": "20250707150329424568",
"merchantName": "Test Ecom Php",
"indicator": "debit",
"requestAmount": 9.0,
"requestCurrency": "SGD",
"referenceNo": "234242312325565",
"transactionDate": "20251028",
"transactionTime": "095500",
"transactionDesc": "Visa Domestic Purchase POS",
"mcc": "5814",
"merchantId": "6012345678901",
"posEntryMode": "0505",
"merchantCity": "SINGAPORE 819",
"acquirerReferenceNo": "635298214632541",
"exchangeRate": 0.7342037,
"confirmedTime": "2024-01-10 15:19:31",
"createdDate": "2024-01-10 15:19:31",
"lastUpdatedDate": "2024-01-10 15:19:31",
"eventId": "3f7a9c2e-5b1d-4c2a-a9f3-2d1e6b8c4f90"
}
}4 Appendix A: Response Codes
Consolidated list of every code reachable from the endpoints in this document (per-endpoint tables above also note when each fires).
| Code | Descriptor |
|---|---|
| 00001 | Failed to fetch data |
| 00006 | Access denied |
| 00008 | Token is invalid |
| 00010 | Invalid parameters |
| 00017 | Incorrect OTP. Please try again. |
| 00018 | Currency is invalid |
| 01006 | Too many OTP requests has been made. Please wait for 60 seconds before requesting again. |
| 01048 | Invalid mobile number. Try again. |
| 11002 | OTC quote error |
| 11004 | Invalid amount |
| 11008 | Invalid Quote ID |
| 20999 | Wallet-account error (dynamic message) |
| 31001 | Validation error |
| 31002 | Card processor error |
| 31005 | Card Information is invalid |
| 31006 | Parameters entered is invalid |
| 31008 | The last 4 digits entered are invalid. |
| 31010 | Wallet Card not exist |
| 31011 | Freeze failed |
| 31012 | Unfreeze failed |
| 31013 | Top Up Failed |
| 31014 | Failed to set PIN |
| 31016 | View card details failed |
| 31017 | Activate failed |
| 31022 | Fund Transfer Failed, Please try again. |
| 31024 | Failed to apply for virtual card. |
| 31025 | Failed to apply for physical card. |
| 31027 | Failed to cancel card. |
| 31031 | Please do not set an overly simple PIN, such as consecutive and repeating numbers. |
| 31034 | Update card settings failed |
| 31052 | This card already has a PIN set. |
| 31055 | Insufficient wallet balance. Card application fee cannot be deducted. |
| 31057 | Sorry, we are no longer supporting the card delivery address to China Mainland |
| 31999 | Card domain error (dynamic message) |
| 50011 | Balance have exceed the limit. |
5 Appendix B: Enum List
Brand
| Name | ID | Descriptor |
|---|---|---|
| VISA | 1 | Visa |
| MASTER | 2 | MasterCard |
CardCategory
| Name | ID | Descriptor |
|---|---|---|
| INFINITE | 1 | Infinite Card |
| PLATINUM | 2 | Platinum Card |
| BUSINESS | 3 | Business Card |
CardMaterial
| Name | ID | Descriptor |
|---|---|---|
| METAL | 1 | Metal Card |
| PLASTIC | 2 | Plastic Card |
| VIRTUAL | 3 | Virtual Card |
CardStatus
After Card Application: only physical cards (METAL/PLASTIC) start at 99 PENDING and require manual approval before moving to 1 ACTIVATED; virtual cards skip PENDING entirely and come back 1 ACTIVATED immediately.
| Name | ID | Descriptor |
|---|---|---|
| INACTIVE | 0 | Inactive |
| ACTIVATED | 1 | Activated |
| FROZEN | 2 | Frozen |
| TERMINATED | 3 | Terminated |
| CANCELLED | 4 | Cancelled Application |
| REJECTED | 5 | Rejected |
| SUSPENDED | 6 | Suspended |
| DISPATCH | 7 | Dispatch |
| PENDING | 99 | Pending Approval |
CardTransactionState (referred to as "Transaction Status" in the legacy doc — same values; called PaymentTransactionState in the underlying shared code, since the same enum backs both Card and Payment transaction states — this doc presents it under the Card-domain name for clarity)
| Name | ID | Descriptor |
|---|---|---|
| PENDING | 0 | Pending |
| AUTHORIZED | 101 | Authorized |
| SUCCESS | 200 | Success |
| CAPTURED | 221 | Captured |
| REVERSED | 301 | Reversed |
| CANCELLED | 302 | Cancelled |
| REFUNDED | 401 | Refunded |
| DENIED | 900 | Denied |
| EXPIRED | 990 | Expired |
CardTransactionType (referred to as "Transaction Type" in the legacy doc — same values)
| Name | ID | Descriptor |
|---|---|---|
| TOP_UP | 1 | Top-up |
| PURCHASE | 2 | Purchase |
| CASH_WITHDRAWAL | 8 | Cash Withdrawal |
| ATM_BALANCE_INQUIRY | 15 | ATM Balance Inquiry |
| BALANCE_TRANSFER | 17 | Card Balance Transfer |
| REFUND | 18 | Refund |
| REVERSAL | 19 | Reversal |
| ACCOUNT_VERIFICATION | 20 | Usually a zero-amount authorization |
| CAPTURE | 21 | Capture |
| DEPOSIT | 22 | Credit account (Money Send / Fund Transfer) |
| INCREMENTAL_AUTH | 23 | Incremental Authorization |
| TIMEOUT_REVERSAL | 24 | Timeout Reversal |
| REVERSAL_TO_ACCOUNT | 28 | Reversal To Wallet Account |
CardBalanceHistoryType
| Name | ID | Descriptor |
|---|---|---|
| CARD_TRANSACTION | 1 | Card Transaction |
| CARD_CONVERSION | 2 | Card Conversion — a whole-card currency-conversion event (the card's own currency is changed). This is an internal/ops-initiated operation; this Open API does not currently expose any endpoint to trigger it. A row with this type may still appear in Inquiry Card Balance History results if such a conversion was performed for the card out-of-band |
SwapOrderType
| Name | ID | Descriptor |
|---|---|---|
| SWAP_AND_TOPUP_CARD | 3 | Swap and Topup Card |
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 |
DeniedReason (used by Card Transaction Notify's deniedReason)
| Name | Descriptor |
|---|---|
| CLIENT_NOT_ACTIVATED | Client Not Activated |
| CARD_EXPIRED | Card Expired |
| CARD_NOT_ACTIVATED | Card Not Activated |
| MCC_RATE_LIMIT_EXCEEDED | MCC Rate Limit Exceeded |
| CARD_FROZEN | Card Frozen |
| CARD_TERMINATED | Card Terminated |
| TRANSACTION_LIMIT_EXCEEDED | Transaction Limit Exceeded |
| MONTHLY_LIMIT_EXCEEDED | Monthly Limit Exceeded |
| MAS_LIMIT_EXCEEDED | MAS Limit Exceeded |
| INSUFFICIENT_FUNDS | Insufficient Funds |
| INVALID_ORIGINAL_TRANSACTION | Invalid Original Transaction |
| INVALID_TRANSACTION | Invalid Transaction |
| INVALID_AMOUNT | Invalid Amount |
| SYSTEM_ERROR | System Error |
| INSTITUTION_LIMIT | Institution Limit |
| ATM_DAILY_COUNT_LIMIT_EXCEEDED | ATM Daily Count Limit Exceeded |
| ATM_AMOUNT_LIMIT_EXCEEDED | ATM Amount Limit Exceeded |
| MAS_ANNUAL_LIMIT_WARNING_90 | MAS Annual Limit Warning 90 |
| MAGSTRIPE_NOT_PERMITTED | Magstripe Not Permitted |
| ANTI_SCAM_MERCHANT_BLOCKED | Anti Scam Merchant Blocked |
| INVALID_VTS_CARD_TOKEN | Invalid VTS Card Token |
| SANCTION_COUNTRY | Sanction Country |
CardPublicKeyType
The cardPublicKeyType request field takes the numeric ID, not the Name string — confirmed on stg (see 3.2.6 Generate Card Public Key).
| Name | ID | Description |
|---|---|---|
| PIN | 0 | Used before Set/Reset Card PIN |
| CARD_PHONE | 1 | Used before Edit Mobile Phone Number |
6 Appendix C: Error Structure
{
"header": {
"success": false,
"errCode": "<code>",
"errMsg": "<description>"
}
}6.1 Client Status Possible Error Codes
These may accompany any endpoint's client-status validation (see each endpoint's 31999/dynamic-message rows above for exactly which ones apply):
| Code | Description |
|---|---|
| 00034 | Your account is suspended, please contact us. |
| 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 us. |
| 00041 | Your account is currently inactive. Please contact us for assistance. |
| 00042 | Your account is currently deactivated. Please contact us for assistance. |
| 00043 | Your account is currently restricted. Please contact us to resolve this issue. |
| 00044 | Your account is currently terminated. Please contact us for more information. |
| 00045 | Your account is currently off-board. Please contact us. |
7 Appendix D: MFA Interceptor Coverage
Institution sub-account MFA (the DTC-MFA-Data header / face-auth token flow) is enforced server-side by openApiMfaInterceptor, wired in WebMvcConfig. It is registered on exactly these endpoints — any other endpoint in this document does not require or check DTC-MFA-Data at all:
| Endpoint | Enforced? |
|---|---|
| Card Application | Yes |
| Card Activation | Yes |
| Set Card PIN | Yes |
| Reset Card PIN | Yes |
| Get Card Sensitive Info | Yes |
| Every other Card Issuing / Card Transaction endpoint | No |
Enforcement is conditional on the caller's institution: if D-SUB-ACCOUNT-ID is present and the owning institution has apiMfaEnabled, a missing or malformed DTC-MFA-Data header is rejected with 00008 Token is invalid before the request reaches the endpoint's own logic; otherwise the header is optional and, if present, is parsed but not enforced (the endpoint itself may still require an otpCode fallback — see each endpoint's request table).