Inyo

Tratando o 3D Secure

O 3D Secure (3DS) adiciona uma camada de autenticação do portador do cartão para reduzir fraudes. Quando o 3DS é acionado, o portador do cartão pode precisar verificar sua identidade junto ao banco emissor antes que o pagamento seja autorizado.

Modos de 3DS

ModoInteração do PortadorDescrição
Data-onlyNenhumaOs dados da transação são enviados ao emissor para avaliação de risco; nenhuma ação do portador é necessária
ChallengeObrigatóriaO portador verifica via OTP, biometria ou aplicativo do banco

Importante: No mercado dos EUA, os bancos não são obrigados a aplicar autenticação step-up. O banco emissor decide se emite um challenge, mesmo que você solicite um durante a tokenização. O 3DS no mercado AFT (Account Funding Transaction) serve apenas para controle de fraude — não há transferência de responsabilidade (liability shift).

Habilitando o 3DS

O 3DS pode ser habilitado de duas formas:

  1. Automática (configuração no backend) — Todos os pagamentos exigem 3DS. Configurada pela equipe Inyo durante o onboarding.
  2. Manual (por transação) — Você controla quando solicitar o 3DS definindo threeDSData durante a tokenização do cartão.

Opções de Integração

Quando a API de pagamento retorna status: "CHALLENGE", o portador do cartão deve concluir a autenticação. Você tem duas formas de tratar o resultado do challenge:

OpçãoComo funcionaMelhor para
Redirecionamento por URLO navegador redireciona para redirectAcsUrl; após a autenticação, o provedor 3DS redireciona de volta para sua successUrl ou failUrlFluxos de checkout de página inteira, aplicações renderizadas no servidor
PostMessageAbra a redirectAcsUrl em um iframe; o provedor 3DS envia um evento postMessage para a janela pai com o resultadoSingle-page apps (SPAs), experiências de checkout embutidas/em modal

Opção 1: Redirecionamento por URL

A abordagem tradicional. O navegador do portador navega até a página 3DS do banco e, após a autenticação, é redirecionado de volta ao seu site.

Configuração do 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
});

Fluxo

1. Frontend tokeniza o cartão → recebe o token
2. Backend chama POST /v2/payment com o token
3. A API retorna status: "CHALLENGE" + redirectAcsUrl
4. Frontend redireciona o navegador para redirectAcsUrl
5. Portador conclui a autenticação no banco
6. Banco redireciona para successUrl (aprovado) ou failUrl (rejeitado)
7. Seu handler no servidor recebe um POST com os dados do pagamento
8. Backend confirma o status do pagamento via GET /payments/{id}

Etapa 4 — Redirecionar para o Challenge

const response = await createPayment(paymentData);

if (response.data.status === 'CHALLENGE' && response.data.redirectAcsUrl) {
  // Redirecionamento de página inteira para a página 3DS do banco
  window.location.href = response.data.redirectAcsUrl;
}

Etapa 7a — Handler da URL de Sucesso

O provedor 3DS envia um POST para sua successUrl com os dados do pagamento:

{
  "paymentId": "dce568c6-98ec-456c-bb33-4a6809c4fff8",
  "externalPaymentId": "order-12345",
  "amount": "99.99",
  "approved": "true"
}

Exemplo de handler no servidor (Node.js / Express):

// POST /3ds/success
app.post('/3ds/success', async (req, res) => {
  const { paymentId, externalPaymentId } = req.body;

  // CRÍTICO: Sempre verifique via API — não confie apenas no payload do redirecionamento
  const payment = await fetch(
    `https://{FQDN}/payments/${externalPaymentId}`,
    { headers: { 'Authorization': `Bearer ${accessToken}` } }
  ).then(r => r.json());

  if (payment.status === 'AUTHORIZED' || payment.status === 'CAPTURED') {
    // Pagamento confirmado — exibir página de sucesso
    res.redirect(`/order/${externalPaymentId}/confirmed`);
  } else {
    // Estado inesperado — exibir erro
    res.redirect(`/order/${externalPaymentId}/error`);
  }
});

Etapa 7b — Handler da URL de Falha

O provedor 3DS envia um POST para sua 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) => {
  // Autenticação falhou — redirecionar o usuário de volta à página de pagamento
  res.redirect('/checkout?error=3ds_failed');
});

Nota: Quando a autenticação falha, o pagamento nunca foi autorizado. Não tente capturar nem cancelar (void).


Opção 2: PostMessage em JavaScript

Para single-page apps e experiências de checkout embutidas, você pode abrir o challenge 3DS em um iframe e receber o resultado via API postMessage do navegador — sem necessidade de redirecionamento de página inteira.

Whitelist da URL de PostMessage (Obrigatória)

A URL da página que receberá os eventos postMessage deve ser registrada junto à Inyo. Trata-se de uma medida de segurança para garantir que apenas a página pretendida possa escutar os resultados da autenticação 3DS.

  • Antes de entrar em produção, forneça à Inyo a origem exata (por exemplo, https://checkout.yoursite.com) da página que escuta os eventos postMessage.
  • Qualquer alteração na URL deve ser comunicada à Inyo para que a whitelist seja atualizada.
  • Os resultados do 3DS não serão entregues via postMessage para origens fora da whitelist.

Isso é adicional à whitelist de domínios do tokenizador descrita em Tokenizando Cartões. Tanto a URL da página do tokenizador quanto a URL da página que escuta o PostMessage devem ser registradas.

Configuração do Tokenizador

Defina enablePostMessage: true em threeDSData:

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

Diferença principal: Ao usar enablePostMessage: true, você não precisa fornecer successUrl nem failUrl. O resultado é entregue via postMessage em vez de um redirecionamento.

Fluxo

1. Frontend tokeniza o cartão → recebe o token
2. Backend chama POST /v2/payment com o token
3. A API retorna status: "CHALLENGE" + redirectAcsUrl
4. Frontend abre redirectAcsUrl em um iframe (ou modal)
5. Portador conclui a autenticação do banco dentro do iframe
6. Provedor 3DS envia um postMessage para a janela pai
7. Frontend escuta a mensagem e trata o resultado
8. Backend confirma o status do pagamento via GET /payments/{id}

Etapa 4 — Abrir o Challenge em um Iframe

<!-- Iframe oculto para o challenge 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) {
  // Exibir o contêiner do iframe
  document.getElementById('threeds-container').style.display = 'block';
  // Carregar a página do challenge 3DS
  document.getElementById('threeds-iframe').src = response.data.redirectAcsUrl;
}

Etapa 7 — Escutar o PostMessage

window.addEventListener('message', async (event) => {
  // Validar a origem por segurança
  if (!event.origin.includes('simpleps.com')) return;

  const data = event.data;

  // Ocultar o iframe
  document.getElementById('threeds-container').style.display = 'none';

  if (data.approved === true || data.approved === 'true') {
    // 3DS bem-sucedido — verificar o status do pagamento no seu backend
    const paymentStatus = await verifyPaymentOnBackend(data.externalPaymentId);

    if (paymentStatus === 'AUTHORIZED' || paymentStatus === 'CAPTURED') {
      showSuccessMessage();
    } else {
      showErrorMessage('Payment could not be confirmed.');
    }
  } else {
    // 3DS falhou — permitir que o usuário tente novamente
    showErrorMessage('Card verification failed. Please try again or use a different card.');
  }
});

Payload do PostMessage — Sucesso

{
  "paymentId": "dce568c6-98ec-456c-bb33-4a6809c4fff8",
  "externalPaymentId": "order-12345",
  "amount": 99.99,
  "approved": true
}

Payload do PostMessage — Falha

{
  "paymentId": "dce568c6-98ec-456c-bb33-4a6809c4fff8",
  "externalPaymentId": "order-12345",
  "amount": 99.99,
  "approved": false,
  "status": "Rejected by ACS",
  "responseCode": "99"
}

Exemplo Completo para SPA

// Tratamento completo de 3DS para uma single-page app
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);
    });
  }

  // Retorna uma promise que resolve quando o 3DS é concluído
  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 no seu fluxo 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 no backend e, então, exibir a confirmação
      const verified = await verifyPayment(result.externalPaymentId);
      showConfirmation(verified);
    } else {
      showRetryPrompt();
    }
  } else if (response.data.status === 'AUTHORIZED') {
    showConfirmation(response.data);
  } else {
    showDeclineMessage(response.data.message);
  }
}

Escolhendo Entre Redirecionamento e PostMessage

ConsideraçãoRedirecionamento por URLPostMessage
Experiência do usuárioNavegação de página inteira; o usuário sai do seu checkoutFluida; o challenge aparece em iframe/modal
Complexidade de implementaçãoMais simples — basta definir as URLsMais código — gerenciar iframe + event listener
Requisitos de servidorNecessita handlers POST no servidor para as URLs de sucesso/falhaNenhum handler no servidor necessário para o resultado do 3DS
Compatibilidade com SPARequer soluções alternativas (perda de estado no redirecionamento)Encaixe natural para SPAs
SegurançaResultado entregue servidor a servidor (POST para sua URL)Resultado entregue no client-side (valide a origem!)
Webview móvelFunciona em qualquer lugarAlguns webviews restringem postMessage em iframe

Recomendação:

  • Use Redirecionamento por URL se você tem um checkout tradicional renderizado no servidor ou precisa de compatibilidade máxima.
  • Use PostMessage se está construindo uma SPA ou quer uma experiência de checkout embutida, sem navegação de página.

Independentemente da opção escolhida, sempre verifique o status do pagamento via API (GET /payments/{externalPaymentId}) antes de concluir o pedido.


Testando o 3DS

Use os cartões de teste com 3DS habilitado da página de Dados de Teste:

Número do CartãoBandeiraTipo de 3DS
5413330033003303MastercardChallenge
5169527513596963MastercardChallenge
4983305199046950VisaChallenge
5454545454545454MastercardData-only
4975303994654672VisaData-only
6011926557021045DiscoverData-only

Checklist de Implementação

  • [ ] Registre a URL da página do tokenizador junto à Inyo (obrigatório para CORS)
  • [ ] Escolha seu método de integração: Redirecionamento por URL ou PostMessage
  • [ ] Se Redirecionamento: Configure successUrl e failUrl no tokenizador; implemente handlers POST no servidor
  • [ ] Se PostMessage: Registre junto à Inyo a URL da página que escuta o PostMessage; defina enablePostMessage: true no tokenizador; implemente o listener do evento message com validação de origem
  • [ ] Detecte status: "CHALLENGE" nas respostas de pagamento
  • [ ] Trate o resultado do 3DS (sucesso e falha)
  • [ ] Sempre verifique o status do pagamento via API GET após a conclusão do 3DS
  • [ ] Teste com os cartões de teste 3DS no sandbox
  • [ ] Trate casos extremos: usuário fecha o iframe/aba, timeout, erros de rede
  • [ ] Notifique a Inyo sobre qualquer alteração de URL antes de fazer deploy em novos domínios