Manejo de 3D Secure
3D Secure (3DS) agrega una capa de autenticación del titular de la tarjeta para reducir el fraude. Cuando se activa 3DS, el titular puede necesitar verificar su identidad con su banco emisor antes de que el pago sea autorizado.
Modos de 3DS
| Modo | Interacción del Titular | Descripción |
|---|---|---|
| Data-only | Ninguna | Los datos de la transacción se envían al emisor para la evaluación de riesgo; no se necesita acción del titular |
| Challenge | Requerida | El titular verifica mediante OTP, biometría o la app del banco |
Importante: En el mercado de EE. UU., los bancos no están obligados a exigir autenticación reforzada (step-up). El banco emisor decide si emite un desafío, incluso si usted solicita uno durante la tokenización. En el mercado AFT (Account Funding Transaction), 3DS es solo para control de fraude — no hay transferencia de responsabilidad.
Habilitación de 3DS
3DS puede habilitarse de dos maneras:
- Automática (configuración de backend) — Todos los pagos requieren 3DS. Configurado por el equipo de Inyo durante el onboarding.
- Manual (por transacción) — Usted controla cuándo solicitar 3DS configurando
threeDSDatadurante la tokenización de la tarjeta.
Opciones de Integración
Cuando la API de pagos devuelve status: "CHALLENGE", el titular de la tarjeta debe completar la autenticación. Tiene dos formas de manejar el resultado del desafío:
| Opción | Cómo funciona | Ideal para |
|---|---|---|
| Redirección por URL | El navegador redirige a redirectAcsUrl; después de la autenticación, el proveedor de 3DS redirige de vuelta a su successUrl o failUrl | Flujos de checkout de página completa, aplicaciones renderizadas en el servidor |
| PostMessage | Abra redirectAcsUrl en un iframe; el proveedor de 3DS envía un evento postMessage a la ventana padre con el resultado | Aplicaciones de página única (SPA), experiencias de checkout embebidas o modales |
Opción 1: Redirección por URL
El enfoque tradicional. El navegador del titular navega a la página 3DS del banco y, después de la autenticación, es redirigido de vuelta a su sitio.
Configuración del Tokenizador
const tokenizer = new InyoTokenizer({
targetId: '#payment-form',
publicKey: 'YOUR_PUBLIC_KEY',
threeDSData: {
enable: true,
successUrl: 'https://yoursite.com/3ds/success',
failUrl: 'https://yoursite.com/3ds/fail'
},
successCallback: handleSuccess,
errorCallback: handleError
});
Flujo
1. Frontend tokenizes card → receives token
2. Backend calls POST /v2/payment with token
3. API returns status: "CHALLENGE" + redirectAcsUrl
4. Frontend redirects browser to redirectAcsUrl
5. Cardholder completes bank authentication
6. Bank redirects to successUrl (approved) or failUrl (rejected)
7. Your server-side handler receives a POST with payment data
8. Backend confirms payment status via GET /payments/{id}
Paso 4 — Redirigir al Desafío
const response = await createPayment(paymentData);
if (response.data.status === 'CHALLENGE' && response.data.redirectAcsUrl) {
// Redirección de página completa a la página 3DS del banco
window.location.href = response.data.redirectAcsUrl;
}
Paso 7a — Manejador de la URL de Éxito
El proveedor de 3DS envía un POST a su successUrl con los datos del pago:
{
"paymentId": "dce568c6-98ec-456c-bb33-4a6809c4fff8",
"externalPaymentId": "order-12345",
"amount": "99.99",
"approved": "true"
}
Ejemplo de manejador del lado del servidor (Node.js / Express):
// POST /3ds/success
app.post('/3ds/success', async (req, res) => {
const { paymentId, externalPaymentId } = req.body;
// CRÍTICO: Verifique siempre vía API — no confíe únicamente en el payload de la redirección
const payment = await fetch(
`https://{FQDN}/payments/${externalPaymentId}`,
{ headers: { 'Authorization': `Bearer ${accessToken}` } }
).then(r => r.json());
if (payment.status === 'AUTHORIZED' || payment.status === 'CAPTURED') {
// Pago confirmado — mostrar página de éxito
res.redirect(`/order/${externalPaymentId}/confirmed`);
} else {
// Estado inesperado — mostrar error
res.redirect(`/order/${externalPaymentId}/error`);
}
});
Paso 7b — Manejador de la URL de Falla
El proveedor de 3DS envía un POST a su failUrl:
{
"paymentId": "dce568c6-98ec-456c-bb33-4a6809c4fff8",
"externalId": "order-12345",
"amount": 99.99,
"approved": false,
"status": "Rejected by ACS",
"responseMsg": "Rejected by ACS",
"responseCode": "99",
"signatureVerification": "N",
"acsChallengeRequired": true,
"acsParStatus": "N"
}
// POST /3ds/fail
app.post('/3ds/fail', (req, res) => {
// La autenticación falló — redirigir al usuario de vuelta a la página de pago
res.redirect('/checkout?error=3ds_failed');
});
Nota: Cuando la autenticación falla, el pago nunca fue autorizado. No intente capturar ni anular (void).
Opción 2: PostMessage de JavaScript
Para aplicaciones de página única y experiencias de checkout embebidas, puede abrir el desafío 3DS en un iframe y recibir el resultado mediante la API postMessage del navegador — sin necesidad de redirección de página completa.
Lista Blanca de URL para PostMessage (Requerida)
La URL de la página que recibirá los eventos postMessage debe registrarse con Inyo. Esta es una medida de seguridad para garantizar que solo su página prevista pueda escuchar los resultados de autenticación 3DS.
- Antes de salir a producción, proporcione a Inyo el origen exacto de la URL (por ejemplo,
https://checkout.yoursite.com) de la página que escucha los eventospostMessage. - Cualquier cambio en la URL debe comunicarse a Inyo para que la lista blanca pueda actualizarse.
- Los resultados de 3DS no se entregarán vía
postMessagea orígenes no incluidos en la lista blanca.
Esto es adicional a la lista blanca de dominios del tokenizador descrita en Tokenización de Tarjetas. Tanto la URL de la página del tokenizador como la URL de la página que escucha el PostMessage deben estar registradas.
Configuración del Tokenizador
Configure enablePostMessage: true en threeDSData:
const tokenizer = new InyoTokenizer({
targetId: '#payment-form',
publicKey: 'YOUR_PUBLIC_KEY',
threeDSData: {
enable: true,
enablePostMessage: true
},
successCallback: handleSuccess,
errorCallback: handleError
});
Diferencia clave: Al usar
enablePostMessage: true, no necesita proporcionarsuccessUrlnifailUrl. El resultado se entrega víapostMessageen lugar de una redirección.
Flujo
1. Frontend tokenizes card → receives token
2. Backend calls POST /v2/payment with token
3. API returns status: "CHALLENGE" + redirectAcsUrl
4. Frontend opens redirectAcsUrl in an iframe (or modal)
5. Cardholder completes bank authentication inside the iframe
6. 3DS provider sends a postMessage to the parent window
7. Frontend listens for the message and handles the result
8. Backend confirms payment status via GET /payments/{id}
Paso 4 — Abrir el Desafío en un Iframe
<!-- Iframe oculto para el desafío 3DS -->
<div id="threeds-container" style="display: none;">
<iframe id="threeds-iframe" width="100%" height="500"
sandbox="allow-forms allow-scripts allow-same-origin allow-popups">
</iframe>
</div>
const response = await createPayment(paymentData);
if (response.data.status === 'CHALLENGE' && response.data.redirectAcsUrl) {
// Mostrar el contenedor del iframe
document.getElementById('threeds-container').style.display = 'block';
// Cargar la página del desafío 3DS
document.getElementById('threeds-iframe').src = response.data.redirectAcsUrl;
}
Paso 7 — Escuchar el PostMessage
window.addEventListener('message', async (event) => {
// Validar el origen por seguridad
if (!event.origin.includes('simpleps.com')) return;
const data = event.data;
// Ocultar el iframe
document.getElementById('threeds-container').style.display = 'none';
if (data.approved === true || data.approved === 'true') {
// 3DS tuvo éxito — verificar el estado del pago desde su backend
const paymentStatus = await verifyPaymentOnBackend(data.externalPaymentId);
if (paymentStatus === 'AUTHORIZED' || paymentStatus === 'CAPTURED') {
showSuccessMessage();
} else {
showErrorMessage('Payment could not be confirmed.');
}
} else {
// 3DS falló — permitir que el usuario reintente
showErrorMessage('Card verification failed. Please try again or use a different card.');
}
});
Payload de PostMessage — Éxito
{
"paymentId": "dce568c6-98ec-456c-bb33-4a6809c4fff8",
"externalPaymentId": "order-12345",
"amount": 99.99,
"approved": true
}
Payload de PostMessage — Falla
{
"paymentId": "dce568c6-98ec-456c-bb33-4a6809c4fff8",
"externalPaymentId": "order-12345",
"amount": 99.99,
"approved": false,
"status": "Rejected by ACS",
"responseCode": "99"
}
Ejemplo Completo para SPA
// Manejo completo de 3DS para una aplicación de página única
class ThreeDSHandler {
constructor() {
this.iframe = document.getElementById('threeds-iframe');
this.container = document.getElementById('threeds-container');
this.pendingResolve = null;
window.addEventListener('message', (event) => {
if (!event.origin.includes('simpleps.com')) return;
this.handleResult(event.data);
});
}
// Devuelve una promesa que se resuelve cuando 3DS finaliza
startChallenge(redirectAcsUrl) {
return new Promise((resolve) => {
this.pendingResolve = resolve;
this.container.style.display = 'block';
this.iframe.src = redirectAcsUrl;
});
}
handleResult(data) {
this.container.style.display = 'none';
this.iframe.src = '';
if (this.pendingResolve) {
this.pendingResolve({
success: data.approved === true || data.approved === 'true',
paymentId: data.paymentId,
externalPaymentId: data.externalPaymentId
});
this.pendingResolve = null;
}
}
}
// Uso en su flujo de checkout:
const threeds = new ThreeDSHandler();
async function processPayment(paymentData) {
const response = await createPayment(paymentData);
if (response.data.status === 'CHALLENGE') {
const result = await threeds.startChallenge(response.data.redirectAcsUrl);
if (result.success) {
// Verificar en el backend y luego mostrar la confirmación
const verified = await verifyPayment(result.externalPaymentId);
showConfirmation(verified);
} else {
showRetryPrompt();
}
} else if (response.data.status === 'AUTHORIZED') {
showConfirmation(response.data);
} else {
showDeclineMessage(response.data.message);
}
}
Cómo Elegir entre Redirección y PostMessage
| Consideración | Redirección por URL | PostMessage |
|---|---|---|
| Experiencia de usuario | Navegación de página completa; el usuario abandona su checkout | Fluida; el desafío aparece en un iframe/modal |
| Complejidad de implementación | Más simple — solo configure las URL | Más código — gestionar el iframe + el listener de eventos |
| Requisitos de servidor | Necesita manejadores POST del lado del servidor para las URL de éxito/falla | No se necesitan manejadores en el servidor para el resultado de 3DS |
| Compatibilidad con SPA | Requiere soluciones alternativas (pérdida de estado en la redirección) | Encaje nativo para SPA |
| Seguridad | Resultado entregado servidor a servidor (POST a su URL) | Resultado entregado del lado del cliente (¡valide el origen!) |
| Webview móvil | Funciona en todas partes | Algunos webviews restringen el postMessage en iframes |
Recomendación:
- Use Redirección por URL si tiene un checkout tradicional renderizado en el servidor o necesita máxima compatibilidad.
- Use PostMessage si está construyendo una SPA o desea una experiencia de checkout embebida sin navegación de página.
Independientemente de la opción que elija, verifique siempre el estado del pago vía la API (GET /payments/{externalPaymentId}) antes de completar el pedido.
Pruebas de 3DS
Use las tarjetas de prueba habilitadas para 3DS de la página de Datos de Prueba:
| Número de Tarjeta | Red | Tipo de 3DS |
|---|---|---|
5413330033003303 | Mastercard | Challenge |
5169527513596963 | Mastercard | Challenge |
4983305199046950 | Visa | Challenge |
5454545454545454 | Mastercard | Data-only |
4975303994654672 | Visa | Data-only |
6011926557021045 | Discover | Data-only |
Lista de Verificación de Implementación
- [ ] Registre la URL de la página de su tokenizador con Inyo (requerido para CORS)
- [ ] Elija su método de integración: Redirección por URL o PostMessage
- [ ] Si usa Redirección: Configure
successUrlyfailUrlen el tokenizador; implemente manejadores POST del lado del servidor - [ ] Si usa PostMessage: Registre con Inyo la URL de la página que escucha el PostMessage; configure
enablePostMessage: trueen el tokenizador; implemente un listener del eventomessagecon validación de origen - [ ] Detecte
status: "CHALLENGE"en las respuestas de pago - [ ] Maneje el resultado de 3DS (éxito y falla)
- [ ] Verifique siempre el estado del pago vía la API GET después de que 3DS finalice
- [ ] Pruebe con tarjetas de prueba 3DS en sandbox
- [ ] Maneje casos límite: el usuario cierra el iframe/la pestaña, timeout, errores de red
- [ ] Notifique a Inyo cualquier cambio de URL antes de desplegar en nuevos dominios
