Developers

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 Management page.
  • 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

  1. Request account registration on the DTC Wallet platform.
  2. Log in and go to the API Key Management page.
  3. 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.
  4. The API Key, API Secret and Sign Key are 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.

  1. 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).
  2. Concatenate in that order, e.g. POST1636360661729/openapi/test{"a":"124"}.
  3. Sign with HMAC-SHA512 + Base64, using the Sign Key from Before Integration.
  4. Add headers to the request:
NameR/O/CDescription
AuthorizationRBearer {access_token}
D-TIMESTAMPRTimestamp number in millis, Singapore timezone. The request will be failed if the timestamp is before or after 2 minutes.
D-SIGNATURERThe generated signature
D-SUB-ACCOUNT-IDCThe 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-TypeRapplication/json
DTC-MFA-DataOMFA 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

NameTypeRequiredDescription
referenceNoStringMUnique reference number for this card request (idempotency key)
productCodeStringMCard product code, assigned by DTC
cardMaterialNumberMCardMaterial enum — see Appendix B
currencyStringMNational currency code (ISO 4217), e.g. SGD
firstNameStringMCardholder first name, ≤25 chars
lastNameStringMCardholder last name, ≤25 chars
preferredPrintedNameStringCPrinted name on card, ≤25 chars; required when cardMaterial is METAL/PLASTIC (physical)
emailStringMCardholder email
mobileObjectMCardholder mobile
mobile.countryCodeStringMCountry calling code, e.g. 65
mobile.numberStringMMobile number
deliveryAddressObjectCRequired when cardMaterial is METAL/PLASTIC (physical) — omitting it entirely is rejected with 31006; ignored for VIRTUAL
deliveryAddress.countryStringMISO-3166 alpha-3 country code. China Mainland is rejected (31057)
deliveryAddress.stateStringOState/Province, ≤40 chars
deliveryAddress.cityStringMCity, ≤40 chars
deliveryAddress.districtStringODistrict, ≤40 chars
deliveryAddress.address1StringMAddress line 1, ≤40 chars
deliveryAddress.address2StringOAddress line 2, ≤40 chars
deliveryAddress.address3StringOAddress line 3
deliveryAddress.postalStringMPostal code, ≤10 chars
deliveryAddress.fullNameStringORecipient name, ≤60 chars; defaults to cardholder's lastName firstName
deliveryAddress.phoneNumberStringORecipient mobile; defaults to cardholder's mobile
cardFeeDetailsArrayOFees to deduct from the client's wallet for this application. Omit the array entirely when no card fee applies to this request
cardFeeDetails[].typeNumberO1=application fee, 2=delivery fee
cardFeeDetails[].amountNumberOFee amount
cardFeeDetails[].currencyStringOFee currency (ISO 4217)
settlementModeStringOMSA_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
autoDebitEnabledNumberO0=OFF (default), 1=ON — see Setup Card Auto Debit

Response Parameters

NameTypeDescription
cardIdLongNewly created (or, if referenceNo already existed, the existing) card id
statusNumberCardStatus 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

CodeDescriptionWhen it happens
00006Access deniedNo valid authClient
00008Token is invalidDTC-MFA-Data present but the sub-account face-auth token is missing/expired
01048Invalid mobile numbermobile fails phone-format validation, or (physical card) deliveryAddress.phoneNumber is present but invalid
31001Validation errorNo card product config matches clientId/currency/cardMaterial/productCode, even after checking the institution's own config for a sub-account caller
31006Parameters entered is invalidquery 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
31024Failed to apply for virtual cardcardMaterial=VIRTUAL and card creation failed, or an unexpected error occurred while applying
31025Failed to apply for physical cardcardMaterial is METAL/PLASTIC and card creation failed, or an unexpected error occurred while applying
31055Insufficient wallet balance. Card application fee cannot be deducted.cardFeeDetails given but the client's wallet balance for the fee currency is insufficient
31057Sorry, we are no longer supporting the card delivery address to China MainlanddeliveryAddress.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 the referenceNo used at application time.
  • [GET] /card/v1/inquiry-info/by-id/{cardId} — look up by cardId.

Path Variables

NameTypeRequiredDescription
referenceNoStringMOnly for the by-reference-number variant
cardIdLongMOnly for the by-id variant

Response Parameters

NameTypeRequiredDescription
cardIdLongMCard id
referenceNoStringOReference number used at application
statusNumberMCardStatus enum
clientIdLongMOwning client id
cardholderNameStringMCardholder full name
truncatedCardNumberStringCMasked PAN, e.g. ****8022; present once status is not Pending
currencyStringOCard's own settlement currency
balanceNumberOAvailable balance
pendingBalanceNumberOBalance held for pending authorizations
cardBrandNumberMBrand enum
cardMaterialNumberMCardMaterial enum
preferredPrintedNameStringOPrinted name; present for physical cards
mobileNumberStringMCardholder mobile
emailStringMCardholder email
pinEnabledBooleanMWhether a PIN has been set
autoDebitEnabledBooleanMWhether auto-debit top-up is enabled
cardLimitEnabledBooleanMWhether spending limits are enabled
transactionLimitNumberCPer-transaction limit; present when cardLimitEnabled=true
monthlyLimitNumberCMonthly limit; present when cardLimitEnabled=true
productCodeStringMCard product code
trackingNoStringODelivery tracking number; present once dispatched
createdAtStringMyyyy-MM-dd HH:mm:ss
issuedDateStringOYYYY-MM-DD; present once status is not Pending
activationDateStringOyyyy-MM-dd HH:mm:ss; present once status is not Pending
deliveryAddressObjectOPresent for physical cards
deliveryAddress.country / state / city / district / address1 / address2 / address3 / postalStringOSame 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

CodeDescriptionWhen it happens
00006Access deniedNo valid authClient
00010Invalid parametersreferenceNo path segment blank (by-reference-number variant)
31005Card Information is invalidCard 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

NameTypeRequiredDescription
cardIdLongMCard id

Possible Error Codes

CodeDescriptionWhen it happens
00006Access deniedNo valid authClient
31005Card Information is invalidCard not found or not owned by the caller
31006Parameters entered is invalidquery missing
31027Failed to cancel cardCancellation 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

NameTypeRequiredDescription
cardIdLongMCard id
otpTypeNumberO1=Email, 2=SMS; must select one

Request Body Sample

{
  "query": {
    "cardId": 123,
    "otpType": 1
  }
}

Response Body Sample

{
  "header": {
    "success": true
  }
}

Possible Error Codes

CodeDescriptionWhen it happens
00006Access deniedNo valid authClient
31005Card Information is invalidCard not found/not owned, or status is not INACTIVE/DISPATCH
31006Parameters entered is invalidquery missing
01006Too 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

NameTypeRequiredDescription
cardIdLongMCard id
lastFourDigitCardNoStringMLast 4 digits of the card number, exactly 4 chars
otpCodeStringCRequired 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

CodeDescriptionWhen it happens
00006Access deniedNo valid authClient
00008Token is invalidDTC-MFA-Data present but sub-account face-auth token invalid/expired
00017Incorrect OTPotpCode does not match (non-face-auth, non-GATE path)
31005Card Information is invalidCard not found/not owned
31006Parameters entered is invalidquery missing; lastFourDigitCardNo missing/not 4 chars; or no face-auth and otpCode blank
31008The last 4 digits entered are invalidlastFourDigitCardNo does not match the card's truncated number
31017Activate failedActivation 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

NameTypeRequiredDescription
cardIdLongMCard id
cardPublicKeyTypeIntMNumeric 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

NameTypeDescription
publicKeyStringBase64-encoded RSA public key

Response Body Sample

{
  "header": {
    "success": true
  },
  "result": {
    "publicKey": "MIIBIjANBgkq..."
  }
}

Possible Error Codes

CodeDescriptionWhen it happens
00006Access deniedNo valid authClient
31005Card Information is invalidCard not found/not owned, or not ACTIVATED
31006Parameters entered is invalidcardPublicKeyType 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

NameTypeRequiredDescription
cardIdLongMCard id
encryptedPinStringMPIN 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

CodeDescriptionWhen it happens
00006Access deniedNo valid authClient
00008Token is invalidDTC-MFA-Data present but sub-account face-auth token invalid/expired
31005Card Information is invalidCard not found/not owned, or not ACTIVATED
31006Parameters entered is invalidquery missing
31014Failed to set PINencryptedPin 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
31031Overly simple PINPIN is a consecutive/repeating sequence
31052This card already has a PIN setCard already has a PIN

3.2.8 OTP For Reset PIN

Endpoint

[POST] /card/v1/otp/reset-pin

Request Parameters

NameTypeRequiredDescription
cardIdLongMCard id
otpTypeNumberO1=Email, 2=SMS; omit to send both

Request Body Sample

{
  "query": {
    "cardId": 123,
    "otpType": 2
  }
}

Response Body Sample

{
  "header": {
    "success": true
  }
}

Possible Error Codes

CodeDescriptionWhen it happens
00006Access deniedNo valid authClient
31005Card Information is invalidCard not found/not owned, or not ACTIVATED
31006Parameters entered is invalidquery missing
01006Too 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

NameTypeRequiredDescription
cardIdLongMCard id
encryptedPinStringMPIN block encrypted with the PIN public key
otpCodeStringCRequired 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

CodeDescriptionWhen it happens
00006Access deniedNo valid authClient
00008Token is invalidDTC-MFA-Data present but sub-account face-auth token invalid/expired
31005Card Information is invalidCard not found/not owned, or not ACTIVATED
31006Parameters entered is invalidquery missing
31014Failed to set PINSame conditions as Set Card PIN's 31014 row — encryptedPin missing/blank, key/decrypt failure, or invalid PIN format
31999Invalid OTP, please try againotpCode 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

NameTypeRequiredDescription
cardIdLongMCard id

Request Body Sample

{
  "query": {
    "cardId": 2323123
  }
}

Response Body Sample

{
  "header": {
    "success": true
  }
}

Possible Error Codes

CodeDescriptionWhen it happens
00006Access deniedNo valid authClient
31005Card Information is invalidCard not found/not owned
31006Parameters entered is invalidquery missing
31011Freeze failedFreeze 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

NameTypeRequiredDescription
cardIdLongMCard id

Request Body Sample

{
  "query": {
    "cardId": 2323123
  }
}

Response Body Sample

{
  "header": {
    "success": true
  }
}

Possible Error Codes

CodeDescriptionWhen it happens
00006Access deniedNo valid authClient
31005Card Information is invalidCard not found/not owned
31006Parameters entered is invalidquery missing
31012Unfreeze failedUnfreeze 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

NameTypeRequiredDescription
cardIdLongMCard id
reasonStringOFree-text termination reason

Request Body Sample

{
  "query": {
    "cardId": 2323123,
    "reason": "Lost card"
  }
}

Response Body Sample

{
  "header": {
    "success": true
  }
}

Possible Error Codes

CodeDescriptionWhen it happens
00006Access deniedNo valid authClient
31005Card Information is invalidCard not found/not owned
31006Parameters entered is invalidquery missing
31027Failed to cancel cardTermination 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

NameTypeRequiredDescription
cardIdLongMCard id
autoDebitEnabledNumberM0=OFF, 1=ON

Request Body Sample

{
  "query": {
    "cardId": 2323123,
    "autoDebitEnabled": 1
  }
}

Response Body Sample

{
  "header": {
    "success": true
  }
}

Possible Error Codes

CodeDescriptionWhen it happens
00006Access deniedNo valid authClient
31005Card Information is invalidCard not found/not owned
31006Parameters entered is invalidquery missing
31034Update card settings failedSetting 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

NameTypeRequiredDescription
cardIdLongMCard id
cardLimitEnabledNumberM0=OFF, 1=ON
perTransactionLimitNumberCMaximum per-transaction amount; required when cardLimitEnabled=1, and must not exceed monthlyLimit
monthlyLimitNumberCMaximum 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

CodeDescriptionWhen it happens
00006Access deniedNo valid authClient
31005Card Information is invalidCard 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)
31006Parameters entered is invalidquery missing
31034Update card settings failedSetting 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

NameTypeRequiredDescription
cardIdLongMCard id

Response Parameters

NameTypeDescription
cardNumberStringFull PAN. Masked in logs/audit trails; only returned in this response body
cvcStringCard CVC
expiryDateStringCard expiry

Request Body Sample

{
  "query": {
    "cardId": 2323123
  }
}

Response Body Sample

{
  "header": {
    "success": true
  },
  "result": {
    "cardNumber": "1234567890123456",
    "cvc": "123",
    "expiryDate": "2024-04"
  }
}

Possible Error Codes

CodeDescriptionWhen it happens
00006Access deniedNo valid authClient
00008Token is invalidDTC-MFA-Data present but sub-account face-auth token invalid/expired
31005Card Information is invalidCard not found/not owned
31006Parameters entered is invalidquery missing
31016View card details failedRetrieval 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

NameTypeRequiredDescription
cardIdLongMCard id
encryptedMobileStringMNew mobile number, encrypted with the CARD_PHONE public key

Possible Error Codes

CodeDescriptionWhen it happens
00006Access deniedNo valid authClient, or the card does not belong to the caller
31006Parameters entered is invalidquery missing, or cardId missing
31010Wallet Card not existcardId does not resolve to a card
31999(dynamic message)Client-status validation rejected the request
CARD_PROCESSOR_ERROR (31002)Card processor errorDecryption 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

NameTypeRequiredDescription
sellCurrencyStringMCurrency to sell (the pay-in currency for a card top-up)
sellAmountDecimalCSell amount
buyCurrencyStringMCurrency to buy (the card's own settlement currency for a card top-up)
buyAmountDecimalCBuy amount

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

Request Body Sample

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

Response Parameters

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

How long is the quote valid for? rate/sellAmount/buyAmount are only guaranteed until expiresAt. 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

CodeDescriptionWhen it happens
00018Currency is invalidsellCurrency/buyCurrency not supported
11002OTC quote errorEnquiry against the pricing source failed
00001Failed to fetch dataUnexpected 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

NameTypeRequiredDescription
cardIdLongMCard id, must be ACTIVATED
currencyStringMPay-in currency — must be a supported virtual (crypto) currency, e.g. USDT
amountNumberMPay-in amount, > 0
quoteIdLongCRequired when currency differs from the card's own settlement currency. The value may be negative.
referenceNoStringOIdempotency key; auto-generated if omitted

Response Parameters

NameTypeRequiredDescription
cardTransactionIdLongMCard transaction created for this top-up (starts PENDING)
cardTransactionStateNumberCCardTransactionState enum
swapOrderIdLongMUnderlying swap order id (starts WAITING_PAYIN)
swapOrderStateNumberMSwapOrderState enum
amountNumberMRequested pay-in amount (echo of the request)
currencyStringMRequested pay-in currency (echo of the request)
creditedAmountNumberCAmount 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)
creditedCurrencyStringCCurrency of creditedAmount — the card's own settlement currency
createdAtStringMyyyy-MM-dd HH:mm:ss
updatedAtStringMyyyy-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

CodeDescriptionWhen it happens
00006Access deniedNo valid authClient
31006Parameters entered is invalidquery missing; cardId/amount/currency missing; or no wallet account exists for the pay-in currency
11004Invalid amountamount ≤ 0
31005Card Information is invalidCard not found or not owned
11008Invalid Quote IDCard currency differs from currency and no quoteId given
50011Balance have exceed the limit.The institution's aggregate card-balance limit would be exceeded
31013Top Up FailedTop-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

NameTypeRequiredDescription
senderCardIdLongMSender card id
recipientCardIdLongMRecipient card id (must be the same currency as the sender)
amountNumberMTransfer amount, > 0
referenceNoStringOIdempotency key; auto-generated if omitted

Response Parameters

NameTypeRequiredDescription
debitCardTransactionObjectOThe DEBIT BALANCE_TRANSFER transaction created on the sender card
debitCardTransaction.idLongMCard transaction id
debitCardTransaction.statusNumberMCardTransactionState enum
debitCardTransaction.typeNumberMCardTransactionType enum — always BALANCE_TRANSFER for this endpoint
debitCardTransaction.cardIdLongMThe sender card id
creditCardTransactionObjectOThe CREDIT BALANCE_TRANSFER transaction created on the recipient card
creditCardTransaction.idLongMCard transaction id
creditCardTransaction.statusNumberMCardTransactionState enum
creditCardTransaction.typeNumberMCardTransactionType enum — always BALANCE_TRANSFER for this endpoint
creditCardTransaction.cardIdLongMThe recipient card id

Possible Error Codes

CodeDescriptionWhen it happens
00006Access deniedNo valid authClient
00010Invalid parametersquery missing, or sender/recipient card not found
11004Invalid amountamount missing or ≤ 0
31022Fund Transfer Failed, Please try again.Transfer failed for another reason
31033The 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

NameTypeRequiredDescription
cardIdLongMCard id
amountNumberMTransfer amount, > 0
referenceNoStringOIdempotency key; auto-generated if omitted

Response Parameters

NameTypeRequiredDescription
cardTransactionObjectOThe REVERSAL_TO_ACCOUNT transaction created on the card
cardTransaction.idLongMCard transaction id
cardTransaction.statusNumberMCardTransactionState enum
cardTransaction.typeNumberMCardTransactionType enum — always REVERSAL_TO_ACCOUNT for this endpoint
cardTransaction.cardIdLongMThe card id transferred from

Possible Error Codes

CodeDescriptionWhen it happens
00006Access deniedNo valid authClient, or the card does not belong to the caller
00010Invalid parametersquery missing
11004Invalid amountamount missing or ≤ 0
31010Wallet Card not existcardId does not resolve to a card
31022Fund Transfer Failed, Please try again.Transfer failed for another reason
31033The 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

NameTypeRequiredDescription
cardIdLongMCard id
startDateStringOYYYYMMDD, inclusive
endDateStringOYYYYMMDD, inclusive (internally treated as inclusive-of-day, i.e. up to but not including the following day)
page.currentNumberMPage number
page.sizeNumberMPage 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

CodeDescriptionWhen it happens
00006Access deniedNo valid authClient
00010Invalid parameterscardId does not resolve to a card
31006Parameters entered is invalidquery 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

NameTypeRequiredDescription
transactionId (path)LongMCard transaction id

Response Parameters

NameTypeRequiredDescription
idLongMTransaction id
typeNumberMCardTransactionType enum
stateNumberMCardTransactionState enum
clientIdLongMOwning client id
cardIdLongMCard id
truncatedCardNumberStringMMasked PAN
originalIdLongOOriginal transaction id (for reversals/refunds)
indicatorStringMDEBIT or CREDIT
amountNumberMSettlement amount, in the card's currency
currencyStringMSettlement currency
cardCurrencyStringOCard's own base currency, when different from currency
requestAmountNumberMAmount as originally requested/authorized
requestCurrencyStringMCurrency as originally requested/authorized
merchantNameStringMMerchant descriptor
referenceNoStringOReference number, when applicable (e.g. top-up/transfer originated transactions)
transactionDateStringOYYYYMMDD
transactionTimeStringOHHMMSS
transactionDescStringOFree-text description
mccStringOMerchant category code
merchantCityStringOMerchant city
merchantIdStringOMerchant id, parsed from the processor's raw auth payload when available
posEntryModeStringOISO 8583 field 22 — first 2 digits PAN entry mode, last 2 PIN entry mode
acquirerReferenceNoStringOAcquirer reference number
exchangeRateNumberOFX 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
createdAtStringMyyyy-MM-dd HH:mm:ss
updatedAtStringMyyyy-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

CodeDescriptionWhen it happens
00006Access deniedNo valid authClient
00010Invalid parameterstransactionId 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 email

Request Parameters

NameTypeRequiredDescription
cardIdLongMCard id
yearMonthStringMYYYYMM

Request Body Sample

{
  "query": {
    "cardId": 2323123,
    "yearMonth": "202505"
  }
}

Response Body Sample

{
  "header": {
    "success": true
  }
}

Possible Error Codes

CodeDescriptionWhen it happens
00006Access deniedNo valid authClient
31005Card Information is invalidCard not found or not owned
31006Parameters entered is invalidquery 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

NameTypeRequiredDescription
cardIdLongOFilter to one card; omit to query across all of the caller's cards
relatedIdLongOFilter to one quote id / card transaction id
dateFromStringOyyyy-MM-dd HH:mm:ss, start of updatedAt range
dateToStringOyyyy-MM-dd HH:mm:ss, end of updatedAt range
page.currentNumberMPage number
page.sizeNumberMPage size

Response Parameters

NameTypeRequiredDescription
idLongMBalance history id
typeNumberMCardBalanceHistoryType enum
clientIdLongMOwning client id
cardIdLongMCard id
balanceBeforeNumberMBalance before this change
changeAmountNumberMSigned change amount (positive = credit, negative = debit)
balanceAfterNumberMBalance after this change
currencyStringMCurrency
relatedIdLongMWhat 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
updatedAtStringMyyyy-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

CodeDescriptionWhen it happens
00006Access deniedNo valid authClient
31006Parameters entered is invalidpage 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.

NameTypeRequiredDescription
eventStringMCARD_STATUS_CHANGE / CARD_DELIVERY / CARD_SETTING / CARD_TRANSACTION
clientIdLongMOwning client id
signatureStringMSee Signature Verification above
dataObjectMEvent payload, shape depends on event

3.4.2 Card Status Change Notify

NameTypeRequiredDescription
cardIdLongMCard id
referenceNoStringMReference number
truncatedCardNumberStringMMasked PAN
previousCardStatusNumberMCardStatus enum, before the change
newCardStatusNumberMCardStatus enum, after the change
eventIdStringMUnique per event — dedupe retried deliveries of the same event by this value

3.4.3 Card Delivery Notify

NameTypeRequiredDescription
cardIdLongMCard id
referenceNoStringMReference number
truncatedCardNumberStringMMasked PAN
trackingNoStringMDelivery tracking number
eventIdStringMDedupe 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-limit triggered: {"cardId":...,"cardLimitEnabled":1,"currency":"SGD","monthlyLimit":6000,"perTransactionLimit":90,"referenceNo":...,"truncatedCardNumber":...} — no autoDebitEnabled key at all.
  • set-up (auto-debit) triggered: {"autoDebitEnabled":1,"cardId":...,"referenceNo":...,"truncatedCardNumber":...} — no cardLimitEnabled/perTransactionLimit/monthlyLimit/currency keys at all.
NameTypeRequiredDescription
cardIdLongMCard id
referenceNoStringMReference number
truncatedCardNumberStringMMasked PAN
autoDebitEnabledNumberC0/1 — present only when this notification was triggered by Setup Card Auto Debit
cardLimitEnabledNumberC0/1 — present only when this notification was triggered by Set Card Limit
perTransactionLimitNumberCPresent when triggered by Set Card Limit and cardLimitEnabled=1
monthlyLimitNumberCPresent when triggered by Set Card Limit and cardLimitEnabled=1
currencyStringCPresent when triggered by Set Card Limit and cardLimitEnabled=1
eventIdStringMDedupe key

3.4.5 Card Transaction Notify

NameTypeRequiredDescription
idLongMTransaction id
typeNumberMCardTransactionType enum
stateNumberMCardTransactionState enum
clientIdLongOAlways 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.
cardIdLongMCard id
truncatedCardNumberStringMMasked PAN
originalIdLongOOriginal transaction id
cardTokenStringMProcessor card token
processorTransactionIdStringOProcessor transaction id
indicatorStringMdebit/credit
amountNumberMSettlement amount
currencyStringMSettlement currency
requestAmountNumberMRequested amount
requestCurrencyStringMRequested currency
merchantNameStringMMerchant descriptor
referenceNoStringOReference number
transactionDateStringOYYYYMMDD
transactionTimeStringOHHMMSS
transactionDescStringOFree-text description
mccStringOMerchant category code
merchantCityStringOMerchant city
merchantIdStringOMerchant ID, resolved from the processor's raw auth request payload; null if unavailable or unparseable
posEntryModeStringOPOS entry mode, concatenation of pan-entry-mode + pin-entry-mode; null if both are absent
acquirerReferenceNoStringOAcquirer reference number
deniedReasonStringOPresent only for declined transactions
exchangeRateNumberOFX rate applied
confirmedTimeStringOyyyy-MM-dd HH:mm:ss when confirmed
createdDateStringMyyyy-MM-dd HH:mm:ss
lastUpdatedDateStringMyyyy-MM-dd HH:mm:ss
eventIdStringMDedupe 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).

CodeDescriptor
00001Failed to fetch data
00006Access denied
00008Token is invalid
00010Invalid parameters
00017Incorrect OTP. Please try again.
00018Currency is invalid
01006Too many OTP requests has been made. Please wait for 60 seconds before requesting again.
01048Invalid mobile number. Try again.
11002OTC quote error
11004Invalid amount
11008Invalid Quote ID
20999Wallet-account error (dynamic message)
31001Validation error
31002Card processor error
31005Card Information is invalid
31006Parameters entered is invalid
31008The last 4 digits entered are invalid.
31010Wallet Card not exist
31011Freeze failed
31012Unfreeze failed
31013Top Up Failed
31014Failed to set PIN
31016View card details failed
31017Activate failed
31022Fund Transfer Failed, Please try again.
31024Failed to apply for virtual card.
31025Failed to apply for physical card.
31027Failed to cancel card.
31031Please do not set an overly simple PIN, such as consecutive and repeating numbers.
31034Update card settings failed
31052This card already has a PIN set.
31055Insufficient wallet balance. Card application fee cannot be deducted.
31057Sorry, we are no longer supporting the card delivery address to China Mainland
31999Card domain error (dynamic message)
50011Balance have exceed the limit.

5 Appendix B: Enum List

Brand

NameIDDescriptor
VISA1Visa
MASTER2MasterCard

CardCategory

NameIDDescriptor
INFINITE1Infinite Card
PLATINUM2Platinum Card
BUSINESS3Business Card

CardMaterial

NameIDDescriptor
METAL1Metal Card
PLASTIC2Plastic Card
VIRTUAL3Virtual 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.

NameIDDescriptor
INACTIVE0Inactive
ACTIVATED1Activated
FROZEN2Frozen
TERMINATED3Terminated
CANCELLED4Cancelled Application
REJECTED5Rejected
SUSPENDED6Suspended
DISPATCH7Dispatch
PENDING99Pending 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)

NameIDDescriptor
PENDING0Pending
AUTHORIZED101Authorized
SUCCESS200Success
CAPTURED221Captured
REVERSED301Reversed
CANCELLED302Cancelled
REFUNDED401Refunded
DENIED900Denied
EXPIRED990Expired

CardTransactionType (referred to as "Transaction Type" in the legacy doc — same values)

NameIDDescriptor
TOP_UP1Top-up
PURCHASE2Purchase
CASH_WITHDRAWAL8Cash Withdrawal
ATM_BALANCE_INQUIRY15ATM Balance Inquiry
BALANCE_TRANSFER17Card Balance Transfer
REFUND18Refund
REVERSAL19Reversal
ACCOUNT_VERIFICATION20Usually a zero-amount authorization
CAPTURE21Capture
DEPOSIT22Credit account (Money Send / Fund Transfer)
INCREMENTAL_AUTH23Incremental Authorization
TIMEOUT_REVERSAL24Timeout Reversal
REVERSAL_TO_ACCOUNT28Reversal To Wallet Account

CardBalanceHistoryType

NameIDDescriptor
CARD_TRANSACTION1Card Transaction
CARD_CONVERSION2Card 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

NameIDDescriptor
SWAP_AND_TOPUP_CARD3Swap and Topup Card

SwapOrderState

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

DeniedReason (used by Card Transaction Notify's deniedReason)

NameDescriptor
CLIENT_NOT_ACTIVATEDClient Not Activated
CARD_EXPIREDCard Expired
CARD_NOT_ACTIVATEDCard Not Activated
MCC_RATE_LIMIT_EXCEEDEDMCC Rate Limit Exceeded
CARD_FROZENCard Frozen
CARD_TERMINATEDCard Terminated
TRANSACTION_LIMIT_EXCEEDEDTransaction Limit Exceeded
MONTHLY_LIMIT_EXCEEDEDMonthly Limit Exceeded
MAS_LIMIT_EXCEEDEDMAS Limit Exceeded
INSUFFICIENT_FUNDSInsufficient Funds
INVALID_ORIGINAL_TRANSACTIONInvalid Original Transaction
INVALID_TRANSACTIONInvalid Transaction
INVALID_AMOUNTInvalid Amount
SYSTEM_ERRORSystem Error
INSTITUTION_LIMITInstitution Limit
ATM_DAILY_COUNT_LIMIT_EXCEEDEDATM Daily Count Limit Exceeded
ATM_AMOUNT_LIMIT_EXCEEDEDATM Amount Limit Exceeded
MAS_ANNUAL_LIMIT_WARNING_90MAS Annual Limit Warning 90
MAGSTRIPE_NOT_PERMITTEDMagstripe Not Permitted
ANTI_SCAM_MERCHANT_BLOCKEDAnti Scam Merchant Blocked
INVALID_VTS_CARD_TOKENInvalid VTS Card Token
SANCTION_COUNTRYSanction 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).

NameIDDescription
PIN0Used before Set/Reset Card PIN
CARD_PHONE1Used 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):

CodeDescription
00034Your account is suspended, please contact us.
00035Please submit the KYC information first
00036Your account verification is pending. Please complete KYC to unlock full access.
00037Your account is rejected. Please contact us.
00041Your account is currently inactive. Please contact us for assistance.
00042Your account is currently deactivated. Please contact us for assistance.
00043Your account is currently restricted. Please contact us to resolve this issue.
00044Your account is currently terminated. Please contact us for more information.
00045Your 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:

EndpointEnforced?
Card ApplicationYes
Card ActivationYes
Set Card PINYes
Reset Card PINYes
Get Card Sensitive InfoYes
Every other Card Issuing / Card Transaction endpointNo

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).