Skip to content

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 funds

Cardholder Object

json
{
  "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

json
{
  "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

http
POST /v1/cardholders

Body

FieldTypeRequiredDescription
namestringFull name
emailstringEmail address

Example

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

http
POST /v1/cardholders/{id}/kyc

Response 200

json
{ "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.

http
POST /v1/cardholders/{id}/cards/apply

Body

FieldTypeRequiredDescription
catalogueuuidCard product ID (get from dashboard)
account_purposestringIntended use of the card (e.g. business_expenses)
occupationstringCardholder's occupation
annual_salarystringAnnual salary band (e.g. 50000_100000)
expected_monthly_volumestringExpected monthly spend band (e.g. 1000_5000)

Example

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

http
POST /v1/cardholders/{id}/cards

Body

FieldTypeRequiredDescription
catalogueuuidCard product ID — must match the approved application
typestringvirtual or physical

Example

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

http
POST /v1/cardholders/{id}/fund

Body

FieldTypeRequiredDescription
amountintegerAmount in smallest unit
currencystringe.g. USDC
remarkstringOptional memo

Example

bash
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

http
GET /v1/cardholders

Get Cardholder

http
GET /v1/cardholders/{id}

Recall Funds

Pull unused funds back from a cardholder's wallet to your business wallet.

http
POST /v1/cardholders/{id}/recall

Same body as Fund Cardholder. Returns 204 No Content.


List Cardholder Cards

http
GET /v1/cardholders/{id}/cards

Get Card

http
GET /v1/cardholders/{ch_id}/cards/{card_id}

Update Card (Freeze / Unfreeze)

http
PATCH /v1/cardholders/{ch_id}/cards/{card_id}

Body

FieldTypeRequiredDescription
statusstringactive to unfreeze, locked to freeze

Update Card Spending Limits

http
PUT /v1/cardholders/{ch_id}/cards/{card_id}/limits

Body

FieldTypeDescription
spend_tx_maxintegerMax per-transaction amount (cents/micro-units)
spend_daily_maxintegerMax daily spend
spend_monthly_maxintegerMax monthly spend
spend_online_enabledboolAllow online / card-not-present transactions
spend_present_enabledboolAllow 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 aesKey

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

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

http
POST /v1/cardholders/{ch_id}/cards/{card_id}/secrets

Body

FieldTypeRequiredDescription
sessionstringRSA-encrypted AES key — from createCardSession()

Response 200

json
{
  "encryptedPan": { "iv": "base64...", "data": "base64..." },
  "encryptedCvc": { "iv": "base64...", "data": "base64..." }
}

TypeScript example

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

http
GET /v1/cardholders/{ch_id}/cards/{card_id}/pin

Body

FieldTypeRequiredDescription
sessionstringRSA-encrypted AES key — from createCardSession()

Response 200

json
{
  "encryptedPin": { "iv": "base64...", "data": "base64..." }
}

TypeScript example

typescript
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

http
PUT /v1/cardholders/{ch_id}/cards/{card_id}/pin

Body

FieldTypeRequiredDescription
sessionstringRSA-encrypted AES key — from createCardSession()
encrypted_dataobjectPIN block encrypted with your AES key

Returns 200 OK.

TypeScript example

typescript
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

http
GET /v1/cardholders/{ch_id}/cards/{card_id}/transactions

Returns the most recent card transactions for this card.

Response 200

json
[
  {
    "id": "uuid",
    "amount": -50000,
    "currency": "USDC",
    "description": "Starbucks #1234",
    "status": "completed",
    "created_at": "2026-03-16T09:00:00Z"
  }
]

DPT Merchant API