Tarjetas Virtuales
Crea y gestiona tarjetas virtuales para pagos en línea usando el SDK de Bloque.
Descripción General
Las tarjetas virtuales proporcionan una forma segura de realizar pagos en línea sin exponer información financiera sensible. Las características incluyen:
- Creación Instantánea: Las tarjetas se crean inmediatamente
- Cumplimiento PCI: Manejo seguro de datos de tarjetas
- Múltiples Tarjetas: Los usuarios pueden tener múltiples tarjetas
- Saldos en Tiempo Real: Consulta saldos en múltiples activos
- Historial de Transacciones: Seguimiento completo de transacciones
Creando una Tarjeta Virtual
Creación Básica
Crea una tarjeta virtual para un usuario:
create-card.ts
import { SDK } from '@bloque/sdk';
const bloque = new SDK({
origin: 'tu-origen',
auth: {
type: 'apiKey',
apiKey: process.env.BLOQUE_API_KEY!,
},
mode: 'production',
});
// Conectar a la sesión del usuario
const userSession = await bloque.connect('user-alias');
// Crear una tarjeta virtual
const card = await userSession.accounts.card.create({
holderUrn: 'did:bloque:tu-origen:user-alias',
name: 'Mi Tarjeta Virtual',
});
console.log('Tarjeta creada:', card.lastFour);
console.log('Estado:', card.status);
console.log('URL de detalles:', card.detailsUrl);
Parámetros
tipos.ts
interface CreateCardParams {
holderUrn?: string; // Optional user URN
name?: string; // Optional card name
webhookUrl?: string; // Optional webhook for events
ledgerId?: string; // Optional ledger account ID
metadata?: Record<string, unknown>; // Custom metadata
}
Respuesta
tipos.ts
interface CardAccount {
urn: string; // Unique resource name
id: string; // Card account ID
lastFour: string; // Last four digits
productType: 'CREDIT' | 'DEBIT';
status: CardStatus;
cardType: 'VIRTUAL' | 'PHYSICAL';
detailsUrl: string; // PCI-compliant details URL
ownerUrn: string;
ledgerId: string;
webhookUrl: string | null;
metadata?: Record<string, unknown>;
createdAt: string;
updatedAt: string;
balance?: Record<string, TokenBalance>; // Only in list responses
}
Listando Tarjetas
Lista todas las cuentas de tarjetas de un usuario con sus saldos:
list-cards.ts
// Usando sesión conectada (recomendado)
const userSession = await bloque.connect('user-alias');
const cards = await userSession.accounts.card.list();
console.log(`Se encontraron ${cards.accounts.length} cuentas de tarjeta`);
cards.accounts.forEach((card) => {
console.log('\nCard:', card.metadata?.name);
console.log('Últimos cuatro:', card.lastFour);
console.log('Estado:', card.status);
if (card.balance) {
Object.entries(card.balance).forEach(([token, balance]) => {
console.log(`${token}: ${balance.current}`);
});
}
});
// Calcular balance total
const totalBalances: Record<string, bigint> = {};
cards.accounts.forEach(card => {
if (card.balance) {
Object.entries(card.balance).forEach(([token, balance]) => {
if (!totalBalances[token]) {
totalBalances[token] = BigInt(0);
}
totalBalances[token] += BigInt(balance.current);
});
}
});
Consultando Saldo
Obtiene el saldo actual de una tarjeta específica:
check-balance.ts
const balances = await bloque.accounts.card.balance({
urn: 'did:bloque:account:card:usr-123:crd-456',
});
Object.entries(balances).forEach(([token, balance]) => {
console.log(`${token}:`);
console.log(` Actual: ${balance.current}`);
console.log(` Pendiente: ${balance.pending}`);
console.log(` Total Entradas: ${balance.in}`);
console.log(` Total Salidas: ${balance.out}`);
const net = BigInt(balance.in) - BigInt(balance.out);
console.log(` Neto: ${net.toString()}`);
});
Historial de Transacciones
Listado Básico
Lista las transacciones de una tarjeta:
list-transactions.ts
const movements = await bloque.accounts.card.movements({
urn: 'did:bloque:account:card:usr-123:crd-456',
asset: 'DUSD/6',
});
movements.forEach((transaction) => {
console.log(`${transaction.direction.toUpperCase()}: ${transaction.amount}`);
console.log(`Date: ${transaction.created_at}`);
if (transaction.details?.metadata?.merchant_name) {
console.log(`Merchant: ${transaction.details.metadata.merchant_name}`);
}
});
Con Filtros y Paginación
filter-transactions.ts
// Get recent incoming transactions
const recentIncoming = await bloque.accounts.card.movements({
urn: 'did:bloque:account:card:usr-123:crd-456',
asset: 'DUSD/6',
limit: 50,
direction: 'in', // Only incoming
after: '2025-01-01T00:00:00Z',
});
// Get transactions from a specific date range
const dateRange = await bloque.accounts.card.movements({
urn: 'did:bloque:account:card:usr-123:crd-456',
asset: 'KSM/12',
after: '2025-01-01T00:00:00Z',
before: '2025-12-31T23:59:59Z',
limit: 100,
});
// Buscar por referencia
const byReference = await bloque.accounts.card.movements({
urn: 'did:bloque:account:card:usr-123:crd-456',
asset: 'DUSD/6',
reference: '0xbff43fa587...',
});
Ejemplo de Paginación
paginate-transactions.ts
async function getAllTransactions(cardUrn: string, asset: string) {
const pageSize = 100;
let allMovements = [];
let hasMore = true;
let lastDate: string | undefined;
while (hasMore) {
const movements = await bloque.accounts.card.movements({
urn: cardUrn,
asset,
limit: pageSize,
before: lastDate,
});
allMovements.push(...movements.data);
console.log(`Se obtuvieron ${movements.data.length} transacciones`);
if (movements.data.length < pageSize) {
hasMore = false;
} else {
lastDate = movements.data[movements.data.length - 1].created_at;
}
}
return allMovements;
}
Gestión de Tarjetas
Actualizar Nombre de Tarjeta
update-card.ts
const updatedCard = await bloque.accounts.card.updateMetadata({
urn: 'did:bloque:account:card:usr-123:crd-456',
metadata: {
name: 'Mi Tarjeta de Negocio'
}
});
console.log('Card name updated:', updatedCard.metadata?.name);
Estados de Tarjeta
Las tarjetas pueden estar en diferentes estados:
stateDiagram-v2
[*] --> creation_in_progress
creation_in_progress --> active
creation_in_progress --> creation_failed
active --> disabled
active --> frozen
active --> deleted
disabled --> deleted
frozen --> active
frozen --> disabled
frozen --> deleted
creation_failed --> [*]
deleted --> [*]
Visualizando Detalles de Tarjeta
El campo detailsUrl proporciona una URL segura y compatible con PCI donde los usuarios pueden ver su número de tarjeta completo, CVV y fecha de expiración:
view-card-details.ts
const card = await bloque.accounts.card.create({
holderUrn: userUrn,
name: 'Mi Tarjeta',
});
// Redirect user to view card details
console.log('Ver detalles de tarjeta:', card.detailsUrl);
Seguridad
Nunca almacenes o registres números de tarjeta completos, CVVs u otros datos sensibles de tarjetas. Usa siempre el detailsUrl proporcionado para mostrar los detalles de la tarjeta a los usuarios.
Múltiples Tarjetas
Los usuarios pueden tener múltiples tarjetas para diferentes propósitos:
multiple-cards.ts
const userUrn = 'did:bloque:tu-origen:user-alias';
const userSession = await bloque.connect(userUrn);
// Tarjeta personal
const personalCard = await userSession.accounts.card.create({
holderUrn: userUrn,
name: 'Tarjeta Personal',
});
// Tarjeta de negocio
const businessCard = await userSession.accounts.card.create({
holderUrn: userUrn,
name: 'Tarjeta Negocio',
});
// Tarjeta de viaje
const travelCard = await userSession.accounts.card.create({
holderUrn: userUrn,
name: 'Gastos de Viaje',
});
console.log('Se crearon 3 tarjetas:');
console.log('- Personal:', personalCard.lastFour);
console.log('- Business:', businessCard.lastFour);
console.log('- Travel:', travelCard.lastFour);
Manejo de Errores
Siempre maneja los errores apropiadamente:
error-handling.ts
try {
const card = await bloque.accounts.card.create({
holderUrn: userUrn,
name: cardName,
});
console.log('Tarjeta creada:', card.urn);
} catch (error) {
if (error instanceof Error) {
console.error('Card creation failed:', error.message);
// Handle specific errors
if (error.message.includes('not found')) {
// User doesn't exist
} else if (error.message.includes('unauthorized')) {
// API key issues
}
}
throw error;
}
Ejemplo Completo
setup-user-card.ts
import { SDK } from '@bloque/sdk';
const bloque = new SDK({
origin: 'tu-origen',
auth: {
type: 'apiKey',
apiKey: process.env.BLOQUE_API_KEY!,
},
mode: 'production',
});
async function setupUserCard(userUrn: string) {
try {
// Conectar a la sesión del usuario
const userSession = await bloque.connect(userUrn);
// Crear una tarjeta virtual
const card = await userSession.accounts.card.create({
holderUrn: userUrn,
name: 'Tarjeta Principal',
});
console.log('✓ Tarjeta creada:', card.lastFour);
// Consultar saldo
const balances = await bloque.accounts.card.balance({
urn: card.urn,
});
console.log('✓ Saldos actuales:');
Object.entries(balances).forEach(([token, balance]) => {
console.log(` ${token}: ${balance.current}`);
});
// Get recent transactions
const movements = await bloque.accounts.card.movements({
urn: card.urn,
asset: 'DUSD/6',
limit: 10,
});
console.log(`✓ Se encontraron ${movements.data.length} transacciones recientes`);
return { success: true, card };
} catch (error) {
console.error('✗ Setup failed:', error);
throw error;
}
}
Controles de Gasto
Los controles de gasto determinan cómo se procesan las transacciones con tarjeta, incluyendo qué cuentas se debitan, cómo funciona la conversión multicurrency y cómo se enrutan los fondos. El modo de control de gasto se configura a través del metadata de la tarjeta al crearla o actualizarla.
Control de Gasto por Defecto
El modo por defecto debita una única cuenta de tarjeta. Soporta conversión automática de moneda cuando la moneda de la transacción difiere del activo de la tarjeta.
create-card-default.ts
const card = await userSession.accounts.card.create({
urn: userUrn,
name: 'Tarjeta Estándar',
metadata: {
spending_control: 'default', // Opcional, es el valor por defecto
default_asset: 'DUSD/6', // Activo principal de la tarjeta
fallback_asset: 'KSM/12', // Usado cuando el activo principal no está disponible
currency_asset_map: { // Mapea monedas de transacción a activos preferidos
USD: ['DUSD/6'],
COP: ['COPM/2'],
EUR: ['EURc/6'],
},
},
});
Control de Gasto Inteligente
El gasto inteligente permite enrutamiento multi-bolsillo basado en Códigos de Categoría de Comercio (MCC). Las transacciones se enrutan a subcuentas (bolsillos) según la categoría del comercio, y el excedente se maneja por bolsillos de menor prioridad.
create-card-smart.ts
const card = await userSession.accounts.card.create({
urn: userUrn,
name: 'Tarjeta Inteligente',
metadata: {
spending_control: 'smart',
priority_mcc: [
'did:bloque:account:card:usr-xxx:pocket-food',
'did:bloque:account:card:usr-xxx:pocket-transport',
'did:bloque:account:card:usr-xxx:pocket-main',
],
mcc_whitelist: {
'did:bloque:account:card:usr-xxx:pocket-food': ['5411', '5412', '5812', '5814'],
'did:bloque:account:card:usr-xxx:pocket-transport': ['4111', '4121', '4131'],
// Sin whitelist para pocket-main = propósito general (acepta cualquier MCC)
},
default_asset: 'DUSD/6',
fallback_asset: 'KSM/12',
},
});
Cómo funciona el enrutamiento:
- El MCC del comercio se compara contra la whitelist de cada bolsillo en orden de prioridad
- Los bolsillos elegibles se debitan comenzando por el de mayor prioridad
- Si un bolsillo tiene fondos insuficientes, el remanente pasa al siguiente bolsillo elegible
- Los bolsillos sin entrada en la whitelist actúan como receptores generales
- Los reembolsos se enrutan de vuelta a los bolsillos originales que fueron debitados, proporcionalmente
flowchart LR
TX[Transacción MCC 5411] --> P1{Bolsillo Comida}
P1 -->|Cubre monto total| Done[Debitar Comida]
P1 -->|Fondos insuficientes| P2{Bolsillo Principal}
P2 --> Split[Débito dividido]
Whitelists de MCC
Puedes proporcionar las whitelists de MCC como arrays en línea o como URLs que apuntan a un array JSON. Usar URLs facilita actualizar las whitelists sin modificar el metadata de la tarjeta.
Configuración de Comisiones
Las comisiones se pueden configurar en dos niveles usando el array spending_fees. Cada comisión puede ser incondicional o estar controlada por una regla que evalúa el contexto de la transacción al momento de la autorización.
Prioridad de Resolución (Fusión por Nombre)
Las comisiones se fusionan a través de tres capas por fee_name. Cada capa puede sobreescribir comisiones específicas de la capa inferior y agregar nuevas. Las comisiones que no se redefinen en una capa superior siempre se preservan.
Por ejemplo, si los valores por defecto definen interchange y fx, el origen agrega premium, y la tarjeta sobreescribe fx:
Las Comisiones Base No Se Pueden Eliminar
Las comisiones de capas inferiores siempre se preservan. Una tarjeta u origen puede cambiar el value, type, account_urn o rule de una comisión existente redefiniéndola con el mismo fee_name, pero no puede eliminar una comisión por completo.
Definiendo Comisiones a Nivel de Origen
Las comisiones a nivel de origen aplican a todas las tarjetas bajo el origen. Se configuran a través del panel de Bloque o la API:
origin-fee-configuration.ts
// Metadata del origen (configurado via panel de Bloque o API)
{
spending_fees: [
{
fee_name: 'interchange',
account_urn: 'urn:bloque:treasury:interchange',
type: 'percentage',
value: 0.0144, // 1.44% del monto de la transacción
category: 'interchange',
},
{
fee_name: 'fx',
account_urn: 'urn:bloque:treasury:fx',
type: 'percentage',
value: 0.02, // 2% — controla el spread de conversión
category: 'fx',
rule: 'fx_conversion',
},
{
fee_name: 'high_ticket_fx',
account_urn: 'urn:bloque:treasury:fx',
type: 'percentage',
value: 0.035, // 3.5% — solo en liquidaciones USD de alto valor
category: 'fx',
rule: 'amount_range_usd',
rule_params: { min: 500, max: 5000 },
},
{
fee_name: 'apple_wallet',
account_urn: 'urn:bloque:treasury:wallet',
type: 'percentage',
value: 0.01, // 1% — solo para transacciones de Apple Wallet
rule: 'wallet',
rule_params: { wallet_name: 'Apple' },
},
],
}
Definiendo Comisiones a Nivel de Tarjeta
Las comisiones a nivel de tarjeta sobreescriben las del origen para una tarjeta específica. Se configuran en metadata.spending_fees al crear o actualizar la tarjeta:
card-fee-override.ts
const card = await userSession.accounts.card.create({
urn: userUrn,
name: 'Tarjeta Premium',
metadata: {
spending_fees: [
{
fee_name: 'interchange',
account_urn: 'urn:bloque:treasury:interchange',
type: 'percentage',
value: 0.01, // 1% — interchange reducido para esta tarjeta
category: 'interchange',
},
{
fee_name: 'fx',
account_urn: 'urn:bloque:treasury:fx',
type: 'percentage',
value: 0.015, // 1.5% — spread FX reducido para esta tarjeta
category: 'fx',
rule: 'fx_conversion',
},
],
},
});
Sobreescritura de Comisiones por Tarjeta
Las comisiones a nivel de tarjeta se fusionan por fee_name. Solo necesitas incluir las comisiones que deseas sobreescribir o agregar — todas las demás comisiones del origen y los valores por defecto se preservan automáticamente.
Tipos de Comisión
types.ts
type SpendingFeeCategory = 'fx' | 'interchange' | 'custom';
interface SpendingFee {
fee_name: string; // Identificador único (ej. "interchange", "fx")
account_urn: string; // Cuenta destino para la comisión
type: 'percentage' | 'flat';
value: number; // Tasa (0.02 = 2%) para porcentaje, o monto para fijo
category?: SpendingFeeCategory; // Propósito de la comisión (por defecto "custom")
rule?: string; // Opcional: nombre de regla condicional
rule_params?: Record<string, unknown>; // Opcional: parámetros para la regla
}
Categoría FX y Spread de Conversión
Las comisiones con category: "fx" controlan directamente el spread del tipo de cambio aplicado durante las conversiones de moneda. Por ejemplo, una comisión fx con value: 0.02 resulta en un spread del 2% (tasa multiplicada por 0.98). Si múltiples comisiones tienen category: "fx", sus valores se suman para determinar el spread total. Si no se configuran comisiones con categoría fx, se usa el spread por defecto del 2%.
Reglas Condicionales de Comisiones
Las comisiones sin campo rule siempre se aplican. Las comisiones con una regla se evalúan contra la solicitud completa de la transacción al momento de la autorización.
Cálculo de Comisiones
Las comisiones se calculan usando aritmética BigInt para evitar problemas de precisión de punto flotante. Durante reembolsos parciales (ajustes), las comisiones se recalculan proporcionalmente basándose en el monto del reembolso relativo a la autorización original.
Desglose de Comisiones en Eventos
Cada webhook de transacción exitosa incluye un objeto fee_breakdown mostrando exactamente qué comisiones se aplicaron:
types.ts
interface FeeBreakdown {
total_fees: string; // Comisiones totales como string BigInt escalado
fees: Array<{
fee_name: string; // Identificador de la comisión
amount: string; // Monto de la comisión como string BigInt escalado
rate: number; // La tasa o valor fijo utilizado
}>;
}
Webhooks de Tarjeta
Cuando proporcionas un webhookUrl durante la creación de la tarjeta, tu endpoint recibe notificaciones POST en tiempo real para cada evento de transacción — compras, rechazos y ajustes (reembolsos).
Payload del Webhook
Todos los eventos de tarjeta comparten esta estructura:
types.ts
interface CardWebhookPayload {
account_urn: string; // URN de la cuenta de tarjeta
transaction_id: string; // Identificador único de la transacción
type: 'authorization' | 'adjustment';
direction: 'debit' | 'credit';
event: CardEventType;
amount?: string; // Monto escalado como string BigInt
asset?: string; // Activo utilizado (ej. "DUSD/6")
local_amount?: number; // Monto en la moneda local del comercio
local_currency?: string; // Código de moneda ISO 4217 (ej. "USD", "COP")
exchange_rate?: number; // Tasa de conversión aplicada (1 si coincidencia directa)
merchant?: MerchantInfo;
medium?: TransactionMedium;
fee_breakdown?: FeeBreakdown;
reason?: string; // Presente en eventos de rechazo
}
Tipos de Evento
Ejemplo de Evento de Compra
purchase-webhook.json
{
"account_urn": "did:bloque:account:card:usr-abc:crd-123",
"transaction_id": "ctx-200kXoaEJLNzcsvNxY1pmBO7fEx",
"type": "authorization",
"direction": "debit",
"event": "purchase",
"amount": "50000000",
"asset": "DUSD/6",
"local_amount": 50.0,
"local_currency": "USD",
"exchange_rate": 1,
"merchant": {
"name": "AMAZON.COM",
"mcc": "5411",
"city": "Seattle",
"country": "USA"
},
"fee_breakdown": {
"total_fees": "720000",
"fees": [
{ "fee_name": "interchange", "amount": "720000", "rate": 0.0144 }
]
}
}
Ejemplo de Evento de Reembolso
refund-webhook.json
{
"account_urn": "did:bloque:account:card:usr-abc:crd-123",
"transaction_id": "ctx-adj-456",
"type": "adjustment",
"direction": "credit",
"event": "credit_adjustment",
"amount": "25000000",
"asset": "DUSD/6",
"local_amount": 25.0,
"local_currency": "USD",
"exchange_rate": 1,
"merchant": {
"name": "AMAZON.COM",
"mcc": "5411",
"city": "Seattle",
"country": "USA"
},
"fee_breakdown": {
"total_fees": "360000",
"fees": [
{ "fee_name": "interchange", "amount": "360000", "rate": 0.0144 }
]
}
}
Ejemplo de Evento de Rechazo
rejection-webhook.json
{
"account_urn": "did:bloque:account:card:usr-abc:crd-123",
"transaction_id": "ctx-789",
"type": "authorization",
"direction": "debit",
"event": "rejected_insufficient_funds",
"required_usd": 150.0,
"reason": "Insufficient funds"
}
Manejando Webhooks
card-webhook-handler.ts
import express from 'express';
const app = express();
app.post('/webhooks/cards', express.json(), (req, res) => {
const payload = req.body;
console.log(`[${payload.transaction_id}] ${payload.event} — ${payload.type}/${payload.direction}`);
switch (payload.event) {
case 'purchase':
console.log(`Compra: ${payload.local_amount} ${payload.local_currency}`);
console.log(`Comercio: ${payload.merchant?.name}`);
console.log(`Comisiones: ${payload.fee_breakdown?.total_fees}`);
// Actualizar registros, notificar usuario, etc.
break;
case 'credit_adjustment':
console.log(`Reembolso: ${payload.local_amount} ${payload.local_currency}`);
// Procesar reembolso en tu sistema
break;
case 'debit_adjustment':
console.log(`Ajuste de débito: ${payload.local_amount} ${payload.local_currency}`);
break;
case 'rejected_insufficient_funds':
console.log(`Rechazado: ${payload.reason}`);
// Notificar al usuario de la transacción fallida
break;
case 'rejected_currency':
console.log(`Moneda no soportada: ${payload.currency}`);
break;
case 'rejected_credit':
console.log(`Crédito rechazado: ${payload.reason}`);
break;
}
res.status(200).send('OK');
});
Idempotencia
La entrega de webhooks usa claves de idempotencia basadas en ${type}-${transaction_id}. Tu handler debe ser idempotente — procesar el mismo evento dos veces debe producir el mismo resultado.
Notificaciones de WhatsApp
Los eventos de compra también pueden activar notificaciones de WhatsApp al titular de la tarjeta si whatsapp_notification está habilitado en el metadata de la tarjeta y el usuario tiene un número de teléfono registrado.
Mejores Prácticas
- Sesiones de Usuario: Siempre conéctate a una sesión de usuario
- KYC Primero: Asegúrate de que los usuarios completen la verificación KYC
- Nombres Significativos: Ayuda a los usuarios a identificar sus tarjetas
- Verificar Estado: Verifica el estado de la tarjeta antes de usarla
- Seguridad: Nunca almacenes números de tarjeta completos
- Manejo de Errores: Usa bloques try-catch
- Probar en Sandbox: Prueba exhaustivamente antes de producción
- Webhooks Idempotentes: Siempre maneja entregas duplicadas de forma segura
- Transparencia de Comisiones: Incluye el desglose de comisiones en los recibos mostrados a los usuarios
- Presupuestos Inteligentes: Usa controles de gasto por bolsillo para ayudar a los usuarios a gestionar presupuestos por categoría
Próximos Pasos