Cardholders
Cardholders are managed users your business creates and controls. You can issue physical or virtual cards to them, fund their wallets, and monitor spending — all programmatically.
Full Lifecycle
1. Create cardholder
2. Complete KYC (via SumSub token)
3. Apply for a card (submits application to card provider)
4. Issue a card (once application is approved)
5. Fund the cardholder's wallet
6. Manage card — freeze/unfreeze, set limits, view secrets
7. Monitor transactions
8. Recall unused fundsCardholder Object
{
"id": "uuid",
"merchant_id": "uuid",
"name": "Bob Jones",
"email": "[email protected]",
"username": "bob_abc123",
"is_kyc_verified": false,
"balances": [
{ "currency": "USDC", "balance": 10000000, "rate": 1.0, "value": 10000000 }
],
"created_at": "2026-03-16T10:00:00Z"
}balances is included in the list response. Each entry uses micro-units (divide by 1,000,000 for display). Only non-zero balances are returned.
Card Object
{
"id": "uuid",
"user_id": "uuid",
"catalogue_id": "uuid",
"status": "active",
"ctype": "virtual",
"last4": "4242",
"exp_month": "03",
"exp_year": "2029",
"spend_tx_max": 100000,
"spend_daily_max": 500000,
"spend_monthly_max": 2000000,
"spend_online_enabled": true,
"spend_present_enabled": false,
"created_at": "2026-03-16T10:00:00Z"
}Card status values: notActivated | active | locked | canceled
Step 1 — Create Cardholder
POST /v1/cardholdersBody
| Field | Type | Required | Description |
|---|---|---|---|
name | string | ✓ | Full name |
email | string | Email address |
Example
curl -X POST https://api-test.dpt.xyz/v1/cardholders \
-H "Authorization: Bearer dptb_..." \
-H "Content-Type: application/json" \
-d '{ "name": "Bob Jones", "email": "[email protected]" }'Response 200
Returns the Cardholder object. Note the id — you will need it for all subsequent calls.
Step 2 — Start KYC
Returns a SumSub SDK token. Embed the SumSub Web SDK in your app and pass the token to collect the cardholder's identity documents.
POST /v1/cardholders/{id}/kycResponse 200
{ "token": "sumsub_sdk_token_..." }After the cardholder completes verification, is_kyc_verified becomes true. KYC status is updated automatically via webhook from SumSub — no polling needed.
Step 3 — Apply for a Card
Submit a card application to the card provider. The cardholder must be KYC-verified first. The application is reviewed and approved asynchronously — status updates arrive via webhook.
POST /v1/cardholders/{id}/cards/applyBody
| Field | Type | Required | Description |
|---|---|---|---|
catalogue | uuid | ✓ | Card product ID (get from dashboard) |
account_purpose | string | ✓ | Intended use of the card (e.g. business_expenses) |
occupation | string | ✓ | Cardholder's occupation |
annual_salary | string | ✓ | Annual salary band (e.g. 50000_100000) |
expected_monthly_volume | string | ✓ | Expected monthly spend band (e.g. 1000_5000) |
Example
curl -X POST https://api-test.dpt.xyz/v1/cardholders/CH_ID/cards/apply \
-H "Authorization: Bearer dptb_..." \
-H "Content-Type: application/json" \
-d '{
"catalogue": "CATALOGUE_UUID",
"account_purpose": "business_expenses",
"occupation": "software_engineer",
"annual_salary": "50000_100000",
"expected_monthly_volume": "1000_5000"
}'Response 200
Returns the card application object including status. Once status is approved, proceed to issue a card.
Step 4 — Issue a Card
The cardholder must have an approved card application before a card can be issued.
POST /v1/cardholders/{id}/cardsBody
| Field | Type | Required | Description |
|---|---|---|---|
catalogue | uuid | ✓ | Card product ID — must match the approved application |
type | string | ✓ | virtual or physical |
Example
curl -X POST https://api-test.dpt.xyz/v1/cardholders/CH_ID/cards \
-H "Authorization: Bearer dptb_..." \
-H "Content-Type: application/json" \
-d '{ "catalogue": "CATALOGUE_UUID", "type": "virtual" }'Step 5 — Fund Cardholder
Transfer funds from your business wallet to the cardholder's wallet.
POST /v1/cardholders/{id}/fundBody
| Field | Type | Required | Description |
|---|---|---|---|
amount | integer | ✓ | Amount in smallest unit |
currency | string | ✓ | e.g. USDC |
remark | string | Optional memo |
Example
curl -X POST https://api-test.dpt.xyz/v1/cardholders/CH_ID/fund \
-H "Authorization: Bearer dptb_..." \
-H "Content-Type: application/json" \
-d '{ "amount": 1000000000, "currency": "USDC" }'Returns 204 No Content.
List Cardholders
GET /v1/cardholdersGet Cardholder
GET /v1/cardholders/{id}Recall Funds
Pull unused funds back from a cardholder's wallet to your business wallet.
POST /v1/cardholders/{id}/recallSame body as Fund Cardholder. Returns 204 No Content.
List Cardholder Cards
GET /v1/cardholders/{id}/cardsGet Card
GET /v1/cardholders/{ch_id}/cards/{card_id}Update Card (Freeze / Unfreeze)
PATCH /v1/cardholders/{ch_id}/cards/{card_id}Body
| Field | Type | Required | Description |
|---|---|---|---|
status | string | ✓ | active to unfreeze, locked to freeze |
Update Card Spending Limits
PUT /v1/cardholders/{ch_id}/cards/{card_id}/limitsBody
| Field | Type | Description |
|---|---|---|
spend_tx_max | integer | Max per-transaction amount (cents/micro-units) |
spend_daily_max | integer | Max daily spend |
spend_monthly_max | integer | Max monthly spend |
spend_online_enabled | bool | Allow online / card-not-present transactions |
spend_present_enabled | bool | Allow in-person / contactless transactions |
Card Secrets & PIN — Encryption Model
Sensitive card data (PAN, CVC, PIN) is never returned in plaintext. Instead, the API uses end-to-end encryption: you generate a one-time AES key, encrypt it with DPT's RSA public key to create a session, and we re-encrypt the card data with your AES key before returning it. Only your client can decrypt the response.
How it works
1. Client generates a random 32-byte AES key
2. Client RSA-OAEP encrypts base64(aesKey) with DPT's public key → session
3. Client sends { session } to the API
4. Server decrypts session → recovers aesKey
5. Server fetches card data from the card provider and re-encrypts it with aesKey
6. Client receives { iv, data } and decrypts with their aesKeyAll encrypted payloads use AES-256-GCM with a 16-byte IV. The data field is base64(ciphertext + 16-byte auth tag).
DPT Public Key
Use the key that matches your environment. Always use the test key against api-test.dpt.xyz and the production key against api.dpt.xyz.
Test
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtQZcsZCAT/fRiACkhLoJ
l6gZM6YV5koykEYHUtdH5yRklfiKWnX0zMsGeCXvkS3kYBb+VzSe7VOo+9NgOFW9
IWoVi7n4FTWVbBzHX+fG/fvu7jkrdustVi64uYf7aNoh0F63w7Hp+Zgdlbpy67u5
/sb2tIKNHpgl7keS9G/mLzkIFkugNslNj7TeyFm35pqKwjPUUvymW36Bp1nB4ThI
J11+cxIhp7Dzq8CWZT0ZwEnR2kmIKWX88aSoerLLnPRFm5kPIWITyimcdDkYV3FA
ozBn8e7Xz/K6VenePWvdqI3UzX+F4beOUkLbi+wp4IySAM7nr8dPXefK5MVp75On
PQIDAQAB
-----END PUBLIC KEY-----Production
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkrpc2FGCmfGzVsA8+njm
iVAS2j05f0/PvFaCDeYn7oLCzMMa+00PjfkurfT9ink99n2rRx1fr3m0GN0DQrsS
T4M3PxU7vFB1cToSeSBTdrvIrI59fPQt7vOe9tqCJ/wZw/tG5pAfLoCCPJ0f0Q+B
osuVEmdVWQZrZPtUx1dR2o2mHEQpwseRwUCWKc7MK0icU3AWw2+ItQsjXTNKq54l
+nnz76xJsT/ycKiJHz160Xh+xb9tcQW5ASkqRzxF9QlJ4IeT/6kIG32H+DYF1HI0
DGuWxgHUGS+Kvvw8Axi+ItBHtHZSmCZq6jbI1ePrS01znVcw1KJhmoGTCgf32M1a
CwIDAQAB
-----END PUBLIC KEY-----TypeScript helper
Install zero dependencies — the examples below use only Node.js built-ins (node:crypto).
import * as crypto from "node:crypto";
interface EncryptedData {
iv: string; // base64-encoded 16-byte IV
data: string; // base64-encoded ciphertext with 16-byte GCM tag appended
}
/**
* Generate a one-time AES-256 key and produce the `session` value required
* by all card secrets / PIN endpoints.
*
* Keep `aesKey` in memory — you need it to decrypt the API response.
* Never persist or log it.
*/
function createCardSession(dptPublicKeyPem: string): {
aesKey: Buffer;
session: string;
} {
const aesKey = crypto.randomBytes(32);
// The RSA payload is the base64 representation of the raw key bytes
const payload = Buffer.from(aesKey.toString("base64"), "utf8");
const encrypted = crypto.publicEncrypt(
{
key: dptPublicKeyPem,
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: "sha1",
},
payload,
);
return { aesKey, session: encrypted.toString("base64") };
}
/** Decrypt an { iv, data } payload returned by the API. */
function cardDecrypt(edata: EncryptedData, aesKey: Buffer): string {
const iv = Buffer.from(edata.iv, "base64");
const buf = Buffer.from(edata.data, "base64");
const tag = buf.subarray(buf.length - 16);
const ciphertext = buf.subarray(0, buf.length - 16);
const decipher = crypto.createDecipheriv("aes-256-gcm", aesKey, iv);
decipher.setAuthTag(tag);
return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8");
}
/** Encrypt a plaintext string with your AES key (used for set PIN). */
function cardEncrypt(plaintext: string, aesKey: Buffer): EncryptedData {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv("aes-256-gcm", aesKey, iv);
const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
const tag = cipher.getAuthTag();
return {
iv: iv.toString("base64"),
data: Buffer.concat([encrypted, tag]).toString("base64"),
};
}
/**
* Format a PIN string into the PIN block expected by the API.
* PIN must be 4–12 digits.
*
* Example: "1234" → "241234FFFFFFFFFF"
*/
function buildPinBlock(pin: string): string {
if (!/^\d{4,12}$/.test(pin)) throw new Error("PIN must be 4–12 digits");
const lenHex = pin.length.toString(16);
return "2" + lenHex + pin + "F".repeat(14 - pin.length);
}Get Card Secrets (PAN + CVC)
Returns the full card number and CVC, encrypted with your AES key.
POST /v1/cardholders/{ch_id}/cards/{card_id}/secretsBody
| Field | Type | Required | Description |
|---|---|---|---|
session | string | ✓ | RSA-encrypted AES key — from createCardSession() |
Response 200
{
"encryptedPan": { "iv": "base64...", "data": "base64..." },
"encryptedCvc": { "iv": "base64...", "data": "base64..." }
}TypeScript example
const DPT_PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----`;
const API_BASE = "https://api.dpt.xyz/v1";
const token = "dptb_...";
async function getCardSecrets(chId: string, cardId: string) {
const { aesKey, session } = createCardSession(DPT_PUBLIC_KEY_PEM);
const res = await fetch(`${API_BASE}/cardholders/${chId}/cards/${cardId}/secrets`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ session }),
});
if (!res.ok) throw new Error(await res.text());
const { encryptedPan, encryptedCvc } = await res.json();
return {
pan: cardDecrypt(encryptedPan, aesKey),
cvc: cardDecrypt(encryptedCvc, aesKey),
};
}
// Usage
const { pan, cvc } = await getCardSecrets("ch_id_here", "card_id_here");
console.log(pan); // "4111111111111234"
console.log(cvc); // "123"Get Card PIN
Returns the current PIN, encrypted with your AES key.
GET /v1/cardholders/{ch_id}/cards/{card_id}/pinBody
| Field | Type | Required | Description |
|---|---|---|---|
session | string | ✓ | RSA-encrypted AES key — from createCardSession() |
Response 200
{
"encryptedPin": { "iv": "base64...", "data": "base64..." }
}TypeScript example
async function getCardPin(chId: string, cardId: string) {
const { aesKey, session } = createCardSession(DPT_PUBLIC_KEY_PEM);
const res = await fetch(`${API_BASE}/cardholders/${chId}/cards/${cardId}/pin`, {
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ session }),
});
if (!res.ok) throw new Error(await res.text());
const { encryptedPin } = await res.json();
// Decrypt the PIN block and extract the digits
const pinBlock = cardDecrypt(encryptedPin, aesKey);
// Block format: "2{lenHex}{pin}{F...}" — e.g. "241234FFFFFFFFFF"
const pinLen = parseInt(pinBlock[1], 16);
return pinBlock.slice(2, 2 + pinLen); // "1234"
}Set Card PIN
PUT /v1/cardholders/{ch_id}/cards/{card_id}/pinBody
| Field | Type | Required | Description |
|---|---|---|---|
session | string | ✓ | RSA-encrypted AES key — from createCardSession() |
encrypted_data | object | ✓ | PIN block encrypted with your AES key |
Returns 200 OK.
TypeScript example
async function setCardPin(chId: string, cardId: string, newPin: string) {
const { aesKey, session } = createCardSession(DPT_PUBLIC_KEY_PEM);
// Build and encrypt the PIN block
const pinBlock = buildPinBlock(newPin); // e.g. "241234FFFFFFFFFF"
const encrypted_data = cardEncrypt(pinBlock, aesKey);
const res = await fetch(`${API_BASE}/cardholders/${chId}/cards/${cardId}/pin`, {
method: "PUT",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ session, encrypted_data }),
});
if (!res.ok) throw new Error(await res.text());
}
// Usage
await setCardPin("ch_id_here", "card_id_here", "1234");List Card Transactions
GET /v1/cardholders/{ch_id}/cards/{card_id}/transactionsReturns the most recent card transactions for this card.
Response 200
[
{
"id": "uuid",
"amount": -50000,
"currency": "USDC",
"description": "Starbucks #1234",
"status": "completed",
"created_at": "2026-03-16T09:00:00Z"
}
]