Payer Auth API v1.0.0
API de autenticación 3D Secure 2.x (EMV 3DS) basada en CyberSource Payer Authentication. Soporta dos modos de integración: un flujo simplificado (recomendado) y un flujo avanzado para clientes que requieren control granular.
Base URL
Quick Start — Flujo Simplificado (3 Pasos)
- POST /api/v1/3ds/auth
Enviar datos de tarjeta y facturación con tux-api-key. Recibirás untransactionIdy unauthUrl. - Abrir authUrl en un iframe
DDC, enrollment y challenge se ejecutan automáticamente dentro del iframe. - Escuchar el resultado
Recibir resultado viapostMessagecon CAVV, ECI, XID. O redirección automática alcallbackUrl.
Arquitectura Multi-Tenant
Cada comercio usa sus propias credenciales CyberSource identificadas por su API Key.
Autenticación
Todos los endpoints de 3DS requieren el header x-api-key con el API Key del merchant (UUID v4).
🔑 API Key
Se aplica a todos los endpoints /api/v1/3ds/* excepto los públicos.
Valor: UUID v4 generado al crear el comercio.
{ "error": 1, "message": "Unauthorized: Missing or invalid API key." }
Endpoints Públicos (sin autenticación)
| Método | Endpoint | Descripción |
|---|---|---|
| GET | /api/v1/3ds/ddc/:transactionId | DDC automatizado (interno) |
| POST | /api/v1/3ds/ddc/:transactionId/complete | Enrollment automatizado (interno) |
| POST | /api/v1/3ds/challenge/response | Callback automático de CyberSource |
| GET | /health | Health check del servicio |
Flujo Simplificado (Recomendado)
Ideal para la mayoría de integraciones. El cliente solo necesita hacer un POST, abrir una URL en un iframe y escuchar un evento.
POST /api/v1/3ds/auth → Recibe { transactionId, authUrl }
authUrl en un iframe → DDC, enrollment y challenge se ejecutan automáticamente
postMessage → Recibe resultado con CAVV, ECI, XID. O redirección al callbackUrl
x-api-keyInicia el flujo de autenticación 3DS completo. Internamente ejecuta Decision Manager + Payer Auth Setup y retorna una URL que automatiza todo el proceso.
Request Body
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| reference | string | ✓ | ID de correlación del cliente (ej: ID de orden) |
| cardNumber | string | ✓ | Número de tarjeta completo |
| expirationMonth | string | ✓ | Mes de expiración (MM) |
| expirationYear | string | ✓ | Año de expiración (YYYY) |
| amount | string | ✓ | Monto de la transacción |
| billingInfo | object | ✓ | Datos de facturación (ver abajo) |
| currency | string | opcional | Moneda ISO 4217 (default: defaultCurrency del merchant o MXN) |
| callbackUrl | string | opcional | URL de redirección post-challenge con resultado en base64 |
| deviceInfo | object | opcional | Información del dispositivo (opcional en este paso) |
billingInfo (requerido)
| Campo | Tipo | Descripción |
|---|---|---|
| firstName | string | Nombre del tarjetahabiente |
| lastName | string | Apellido del tarjetahabiente |
| string | Email del tarjetahabiente | |
| country | string | Código de país ISO (ej: MX, US) |
| postalCode | string | Código postal |
| state | string | Estado o provincia |
| address1 | string | Dirección línea 1 |
{
"reference": "order-12345",
"cardNumber": "378282246310005",
"expirationMonth": "12",
"expirationYear": "2028",
"amount": "150.00",
"currency": "MXN",
"callbackUrl": "https://mi-sitio.com/resultado-pago",
"billingInfo": {
"firstName": "Juan",
"lastName": "Pérez",
"email": "juan@example.com",
"country": "MX",
"postalCode": "06600",
"state": "CDMX",
"address1": "Av. Reforma 222"
}
}
{
"error": 0,
"transactionId": "69d5c1c61393101fcb48e78d",
"authUrl": "https://auth.etomin.com/api/v1/3ds/ddc/69d5c1c61393101fcb48e78d"
}
400 — Campos requeridos faltantes. 500 — Error en CyberSource o error interno.
Integración del Cliente
Paso 1: Abrir authUrl en un iframe
<iframe id="3ds-frame" src="{authUrl}" width="400" height="400"></iframe>
Paso 2: Escuchar el resultado via postMessage
window.addEventListener('message', function(event) {
if (event.data && event.data.type === '3ds-complete') {
var result = event.data.result;
if (result.error === 0) {
// Autenticación exitosa
console.log('authFlow:', result.authFlow); // 'FRICTIONLESS' o 'CHALLENGE'
console.log('cavv:', result.consumerAuthenticationInformation.cavv);
console.log('eci:', result.consumerAuthenticationInformation.eci);
console.log('xid:', result.consumerAuthenticationInformation.xid);
} else {
// Error en la autenticación
console.log('Error:', result.message);
}
}
});
Estructura del postMessage (event.data)
{
"type": "3ds-complete",
"result": {
"error": 0,
"challengeRequired": false,
"authFlow": "FRICTIONLESS",
"transactionId": "69d5c1c61393101fcb48e78d",
"reference": "order-12345",
"reasonCode": "100",
"consumerAuthenticationInformation": {
"cavv": "AJkBBkhgQQAAAE4gSEJydQAAAAA=",
"eci": "07",
"eciRaw": "07",
"xid": "AgEAAI7O9IrXaURqopC/nygM5WY=",
"paresStatus": "Y",
"specificationVersion": "2.1.0",
"veresEnrolled": "B",
"authenticationTransactionId": "YVjdB2QbePwxkrVSs510"
}
},
"href": "https://mi-sitio.com/resultado-pago?result={base64}"
}
Callback URL (alternativa al postMessage)
Si se proporcionó callbackUrl en el request, al completar la autenticación el navegador redirige a:
https://mi-sitio.com/resultado-pago?result={base64}
El parámetro result es un JSON codificado en base64 con la misma estructura del postMessage. Para decodificarlo:
var result = JSON.parse(atob(urlParams.get('result')));
Flujo Avanzado (Manual)
Para clientes que requieren control total sobre cada paso del proceso 3DS. Diagrama de secuencia del flujo completo.
Endpoints — Flujo Avanzado
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"
}
}
Otros Endpoints
{ "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 |
| 340000000002534 | AMEX | Challenge Challenge requerido |
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 |
| 404 | Transaction not found... | Transacción no existe o en paso incorrecto |
| 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) |