Inyo

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

ModoInteracción del TitularDescripción
Data-onlyNingunaLos datos de la transacción se envían al emisor para la evaluación de riesgo; no se necesita acción del titular
ChallengeRequeridaEl 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:

  1. Automática (configuración de backend) — Todos los pagos requieren 3DS. Configurado por el equipo de Inyo durante el onboarding.
  2. Manual (por transacción) — Usted controla cuándo solicitar 3DS configurando threeDSData durante 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ónCómo funcionaIdeal para
Redirección por URLEl navegador redirige a redirectAcsUrl; después de la autenticación, el proveedor de 3DS redirige de vuelta a su successUrl o failUrlFlujos de checkout de página completa, aplicaciones renderizadas en el servidor
PostMessageAbra redirectAcsUrl en un iframe; el proveedor de 3DS envía un evento postMessage a la ventana padre con el resultadoAplicaciones 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 eventos postMessage.
  • 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 postMessage a 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 proporcionar successUrl ni failUrl. El resultado se entrega vía postMessage en 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ónRedirección por URLPostMessage
Experiencia de usuarioNavegación de página completa; el usuario abandona su checkoutFluida; el desafío aparece en un iframe/modal
Complejidad de implementaciónMás simple — solo configure las URLMás código — gestionar el iframe + el listener de eventos
Requisitos de servidorNecesita manejadores POST del lado del servidor para las URL de éxito/fallaNo se necesitan manejadores en el servidor para el resultado de 3DS
Compatibilidad con SPARequiere soluciones alternativas (pérdida de estado en la redirección)Encaje nativo para SPA
SeguridadResultado entregado servidor a servidor (POST a su URL)Resultado entregado del lado del cliente (¡valide el origen!)
Webview móvilFunciona en todas partesAlgunos 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 TarjetaRedTipo de 3DS
5413330033003303MastercardChallenge
5169527513596963MastercardChallenge
4983305199046950VisaChallenge
5454545454545454MastercardData-only
4975303994654672VisaData-only
6011926557021045DiscoverData-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 successUrl y failUrl en 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: true en el tokenizador; implemente un listener del evento message con 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