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
| Modo | Interação do Portador | Descrição |
|---|---|---|
| Data-only | Nenhuma | Os dados da transação são enviados ao emissor para avaliação de risco; nenhuma ação do portador é necessária |
| Challenge | Obrigatória | O 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:
- Automática (configuração no backend) — Todos os pagamentos exigem 3DS. Configurada pela equipe Inyo durante o onboarding.
- Manual (por transação) — Você controla quando solicitar o 3DS definindo
threeDSDatadurante 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ção | Como funciona | Melhor para |
|---|---|---|
| Redirecionamento por URL | O navegador redireciona para redirectAcsUrl; após a autenticação, o provedor 3DS redireciona de volta para sua successUrl ou failUrl | Fluxos de checkout de página inteira, aplicações renderizadas no servidor |
| PostMessage | Abra a redirectAcsUrl em um iframe; o provedor 3DS envia um evento postMessage para a janela pai com o resultado | Single-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 eventospostMessage. - 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
postMessagepara 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 fornecersuccessUrlnemfailUrl. O resultado é entregue viapostMessageem 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ção | Redirecionamento por URL | PostMessage |
|---|---|---|
| Experiência do usuário | Navegação de página inteira; o usuário sai do seu checkout | Fluida; o challenge aparece em iframe/modal |
| Complexidade de implementação | Mais simples — basta definir as URLs | Mais código — gerenciar iframe + event listener |
| Requisitos de servidor | Necessita handlers POST no servidor para as URLs de sucesso/falha | Nenhum handler no servidor necessário para o resultado do 3DS |
| Compatibilidade com SPA | Requer soluções alternativas (perda de estado no redirecionamento) | Encaixe natural para SPAs |
| Segurança | Resultado entregue servidor a servidor (POST para sua URL) | Resultado entregue no client-side (valide a origem!) |
| Webview móvel | Funciona em qualquer lugar | Alguns 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ão | Bandeira | Tipo de 3DS |
|---|---|---|
5413330033003303 | Mastercard | Challenge |
5169527513596963 | Mastercard | Challenge |
4983305199046950 | Visa | Challenge |
5454545454545454 | Mastercard | Data-only |
4975303994654672 | Visa | Data-only |
6011926557021045 | Discover | Data-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
successUrlefailUrlno tokenizador; implemente handlers POST no servidor - [ ] Se PostMessage: Registre junto à Inyo a URL da página que escuta o PostMessage; defina
enablePostMessage: trueno tokenizador; implemente o listener do eventomessagecom 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
