Inyo

Tokenizando Cartões

Visão Geral

Os requisitos do PCI DSS proíbem armazenar ou transmitir dados brutos de cartão a menos que você possua a certificação apropriada. A solução de tokenização da Inyo cuida disso para você: a biblioteca inyo.js criptografa os dados do cartão diretamente no navegador e retorna um token que só as suas chaves de API podem usar.

Como funciona:

  1. Carregue o inyo.js na sua página de pagamento
  2. Adicione atributos data-field aos seus campos de entrada de cartão
  3. Inicialize o InyoTokenizer com a sua chave pública e callbacks
  4. Chame tokenizeCard() quando o usuário enviar o formulário — a biblioteca lê o formulário, criptografa os dados e retorna um token
  5. Envie o token para o seu backend para criar um pagamento via POST /v2/payment

Whitelist de Domínios (Obrigatório)

Por razões de CORS e segurança, a URL da página que carrega o tokenizador deve ser registrada na Inyo antes de poder fazer requisições de tokenização. Isso se aplica a todos os ambientes (sandbox e produção).

  • Antes de entrar em produção, forneça à Inyo a URL de origem completa (ex.: https://checkout.yoursite.com) de cada página que chamará o tokenizador.
  • Qualquer mudança na URL (novo domínio, subdomínio ou porta) deve ser comunicada à Inyo para que a whitelist seja atualizada.
  • Requisições de tokenização de origens fora da whitelist serão bloqueadas pelo CORS.

Se você estiver usando a integração PostMessage para o 3DS, a URL da página que recebe os eventos postMessage também deve ser registrada na Inyo. Isso garante que apenas a página pretendida possa escutar os resultados da autenticação 3DS. Veja Tratando o 3D Secure para detalhes.

Entre em contato com o seu gerente de integração Inyo ou com [email protected] para registrar as suas URLs.

Carregando a Biblioteca

Inclua o script do tokenizador com um hash de integridade para prevenir adulterações:

<script
  src="https://cdn.simpleps.com/sandbox/inyo.js"
  integrity="sha384-..."
  crossorigin="anonymous">
</script>
AmbienteURL
Sandboxhttps://cdn.simpleps.com/sandbox/inyo.js
Produçãohttps://cdn.simpleps.com/production/inyo.js

Configuração do Formulário HTML

Adicione atributos data-field aos elementos de entrada do cartão. O tokenizador se vincula a eles automaticamente — não é necessário ler os valores dos inputs você mesmo.

<div id="payment-form">
  <!-- Nome do portador do cartão -->
  <input type="text" data-field="cardholder" id="cc-name" required>

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

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

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

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

Valores obrigatórios de data-field:

data-fieldCampoObservações
cardholderNome completo no cartãoComo impresso no cartão
panNúmero do cartão13–19 dígitos
expirationDateValidadeFormato: MM/YY
securitycodeCVV/CVC3 dígitos (Visa/MC/Discover) ou 4 dígitos (Amex)

Inicializando o Tokenizador

Crie uma instância do InyoTokenizer quando o DOM estiver pronto. A configuração do 3DS depende do método de integração que você escolher — URL Redirect ou PostMessage:

Exemplo — URL Redirect (tradicional)

O provedor de 3DS redireciona o navegador para o seu successUrl ou failUrl após a autenticação. Ideal para aplicações renderizadas no servidor e fluxos de checkout de página inteira.

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();
  });
});

Exemplo — PostMessage (SPA / embutido)

O resultado do 3DS é entregue via API postMessage do navegador para a janela pai. Ideal para single-page apps e experiências de checkout em iframe/modal — sem necessidade de navegação 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();
  });
});

Ao usar enablePostMessage: true, você não fornece successUrl nem failUrl. Em vez disso, você escuta eventos message na janela pai após abrir o redirectAcsUrl em um iframe. Veja Tratando o 3D Secure para o guia de implementação completo.

Parâmetros de Configuração

ParâmetroTipoObrigatórioDescrição
targetIdstringSimSeletor CSS do contêiner que contém os inputs data-field (ex.: '#payment-form')
publicKeystringSimSua chave pública de estabelecimento, fornecida pela Inyo
successCallbackfunctionSimChamado quando a tokenização é bem-sucedida
errorCallbackfunctionSimChamado quando a tokenização falha
storeLaterUsebooleanNãofalse (padrão) = token de uso único; true = token recorrente/armazenado
threeDSDataobjectNãoConfiguração do 3D Secure (veja abaixo)

Configuração do 3DS (threeDSData)

CampoTipoObrigatórioDescrição
enablebooleanSimSe deve solicitar autenticação 3DS
successUrlstringApenas modo Redirect. URL para a qual o provedor de 3DS redireciona em caso de sucesso
failUrlstringApenas modo Redirect. URL para a qual o provedor de 3DS redireciona em caso de falha
enablePostMessagebooleanApenas modo PostMessage. Defina true para receber os resultados do 3DS via window.postMessage em vez de redirecionamento de URL

Escolha um modo:

  • Redirect: Defina successUrl + failUrl. Não defina enablePostMessage.
  • PostMessage: Defina enablePostMessage: true. Não defina successUrl / failUrl.

Habilitar o 3DS não garante que um challenge ocorrerá — o banco emissor decide com base na sua avaliação de risco. Veja Tratando o 3D Secure para o fluxo completo e exemplos de código para ambos os modos.

Tratando os Callbacks

Callback de Sucesso

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

  if (response.reasonCode === 'WAITING_TRANSACTION') {
    // Token criado — extraia-o
    const token = response.additionalData.token;
    const lastFour = response.additionalData.lastFour;

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

    // Envie o token para o seu servidor backend
    // Seu backend chamará POST /v2/payment com este token
    submitPaymentToBackend(token);
  } else {
    console.warn('Unexpected response step:', response.step);
  }
}

Campos da resposta de sucesso:

CampoDescrição
reasonCode"WAITING_TRANSACTION" quando o token está pronto
additionalData.tokenO UUID do token do cartão — use como cardTokenId nas requisições de pagamento
additionalData.lastFourÚltimos 4 dígitos do número do cartão (para exibição)

Callback de Erro

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

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

  // Destaca o campo específico que falhou
  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 erro:

CódigoDescrição
INVALID_PANO número do cartão é inválido (falhou na verificação de Luhn ou bandeira não suportada)
INVALID_EXPIRY_DATEA data de validade é inválida ou o cartão está expirado
INVALID_CVVO código de segurança é inválido

Tokens de Uso Único vs. Recorrentes

Tipo de TokenstoreLaterUseUso
Uso únicofalseApenas uma transação — não pode ser reutilizado
RecorrentetruePode ser armazenado e reutilizado para cobranças futuras

Regras:

  • Você não deve armazenar tokens de uso único após o uso
  • Você pode armazenar tokens recorrentes no lado do servidor para transações futuras
  • Você nunca deve armazenar dados brutos do cartão (PAN, CVV, validade), independentemente do tipo de token
  • Ao usar um token recorrente armazenado para um pagamento subsequente, passe o previousPaymentId (da autorização original) junto com o cardTokenId

Exemplos Completos

Exemplo A — URL Redirect (checkout renderizado no 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;
        // Envie o token para o seu backend → POST /v2/payment
        // Se a API retornar status: "CHALLENGE", seu backend
        // redireciona o navegador para redirectAcsUrl.
        // Após a autenticação, o provedor de 3DS redireciona para
        // o seu successUrl ou 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>

Exemplo B — PostMessage (SPA / checkout embutido)

<!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 do 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   // ← Sem necessidade de 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');
        }
      });

      // Escuta os resultados do 3DS via postMessage
      window.addEventListener('message', handle3DSResult);
    });

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

        // Chame seu backend para criar o pagamento
        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) {
          // Abre o challenge 3DS no overlay com 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) {
      // Segurança: valide a origem da mensagem
      if (!event.origin.includes('simpleps.com')) return;

      const data = event.data;

      // Fecha o overlay do 3DS
      document.getElementById('threeds-overlay').style.display = 'none';
      document.getElementById('threeds-iframe').src = '';

      if (data.approved === true || data.approved === 'true') {
        // Sempre verifique no seu backend antes de concluir o 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 Passos

Com um token em mãos, prossiga para Autorizando um Pagamento com Cartão para criar a transação via POST /v2/payment.