Payer Auth API v1.0.0
Microservicio independiente que encapsula el flujo completo de CyberSource 3D Secure 2.x (EMV 3DS) Payer Authentication. Soporta múltiples comercios, cada uno con sus propias credenciales CyberSource.
Base URL
Quick Start en 5 Pasos
- Crear un comercio
Llamar aPOST /api/v1/merchantcon tux-admin-keyy las credenciales de CyberSource. - Guardar tu API Key
LaapiKeysolo se muestra al crear el comercio. Guárdala en un lugar seguro. - Llamar a /setup
Enviar datos de tarjeta y facturación. Recibirás elaccessTokenydeviceDataCollectionUrl. - Completar Device Data Collection
Montar el iframe oculto de Cardinal Commerce para recopilar información del dispositivo. - Llamar a /enrollment
EnviardeviceInfodel navegador. Manejar el flujo frictionless o lanzar el iframe de challenge.
Arquitectura Multi-Tenant
Cada comercio usa sus propias credenciales CyberSource identificadas por su API Key.
Autenticación
La API soporta dos mecanismos de autenticación por header HTTP.
🔑 API Key
Para comercios. Se aplica a todos los endpoints excepto los públicos.
Valor: UUID generado al crear el comercio.
{ "error": 1, "message": "Unauthorized: Missing or invalid API key." }
🔐 Admin Key
Para administración. Solo aplica a endpoints /api/v1/merchant.
Valor: variable de entorno ADMIN_API_KEY.
{ "error": 1, "message": "Forbidden: Invalid admin key." }
Endpoints Públicos (sin autenticación)
| Método | Endpoint | Descripción |
|---|---|---|
| GET | /health | Health check del servicio |
| POST | /api/v1/3ds/challenge/response | Callback automático de CyberSource |
Flujo 3DS Completo
Diagrama de secuencia del flujo completo de autenticación 3D Secure. Haz clic en cada paso para ver detalles.
Endpoints — Flujo 3DS
x-api-keyEjecuta Decision Manager (evaluación de riesgo) y Payer Auth Setup. Retorna el accessToken y deviceDataCollectionUrl necesarios para el iframe de Device Data Collection.
Request Body
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| reference | string | ✓ | ID de correlación del cliente |
| cardNumber | string | ✓ | Número de tarjeta completo |
| expirationMonth | string | ✓ | Mes de expiración (MM) |
| expirationYear | string | ✓ | Año de expiración (YYYY) |
| amount | string/number | ✓ | Monto de la transacción |
| currency | string | opcional | Código ISO. Default: MXN |
| callbackUrl | string | opcional | URL de redirección post-challenge |
| billingInfo | object | ✓ | Datos de facturación (ver abajo) |
| deviceInfo | object | opcional | Info del dispositivo (si disponible en este paso) |
billingInfo (requerido)
| Campo | Descripción |
|---|---|
| firstName, lastName | Nombre del titular |
| Email del titular | |
| country | Código ISO 2 letras (ej. MX) |
| postalCode, state, address1 | Dirección de facturación |
{
"reference": "order-12345",
"cardNumber": "4111111111111111",
"expirationMonth": "12",
"expirationYear": "2025",
"amount": "150.00",
"currency": "MXN",
"callbackUrl": "https://mi-sitio.com/3ds-result",
"billingInfo": {
"firstName": "Juan",
"lastName": "Pérez",
"email": "juan@example.com",
"country": "MX",
"postalCode": "01000",
"state": "CDMX",
"address1": "Av. Reforma 123"
}
}
curl -X POST https://auth.etomin.com/api/v1/3ds/setup \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"reference": "order-12345",
"cardNumber": "4111111111111111",
"expirationMonth": "12",
"expirationYear": "2025",
"amount": "150.00",
"currency": "MXN",
"callbackUrl": "https://mi-sitio.com/3ds-result",
"billingInfo": {
"firstName": "Juan",
"lastName": "Pérez",
"email": "juan@example.com",
"country": "MX",
"postalCode": "01000",
"state": "CDMX",
"address1": "Av. Reforma 123"
}
}'
Response 200
{
"error": 0,
"transactionId": "60f1b2c3d4e5f6a7b8c9d0e1",
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"deviceDataCollectionUrl": "https://centinelapi.cardinalcommerce.com/V1/Cruise/Collect",
"referenceId": "abc123def456",
"orgUnitId": "5a4b3c2d1e0f"
}
x-api-keyEnvía la información del dispositivo del navegador recopilada por el frontend. Retorna resultado frictionless o URL de challenge.
deviceInfo — 10 campos requeridos
| Campo | Tipo | Ejemplo |
|---|---|---|
| httpAccept | string | "text/html,application/xhtml+xml" |
| httpUserAgent | string | "Mozilla/5.0 (Macintosh...)" |
| httpBrowserColorDepth | string | "24" |
| httpBrowserJavaEnabled | boolean | false |
| httpBrowserJavaScriptEnabled | boolean | true |
| httpBrowserLanguage | string | "es-MX" |
| httpBrowserScreenHeight | string | "900" |
| httpBrowserScreenWidth | string | "1440" |
| httpBrowserTimeDifference | string | "360" |
| ipAddress | string | "192.168.1.100" |
{
"error": 0,
"challengeRequired": false,
"authFlow": "FRICTIONLESS",
"transactionId": "60f1b2c3d4e5f6a7b8c9d0e1",
"reasonCode": "100",
"consumerAuthenticationInformation": {
"cavv": "AAABAWFlmQAAAABjRWWZEEFgFz8=",
"cavvAlgorithm": "2",
"eciRaw": "05",
"eci": "05",
"xid": "...",
"directoryServerTransactionId": "...",
"specificationVersion": "2.2.0",
"paresStatus": "Y",
"authenticationResult": "Y"
}
}
{
"error": 0,
"challengeRequired": true,
"authFlow": "CHALLENGE",
"transactionId": "60f1b2c3d4e5f6a7b8c9d0e1",
"enrollmentToken": "eyJhbGciOiJIUzI1NiJ9...",
"stepUpUrl": "https://centinelapi.cardinalcommerce.com/V2/Cruise/StepUp",
"authenticationTransactionId": "..."
}
Callback después de que el tarjetahabiente completa el challenge. No es llamado por el frontend directamente.
Responde con HTML que envía un postMessage al parent window y redirige al callbackUrl del comercio con el resultado en base64:
https://mi-sitio.com/3ds-result?result=eyJlcnJvciI6MCwidHJhbnNhY3Rpb25JZC...
El query param result decodificado en Base64 contiene el JSON del resultado de autenticación.
x-api-keyConsulta o re-valida el resultado de autenticación. Si ya fue validado, retorna el resultado cacheado.
{ "transactionId": "60f1b2c3d4e5f6a7b8c9d0e1" }
{
"error": 0,
"transactionId": "60f1b2c3d4e5f6a7b8c9d0e1",
"reference": "order-12345",
"authFlow": "FRICTIONLESS",
"reasonCode": "100",
"authenticationResult": "Y",
"consumerAuthenticationInformation": {
"cavv": "...",
"eci": "05",
"xid": "...",
"directoryServerTransactionId": "...",
"specificationVersion": "2.2.0",
"paresStatus": "Y"
}
}
Merchant Management (Admin)
x-admin-key.Crea un nuevo comercio con credenciales CyberSource. Genera automáticamente un UUID como API Key.
apiKey solo se muestra en este response. Guárdala inmediatamente.{
"name": "Mi Comercio",
"cybersourceMerchantId": "merchant_live_id",
"cybersourceMerchantKeyId": "live_key_id",
"cybersourceMerchantSecretKey": "live_secret_key",
"cybersourceMerchantIdSandbox": "merchant_test",
"cybersourceMerchantKeyIdSandbox": "test_key",
"cybersourceMerchantSecretKeySandbox": "test_secret",
"mode": "SANDBOX",
"status": "ACTIVE",
"orgUnitId": "5a4b3c2d1e0f",
"defaultCurrency": "MXN"
}
{
"error": 0,
"message": "Merchant created successfully.",
"data": {
"id": "60f1b2c3d4e5f6a7b8c9d0e1",
"name": "Mi Comercio",
"apiKey": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"mode": "SANDBOX",
"status": "ACTIVE"
}
}
Retorna la lista de todos los comercios con credenciales enmascaradas.
Obtiene los detalles de un comercio específico por su id.
Actualiza la configuración del comercio. Solo actualiza los campos enviados en el body (PATCH semántico).
Campos actualizables: name, status, mode, cybersourceMerchantId, cybersourceMerchantKeyId, cybersourceMerchantSecretKey, cybersourceMerchantIdSandbox, cybersourceMerchantKeyIdSandbox, cybersourceMerchantSecretKeySandbox, orgUnitId, defaultCurrency, challengeReturnUrl.
{ "status": "ok", "timestamp": "2025-01-15T10:30:00.000Z" }
Guía de Integración Frontend
Código JavaScript completo para integrar el flujo 3DS en tu frontend.
Paso 1 — Llamar a /setup
const setupResponse = await fetch('https://auth.etomin.com/api/v1/3ds/setup', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': 'YOUR_API_KEY'
},
body: JSON.stringify({
reference: 'order-12345',
cardNumber: '4111111111111111',
expirationMonth: '12',
expirationYear: '2025',
amount: '150.00',
currency: 'MXN',
callbackUrl: window.location.origin + '/3ds-result',
billingInfo: {
firstName: 'Juan',
lastName: 'Pérez',
email: 'juan@example.com',
country: 'MX',
postalCode: '01000',
state: 'CDMX',
address1: 'Av. Reforma 123'
}
})
});
const setup = await setupResponse.json();
// setup.transactionId, setup.accessToken, setup.deviceDataCollectionUrl
Paso 2 — Device Data Collection (iframe oculto)
// Crear iframe oculto para DDC
const iframe = document.createElement('iframe');
iframe.name = 'collectionIframe';
iframe.style.display = 'none';
document.body.appendChild(iframe);
// Crear form y enviar al DDC URL
const form = document.createElement('form');
form.method = 'POST';
form.target = 'collectionIframe';
form.action = setup.deviceDataCollectionUrl;
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'JWT';
input.value = setup.accessToken;
form.appendChild(input);
document.body.appendChild(form);
form.submit();
// Escuchar evento de DDC completado
window.addEventListener('message', function(event) {
if (event.origin.includes('cardinalcommerce.com')) {
console.log('DDC Complete:', event.data);
// Proceder al enrollment
}
});
Paso 3 — Recopilar deviceInfo y llamar a /enrollment
const deviceInfo = {
httpAccept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
httpUserAgent: navigator.userAgent,
httpBrowserColorDepth: String(screen.colorDepth),
httpBrowserJavaEnabled: false,
httpBrowserJavaScriptEnabled: true,
httpBrowserLanguage: navigator.language,
httpBrowserScreenHeight: String(screen.height),
httpBrowserScreenWidth: String(screen.width),
httpBrowserTimeDifference: String(new Date().getTimezoneOffset()),
ipAddress: '0.0.0.0' // Obtener del servidor o servicio externo
};
const enrollResponse = await fetch('https://auth.etomin.com/api/v1/3ds/enrollment', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': 'YOUR_API_KEY'
},
body: JSON.stringify({
transactionId: setup.transactionId,
deviceInfo
})
});
const enrollment = await enrollResponse.json();
Paso 4 — Manejar Challenge (iframe visible)
if (enrollment.challengeRequired) {
// Crear iframe para step-up challenge
const stepUpIframe = document.createElement('iframe');
stepUpIframe.name = 'step-up-iframe';
stepUpIframe.height = '400';
stepUpIframe.width = '400';
document.getElementById('challenge-container').appendChild(stepUpIframe);
// Enviar JWT al step-up URL
const stepUpForm = document.createElement('form');
stepUpForm.method = 'POST';
stepUpForm.target = 'step-up-iframe';
stepUpForm.action = enrollment.stepUpUrl;
const jwtInput = document.createElement('input');
jwtInput.type = 'hidden';
jwtInput.name = 'JWT';
jwtInput.value = enrollment.enrollmentToken;
stepUpForm.appendChild(jwtInput);
const mdInput = document.createElement('input');
mdInput.type = 'hidden';
mdInput.name = 'MD';
mdInput.value = enrollment.transactionId;
stepUpForm.appendChild(mdInput);
document.body.appendChild(stepUpForm);
stepUpForm.submit();
// Escuchar resultado del challenge via postMessage
window.addEventListener('message', function(event) {
if (event.data && event.data.type === '3ds-complete') {
console.log('Challenge complete:', event.data.result);
// event.data.href contiene la URL de redirección
}
});
}
Paso 5 — Verificar resultado
// Si se usa callbackUrl, decodificar el result del query param
const params = new URLSearchParams(window.location.search);
const resultBase64 = params.get('result');
if (resultBase64) {
const result = JSON.parse(atob(resultBase64));
console.log('3DS Result:', result);
// result.authenticationResult: 'Y' = exitoso
// result.consumerAuthenticationInformation.cavv
// result.consumerAuthenticationInformation.eci
}
const validationResponse = await fetch('https://auth.etomin.com/api/v1/3ds/validation', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': 'YOUR_API_KEY'
},
body: JSON.stringify({
transactionId: setup.transactionId
})
});
const validation = await validationResponse.json();
Valores de Autenticación 3DS
| Campo | Descripción | Valores |
|---|---|---|
| authenticationResult / paresStatus | Resultado de autenticación | Y=Exitoso · N=Fallido · U=No disponible · R=Rechazado · A=Intento |
| eci | Electronic Commerce Indicator | 05=Visa auth · 06=Visa intento · 02=MC auth · 01=MC intento |
| cavv | Cardholder Auth Verification Value | Base64 string — valor criptográfico de autenticación |
| xid | Transaction Identifier | Identificador de la transacción 3DS |
| directoryServerTransactionId | DS Transaction ID | UUID del Directory Server |
| specificationVersion | Versión 3DS | "2.1.0" o "2.2.0" |
| reasonCode | Código de razón CyberSource | 100=Exitoso · 475=Pendiente · 476=Challenge |
Tipos de Tarjeta Soportados
| Marca | Código CyberSource | Prefijos |
|---|---|---|
| Visa | 001 | 4 |
| Mastercard | 002 | 51-55, 2221-2720 |
| American Express | 003 | 34, 37 |
| Discover | 004 | 6011, 644-649, 65 |
| JCB | 007 | 3528-3589 |
| Maestro | 042 | 50, 56-69 |
Tarjetas de Prueba (Sandbox)
| Número | Marca | Escenario |
|---|---|---|
| 4111111111111111 | Visa | Frictionless Autenticación exitosa (Y) |
| 4000000000002503 | Visa | Challenge Challenge requerido |
| 4000000000002511 | Visa | Fallida Challenge — Autenticación fallida (N) |
| 5200000000001096 | Mastercard | Frictionless Autenticación exitosa |
| 5200000000001005 | Mastercard | Challenge Challenge requerido |
| 378282246310005 | AMEX | Frictionless Autenticación exitosa |
Códigos de Error
Formato estándar de respuesta de error:
{
"error": 1,
"message": "Descripción del error",
"details": "Detalle técnico (solo en 500)"
}
| HTTP | Mensaje | Causa |
|---|---|---|
| 400 | Missing required fields: ... | Campos obligatorios no enviados |
| 400 | Missing required billingInfo fields: ... | Sub-campos de billingInfo faltantes |
| 400 | Missing required deviceInfo fields: ... | Campos del navegador faltantes |
| 401 | Unauthorized: Missing or invalid API key | x-api-key ausente o inválido |
| 403 | Forbidden: Invalid admin key | x-admin-key ausente o inválido |
| 404 | Transaction not found... | Transacción no existe o en paso incorrecto |
| 404 | Merchant not found | Comercio no existe |
| 500 | Payer Auth Setup failed | Error en CyberSource Setup |
| 500 | Check Enrollment failed | Error en CyberSource Enrollment |
| 500 | Internal error during 3DS... | Error interno del servidor |
Estados de la Transacción
Máquina de estados del flujo de autenticación. Haz clic en un estado para ver su descripción.
authFlow & challengeStatus
| Campo | Valores posibles |
|---|---|
| authFlow | PENDING → FRICTIONLESS | CHALLENGE |
| challengeStatus | pending → processing → completed (lock atómico anti-duplicados) |