Inyo

Tokenización de Tarjetas

Descripción General

Los requisitos de PCI DSS prohíben almacenar o transmitir datos de tarjeta sin procesar a menos que usted posea la certificación apropiada. La solución de tokenización de Inyo se encarga de esto por usted: la biblioteca inyo.js cifra los datos de la tarjeta directamente en el navegador y devuelve un token que solo sus claves de API pueden usar.

Cómo funciona:

  1. Cargue inyo.js en su página de pago
  2. Agregue atributos data-field a sus campos de entrada de tarjeta
  3. Inicialice InyoTokenizer con su clave pública y sus callbacks
  4. Llame a tokenizeCard() cuando el usuario envíe el formulario — la biblioteca lee el formulario, cifra los datos y devuelve un token
  5. Envíe el token a su backend para crear un pago mediante POST /v2/payment

Lista Blanca de Dominios (Requerido)

Por razones de CORS y de seguridad, la URL de la página que carga el tokenizador debe registrarse con Inyo antes de que pueda realizar solicitudes de tokenización. Esto aplica a todos los entornos (sandbox y producción).

  • Antes de salir a producción, proporcione a Inyo la URL de origen completa (p. ej., https://checkout.yoursite.com) de cada página que llamará al tokenizador.
  • Cualquier cambio en la URL (nuevo dominio, subdominio o puerto) debe comunicarse a Inyo para que la lista blanca pueda actualizarse.
  • Las solicitudes de tokenización desde orígenes no incluidos en la lista blanca serán bloqueadas por CORS.

Si está usando la integración PostMessage para 3DS, la URL de la página que recibe los eventos postMessage también debe registrarse con Inyo. Esto garantiza que solo su página prevista pueda escuchar los resultados de autenticación 3DS. Vea Manejo de 3D Secure para más detalles.

Contacte a su gerente de integración de Inyo o a [email protected] para registrar sus URLs.

Carga de la Biblioteca

Incluya el script del tokenizador con un hash de integridad para prevenir alteraciones:

<script
  src="https://cdn.simpleps.com/sandbox/inyo.js"
  integrity="sha384-..."
  crossorigin="anonymous">
</script>
EntornoURL
Sandboxhttps://cdn.simpleps.com/sandbox/inyo.js
Producciónhttps://cdn.simpleps.com/production/inyo.js

Configuración del Formulario HTML

Agregue atributos data-field a sus elementos de entrada de tarjeta. El tokenizador se vincula a ellos automáticamente — no necesita leer los valores de los inputs usted mismo.

<div id="payment-form">
  <!-- Nombre del tarjetahabiente -->
  <input type="text" data-field="cardholder" id="cc-name" required>

  <!-- Número de tarjeta (PAN) -->
  <input type="text" data-field="pan" id="cc-number" maxlength="19" required>

  <!-- Fecha de vencimiento (MM/YY) -->
  <input type="text" data-field="expirationDate" id="cc-expiration"
         maxlength="5" placeholder="MM/YY" required>

  <!-- CVV / Código de seguridad -->
  <input type="text" data-field="securitycode" id="cc-cvv"
         maxlength="4" required>

  <button type="button" id="pay-btn">Pay Now</button>
</div>

Valores de data-field requeridos:

data-fieldEntradaNotas
cardholderNombre completo en la tarjetaTal como está impreso en la tarjeta
panNúmero de tarjeta13–19 dígitos
expirationDateVencimientoFormato: MM/YY
securitycodeCVV/CVC3 dígitos (Visa/MC/Discover) o 4 dígitos (Amex)

Inicialización del Tokenizador

Cree una instancia de InyoTokenizer cuando el DOM esté listo. La configuración de 3DS depende del método de integración que elija — Redirección por URL o PostMessage:

Ejemplo — Redirección por URL (tradicional)

El proveedor de 3DS redirige el navegador a su successUrl o failUrl después de la autenticación. Ideal para aplicaciones renderizadas en el servidor y flujos de checkout de página completa.

document.addEventListener('DOMContentLoaded', () => {
  const tokenizer = new InyoTokenizer({
    targetId: '#payment-form',
    publicKey: 'YOUR_PUBLIC_KEY',
    storeLaterUse: false,
    threeDSData: {
      enable: true,
      successUrl: 'https://yoursite.com/3ds/success',
      failUrl: 'https://yoursite.com/3ds/fail'
    },
    successCallback: handleSuccess,
    errorCallback: handleError
  });

  document.getElementById('pay-btn').addEventListener('click', () => {
    tokenizer.tokenizeCard();
  });
});

Ejemplo — PostMessage (SPA / embebido)

El resultado de 3DS se entrega mediante la API postMessage del navegador a la ventana padre. Ideal para aplicaciones de una sola página y experiencias de checkout en iframe/modal — no requiere navegación de página.

document.addEventListener('DOMContentLoaded', () => {
  const tokenizer = new InyoTokenizer({
    targetId: '#payment-form',
    publicKey: 'YOUR_PUBLIC_KEY',
    storeLaterUse: false,
    threeDSData: {
      enable: true,
      enablePostMessage: true
    },
    successCallback: handleSuccess,
    errorCallback: handleError
  });

  document.getElementById('pay-btn').addEventListener('click', () => {
    tokenizer.tokenizeCard();
  });
});

Cuando use enablePostMessage: true, usted no proporciona successUrl ni failUrl. En su lugar, escucha eventos message en la ventana padre después de abrir el redirectAcsUrl en un iframe. Vea Manejo de 3D Secure para la guía de implementación completa.

Parámetros de Configuración

ParámetroTipoRequeridoDescripción
targetIdstringSelector CSS del contenedor que contiene los inputs con data-field (p. ej., '#payment-form')
publicKeystringSu clave pública de comerciante, proporcionada por Inyo
successCallbackfunctionSe llama cuando la tokenización tiene éxito
errorCallbackfunctionSe llama cuando la tokenización falla
storeLaterUsebooleanNofalse (predeterminado) = token de un solo uso; true = token recurrente/almacenado
threeDSDataobjectNoConfiguración de 3D Secure (vea a continuación)

Configuración de 3DS (threeDSData)

CampoTipoRequeridoDescripción
enablebooleanSi se debe solicitar la autenticación 3DS
successUrlstringSolo modo redirección. URL a la que el proveedor de 3DS redirige en caso de éxito
failUrlstringSolo modo redirección. URL a la que el proveedor de 3DS redirige en caso de falla
enablePostMessagebooleanSolo modo PostMessage. Establezca true para recibir los resultados de 3DS mediante window.postMessage en lugar de redirección por URL

Elija un modo:

  • Redirección: Establezca successUrl + failUrl. No establezca enablePostMessage.
  • PostMessage: Establezca enablePostMessage: true. No establezca successUrl / failUrl.

Habilitar 3DS no garantiza que ocurra un challenge — el banco emisor decide según su evaluación de riesgo. Vea Manejo de 3D Secure para el flujo completo y ejemplos de código para ambos modos.

Manejo de Callbacks

Callback de Éxito

function handleSuccess(response) {
  console.log('Tokenization response:', response);

  if (response.reasonCode === 'WAITING_TRANSACTION') {
    // Token creado — extráigalo
    const token = response.additionalData.token;
    const lastFour = response.additionalData.lastFour;

    console.log(`Card ending in ${lastFour}, token: ${token}`);

    // Envíe el token a su servidor backend
    // Su backend llamará a POST /v2/payment con este token
    submitPaymentToBackend(token);
  } else {
    console.warn('Unexpected response step:', response.step);
  }
}

Campos de la respuesta de éxito:

CampoDescripción
reasonCode"WAITING_TRANSACTION" cuando el token está listo
additionalData.tokenEl UUID del token de la tarjeta — úselo como cardTokenId en las solicitudes de pago
additionalData.lastFourÚltimos 4 dígitos del número de tarjeta (para mostrar)

Callback de Error

function handleError(response) {
  console.error('Tokenization error:', response);

  // Limpie los estados de error anteriores
  document.querySelectorAll('#cc-number, #cc-expiration, #cc-cvv')
    .forEach(el => el.classList.remove('is-invalid'));

  // Resalte el campo específico que falló
  switch (response.code) {
    case 'INVALID_PAN':
      document.querySelector('#cc-number').classList.add('is-invalid');
      break;
    case 'INVALID_EXPIRY_DATE':
      document.querySelector('#cc-expiration').classList.add('is-invalid');
      break;
    case 'INVALID_CVV':
      document.querySelector('#cc-cvv').classList.add('is-invalid');
      break;
    default:
      console.error('Unknown error code:', response.code);
  }
}

Códigos de error:

CódigoDescripción
INVALID_PANEl número de tarjeta es inválido (falló la verificación de Luhn o esquema no soportado)
INVALID_EXPIRY_DATELa fecha de vencimiento es inválida o la tarjeta está vencida
INVALID_CVVEl código de seguridad es inválido

Tokens de Un Solo Uso vs. Recurrentes

Tipo de TokenstoreLaterUseUso
Un solo usofalseSolo una transacción — no puede reutilizarse
RecurrentetruePuede almacenarse y reutilizarse para cargos futuros

Reglas:

  • No debe almacenar tokens de un solo uso después de usarlos
  • Puede almacenar tokens recurrentes en el servidor para transacciones futuras
  • Nunca debe almacenar datos de tarjeta sin procesar (PAN, CVV, vencimiento), sin importar el tipo de token
  • Al usar un token recurrente almacenado para un pago posterior, pase el previousPaymentId (de la autorización original) junto con el cardTokenId

Ejemplos Completos

Ejemplo A — Redirección por URL (checkout renderizado en el servidor)

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Inyo Payment — Redirect</title>
</head>
<body>

  <form id="checkout-form" novalidate>
    <div id="payment-form">
      <div>
        <label for="cc-name">Name on Card</label>
        <input type="text" data-field="cardholder" id="cc-name" required>
      </div>
      <div>
        <label for="cc-number">Card Number</label>
        <input type="text" data-field="pan" id="cc-number" maxlength="19" required>
      </div>
      <div>
        <label for="cc-expiration">Expiration</label>
        <input type="text" data-field="expirationDate" id="cc-expiration"
               placeholder="MM/YY" maxlength="5" required>
      </div>
      <div>
        <label for="cc-cvv">CVV</label>
        <input type="text" data-field="securitycode" id="cc-cvv"
               maxlength="4" required>
      </div>
      <button type="button" id="pay-btn">Pay $99.99</button>
    </div>
  </form>

  <script src="https://cdn.simpleps.com/sandbox/inyo.js"></script>
  <script>
    document.addEventListener('DOMContentLoaded', () => {
      const tokenizer = new InyoTokenizer({
        targetId: '#payment-form',
        publicKey: 'YOUR_PUBLIC_KEY',
        storeLaterUse: false,
        threeDSData: {
          enable: true,
          successUrl: 'https://yoursite.com/3ds/success',
          failUrl: 'https://yoursite.com/3ds/fail'
        },
        successCallback: handleSuccess,
        errorCallback: handleError
      });

      document.getElementById('pay-btn').addEventListener('click', () => {
        const form = document.getElementById('checkout-form');
        if (form.checkValidity()) {
          tokenizer.tokenizeCard();
        } else {
          form.classList.add('was-validated');
        }
      });
    });

    function handleSuccess(response) {
      if (response.reasonCode === 'WAITING_TRANSACTION') {
        const token = response.additionalData.token;
        // Envíe el token a su backend → POST /v2/payment
        // Si la API devuelve status: "CHALLENGE", su backend
        // redirige el navegador a redirectAcsUrl.
        // Después de la autenticación, el proveedor de 3DS redirige a
        // su successUrl o failUrl.
        fetch('/api/pay', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ cardToken: token, amount: 99.99 })
        });
      }
    }

    function handleError(response) {
      document.querySelectorAll('#cc-number, #cc-expiration, #cc-cvv')
        .forEach(el => el.classList.remove('is-invalid'));

      if (response.code === 'INVALID_PAN')
        document.querySelector('#cc-number').classList.add('is-invalid');
      else if (response.code === 'INVALID_EXPIRY_DATE')
        document.querySelector('#cc-expiration').classList.add('is-invalid');
      else if (response.code === 'INVALID_CVV')
        document.querySelector('#cc-cvv').classList.add('is-invalid');
    }
  </script>

</body>
</html>

Ejemplo B — PostMessage (checkout SPA / embebido)

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Inyo Payment — PostMessage</title>
</head>
<body>

  <form id="checkout-form" novalidate>
    <div id="payment-form">
      <div>
        <label for="cc-name">Name on Card</label>
        <input type="text" data-field="cardholder" id="cc-name" required>
      </div>
      <div>
        <label for="cc-number">Card Number</label>
        <input type="text" data-field="pan" id="cc-number" maxlength="19" required>
      </div>
      <div>
        <label for="cc-expiration">Expiration</label>
        <input type="text" data-field="expirationDate" id="cc-expiration"
               placeholder="MM/YY" maxlength="5" required>
      </div>
      <div>
        <label for="cc-cvv">CVV</label>
        <input type="text" data-field="securitycode" id="cc-cvv"
               maxlength="4" required>
      </div>
      <button type="button" id="pay-btn">Pay $99.99</button>
    </div>
  </form>

  <!-- Iframe oculto para el challenge 3DS -->
  <div id="threeds-overlay" style="display:none; position:fixed; inset:0;
       background:rgba(0,0,0,0.5); z-index:1000; align-items:center;
       justify-content:center;">
    <iframe id="threeds-iframe" style="width:500px; height:600px;
            border:none; border-radius:8px; background:white;"
            sandbox="allow-forms allow-scripts allow-same-origin allow-popups">
    </iframe>
  </div>

  <script src="https://cdn.simpleps.com/sandbox/inyo.js"></script>
  <script>
    let tokenizer;

    document.addEventListener('DOMContentLoaded', () => {
      tokenizer = new InyoTokenizer({
        targetId: '#payment-form',
        publicKey: 'YOUR_PUBLIC_KEY',
        storeLaterUse: false,
        threeDSData: {
          enable: true,
          enablePostMessage: true   // ← No se necesitan successUrl/failUrl
        },
        successCallback: handleTokenSuccess,
        errorCallback: handleTokenError
      });

      document.getElementById('pay-btn').addEventListener('click', () => {
        const form = document.getElementById('checkout-form');
        if (form.checkValidity()) {
          tokenizer.tokenizeCard();
        } else {
          form.classList.add('was-validated');
        }
      });

      // Escuche los resultados 3DS por postMessage
      window.addEventListener('message', handle3DSResult);
    });

    async function handleTokenSuccess(response) {
      if (response.reasonCode === 'WAITING_TRANSACTION') {
        const token = response.additionalData.token;

        // Llame a su backend para crear el pago
        const res = await fetch('/api/pay', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ cardToken: token, amount: 99.99 })
        });
        const payment = await res.json();

        if (payment.status === 'CHALLENGE' && payment.redirectAcsUrl) {
          // Abra el challenge 3DS en el overlay del iframe
          document.getElementById('threeds-overlay').style.display = 'flex';
          document.getElementById('threeds-iframe').src = payment.redirectAcsUrl;
        } else if (payment.status === 'AUTHORIZED' || payment.status === 'CAPTURED') {
          showSuccess(payment);
        } else {
          showError(payment.message || 'Payment declined.');
        }
      }
    }

    function handle3DSResult(event) {
      // Seguridad: valide el origen del mensaje
      if (!event.origin.includes('simpleps.com')) return;

      const data = event.data;

      // Cierre el overlay de 3DS
      document.getElementById('threeds-overlay').style.display = 'none';
      document.getElementById('threeds-iframe').src = '';

      if (data.approved === true || data.approved === 'true') {
        // Verifique siempre en su backend antes de completar el pedido
        verifyAndComplete(data.externalPaymentId);
      } else {
        showError('Card verification failed. Please try again.');
      }
    }

    async function verifyAndComplete(externalPaymentId) {
      const res = await fetch(`/api/payment/${externalPaymentId}/verify`);
      const payment = await res.json();
      if (payment.status === 'AUTHORIZED' || payment.status === 'CAPTURED') {
        showSuccess(payment);
      } else {
        showError('Payment could not be confirmed.');
      }
    }

    function handleTokenError(response) {
      document.querySelectorAll('#cc-number, #cc-expiration, #cc-cvv')
        .forEach(el => el.classList.remove('is-invalid'));

      if (response.code === 'INVALID_PAN')
        document.querySelector('#cc-number').classList.add('is-invalid');
      else if (response.code === 'INVALID_EXPIRY_DATE')
        document.querySelector('#cc-expiration').classList.add('is-invalid');
      else if (response.code === 'INVALID_CVV')
        document.querySelector('#cc-cvv').classList.add('is-invalid');
    }

    function showSuccess(payment) {
      alert(`Payment confirmed! ID: ${payment.paymentId || payment.externalPaymentId}`);
    }

    function showError(message) {
      alert(message);
    }
  </script>

</body>
</html>

Próximos Pasos

Con un token en mano, continúe con Autorización de un Pago con Tarjeta para crear la transacción mediante POST /v2/payment.