Autenticação 3D Secure (3DS)
Como o SDK LegacyPay autentica o portador do cartão automaticamente durante a tokenização, sem que você precise lidar com SDKs externos.
Pré-requisito: Este guia assume que você já carregou o
legacy-pay.jse inicializou oclient. Se ainda não fez isso, veja Tokenização de Cartões.
O 3D Secure (3DS) adiciona uma camada de autenticação do portador do cartão — geralmente um SMS, biometria ou aprovação no app do banco — antes da transação ser processada. Ele reduz chargebacks e aumenta a taxa de aprovação.
No LegacyPay, a autenticação 3DS roda automaticamente dentro do client.prepareCardPayment(). Você só carrega o legacy-pay.js e não precisa importar manualmente nenhum SDK externo: quando o seu perfil exige um SDK 3DS próprio, o próprio legacy-pay.js carrega esse SDK sozinho a partir do sdkUrl informado em getConfig(). Você não vê desafios no seu código, não monta payloads diferentes para bancos diferentes. Quando o prepareCardPayment() retorna com sucesso, o prepared.payinCard já contém os dados de autenticação prontos para o /payin.
CSP: como olegacy-pay.jsinjeta o SDK do provider no browser, sua Content-Security-Policy precisa permitir o host dosdkUrlretornado emgetConfig().capabilities.threeDS— tanto emscript-srcquanto emconnect-src. Leia esse valor em runtime e libere o host correspondente. Sem isso, o script é bloqueado e o 3DS falha.
Sucesso do 3DS ≠ venda aprovadaQuando
prepareCardPayment()resolve com sucesso (ouprepared.threeDS.status === "authenticated"), isso significa apenas que o portador foi autenticado pelo banco — não que a venda foi aprovada. A aprovação depende da adquirente/antifraude e acontece depois, de forma assíncrona. Nunca exiba "pagamento aprovado" com base no retorno do SDK.Confirme o status real da venda por um destes caminhos:
- Webhook (recomendado): aguarde o evento com
status: "APPROVED"— veja Webhooks.- Polling: consulte
GET /payin/{id}até o status sair dePENDING.
Verificar se 3DS está ativo
Cada loja é configurada com seu próprio perfil de risco. Consulte:
var config = await client.getConfig();
// config.capabilities.threeDS = {
// enabled: true // ou false
// }Quando enabled: false, o prepareCardPayment() não dispara desafios — o token é gerado direto.
Como funciona pra quem integra
var client = LegacyPay.init({ publicKey: "pk_live_...", apiBaseUrl: "..." });
try {
var prepared = await client.prepareCardPayment({
amount: 9900,
referenceId: "pedido-123",
installments: 1,
card: { holderName, number, expirationMonth, expirationYear, cvv },
customer: { name, email, document, phone, address }
});
// prepared = {
// payinCard: {
// token: "cvt_live_...", // PAN nunca sai do SDK
// installments: 1,
// // fluxo pre-order por sessão: o id da sessão (operation_session_id). Outros fluxos
// // podem trazer { cavv, eci }. Repasse o que vier — não dependa de cavv/eci no browser.
// threeDSecure: { referenceId: "..." }
// },
// antifraud: { skipped: false, sessionId: "mfp-..." },
// card: { token: "cvt_live_...", brand: "visa", last4: "1111" },
// threeDS: { skipped: false, status: "authenticated" }
// }
// buildPayinPayload() mescla os campos base com prepared.payinCard e antifraud.
var payload = client.buildPayinPayload({
amount: 9900,
referenceId: "pedido-123",
payerIp: ipDoComprador,
isPhysicalProduct: false,
customer: { name, email, document, phone, address },
items: [{ title: "Produto", quantity: 1, unitPrice: 9900 }]
}, prepared);
// payload = {
// paymentMethod: "CREDIT_CARD",
// amount: 9900,
// referenceId: "pedido-123",
// payerIp: "...",
// isPhysicalProduct: false,
// customer: { ... },
// items: [ ... ],
// card: {
// token: "cvt_live_...",
// installments: 1,
// threeDSecure: { referenceId: "..." } // o backend resolve cavv/eci a partir do referenceId
// },
// antifraud: { sessionId: "mfp-..." } // omitido se antifraude não rodou
// }
// Envie payload para o SEU backend — substitua a URL pelo seu endpoint real.
// Nunca chame /payin direto do browser (exige sk_live).
await fetch("/api/pagamento", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
} catch (err) {
if (err.code === "THREEDS_FAILED") {
// Mostre uma mensagem amigável e peça outro cartão.
}
}Durante o prepareCardPayment(), o SDK pode abrir um modal/iframe com o desafio do banco. Ele aparece sobre a sua página e fecha sozinho quando o comprador confirma. Você não precisa fazer nada — só esperar a Promise resolver.
Nunca faça fallback "tokenizar sem 3DS".Se o
prepareCardPayment()lançar erro (THREEDS_FAILED, etc.), não tente tokenizar o cartão por
conta própria e mandar pro/payinsem o 3DS. Em lojas comthreeDS.enabled, a adquirente recusa
a transação sem o dado de 3DS (você verá422 REJECTED_BY_RISK— ou, em adquirentes que exigem 3DS,
422 INVALID_DATAindicando que ooperation_session_idestá faltando). O caminho certo no erro é
pedir outro cartão.
O que vem em threeDSecure depende do provider
threeDSecure depende do providerO prepareCardPayment() preenche prepared.payinCard.threeDSecure, mas o formato muda conforme o
seu perfil de processamento — e o buildPayinPayload() repassa isso pro /payin automaticamente. Você não
precisa montar nada à mão; só não assuma cavv/eci no browser:
| Fluxo | payinCard.threeDSecure traz | Quem resolve cavv/eci |
|---|---|---|
| Pre-order por sessão | { referenceId } — o id da sessão 3DS | O backend resolve cavv/eci a partir do referenceId. Não vêm no browser. |
| Autenticação no browser | pode trazer { cavv, eci, ... } | O próprio SDK, no browser |
No fluxo pre-order por sessão, a única coisa que precisa chegar ao
/payiné o
card.threeDSecure.referenceId. Sem ele, a transação é recusada.cavv/ecipodem não ser
produzidos no browser — quem os obtém é o nosso backend.
Alternativa: 3DS na nossa origem mantendo o seu formulário (prepareCardPaymentHosted)
prepareCardPaymentHosted)Se você já tem o seu formulário de cartão e quer evitar mexer no CSP (liberar hosts de adquirente),
use prepareCardPaymentHosted — é igual ao prepareCardPayment (mesma entrada e mesma saída), mas a
tokenização + 3DS rodam num iframe oculto hospedado por nós. Você continua coletando o cartão nos
seus campos; o iframe só aparece (tela cheia) se houver desafio 3DS.
var client = LegacyPay.init({ publicKey: "pk_live_...", apiBaseUrl: "..." });
// Você coleta o cartão no SEU formulário e passa tudo numa única chamada:
var prepared = await client.prepareCardPaymentHosted({
amount: 9900,
installments: 1,
referenceId: "pedido-123",
card: { holderName, number, expirationMonth, expirationYear, cvv },
customer: { name, email, document, phone, address }
});
// Daqui pra frente é idêntico ao fluxo normal:
var payload = client.buildPayinPayload({ amount: 9900, referenceId: "pedido-123", payerIp, isPhysicalProduct: false, customer, items }, prepared);
// Envie payload pro SEU backend → ele chama /payin.- CSP da sua página: basta
frame-src https://checkout.legacyecom.com.br. Nenhum host de adquirente. - O
prepareCardPaymentself-hosted continua existindo e funcionando —prepareCardPaymentHostedé só
uma alternativa pra quem precisa rodar o 3DS fora do CSP da própria página.
Card Element (iframe) — campos no nosso iframe (PCI reduzido)
Se você quer também tirar os campos de cartão do seu DOM (menor escopo PCI), use o
Card Element: um iframe hospedado por nós que renderiza os campos do cartão e roda tokenização +
3DS na nossa origem. Vantagens:
- Sem CSP de adquirente. O 3DS conecta no provedor a partir do nosso iframe — sua página só
precisa deframe-srcpro nosso domínio de checkout. Nada de liberar hosts de terceiros. - Sem carregar SDK de provider e sem manter versão de SDK no seu front.
- Menor escopo PCI — o número do cartão é digitado no nosso iframe, não no seu DOM.
<script src="https://api.legacyecom.com.br/checkout/sdk/legacy-pay.js"></script>
<div id="card-container"></div>
<button id="pay">Pagar</button>
<script>
var client = LegacyPay.init({ publicKey: "pk_live_xxx", apiBaseUrl: "https://api.legacyecom.com.br" });
var element = client.mountCardElement("#card-container", {
amount: 9900, // em centavos
installments: 1,
referenceId: "pedido-123",
maxInstallments: 12,
customer: { name, email, document, phone, address },
onResult: function (r) {
// r = { payinCard, antifraud }. Envie pro SEU backend → ele chama /payin com sk_live.
fetch("/api/pagamento", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ card: r.payinCard, antifraud: r.antifraud })
});
},
onError: function (err) {
// err.code / err.details. Em falha de 3DS, mostre erro e peça outro cartão.
}
});
// Dispare a tokenização + 3DS pelo SEU botão:
document.getElementById("pay").addEventListener("click", function () { element.submit(); });
</script>CSP da sua página: basta frame-src https://checkout.legacyecom.com.br (seu domínio de checkout).
O fluxo do /payin continua igual: o payinCard vai pro seu backend, que chama o /payin.
Prefira o Card Element ao fluxo manual abaixo sempre que possível — ele elimina os problemas de CSP,
de SDK e de 3DS de uma vez.
Exemplo React/Next.js
Componente de checkout (client component). Carrega o legacy-pay.js, roda prepareCardPayment()
(3DS + tokenização + antifraude) e envia o payload pronto pro seu backend — que chama o /payin
com sk_live. Em erro de 3DS, pede outro cartão (sem fallback).
"use client";
import { useEffect, useRef, useState } from "react";
const API_BASE = "https://api.legacyecom.com.br";
const PUBLIC_KEY = "pk_live_xxx";
// Carrega o legacy-pay.js uma vez. O legacy-pay.js injeta o SDK 3DS do provider quando
// necessário — garanta o CSP (script-src/connect-src) para o host do sdkUrl de getConfig().
function loadLegacyPay(): Promise<any> {
return new Promise((resolve, reject) => {
if ((window as any).LegacyPay) return resolve((window as any).LegacyPay);
const s = document.createElement("script");
s.src = `${API_BASE}/checkout/sdk/legacy-pay.js`;
s.onload = () => resolve((window as any).LegacyPay);
s.onerror = () => reject(new Error("Falha ao carregar legacy-pay.js"));
document.head.appendChild(s);
});
}
export function CheckoutCard({ amount, referenceId, customer }: {
amount: number; referenceId: string; customer: any;
}) {
const clientRef = useRef<any>(null);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
let active = true;
loadLegacyPay()
.then((LegacyPay) => { if (active) clientRef.current = LegacyPay.init({ publicKey: PUBLIC_KEY, apiBaseUrl: API_BASE }); })
.catch((e) => active && setError(e.message));
return () => { active = false; };
}, []);
async function pay(card: { holderName: string; number: string; expiry: string; cvv: string }, installments = 1) {
setError(null);
setLoading(true);
try {
const client = clientRef.current;
if (!client) throw new Error("Checkout não inicializado. Recarregue a página.");
// prepareCardPayment roda antifraude + tokenização + 3DS pré-order.
// Quando há SDK 3DS de provider, isto abre o desafio e devolve payinCard.threeDSecure (ex.: referenceId).
const prepared = await client.prepareCardPayment({
amount, // em centavos
installments,
referenceId,
card: {
holderName: card.holderName,
number: card.number.replace(/\D/g, ""),
expiry: card.expiry, // "MM/AA" (ou expirationMonth/expirationYear)
cvv: card.cvv,
installments,
},
customer,
});
// buildPayinPayload mescla os campos base com prepared.payinCard (token + threeDSecure) e antifraud.
const payload = client.buildPayinPayload(
{ amount, referenceId, payerIp: "", isPhysicalProduct: false, customer, items: [] },
prepared,
);
// Envie pro SEU backend — ele chama /payin com sk_live. NUNCA chame /payin do browser.
const res = await fetch("/api/pagamento", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
}).then((r) => r.json());
// Sucesso do SDK ≠ venda aprovada — confirme via webhook ou GET /payin/{id}.
return res;
} catch (err: any) {
// 3DS falhou/expirou/cancelado, ou cartão sem suporte a 3DS.
// NÃO faça fallback sem 3DS — peça outro cartão.
if (err?.code === "THREEDS_FAILED") setError("Não foi possível autenticar o cartão. Tente outro cartão.");
else setError(err?.message || "Falha no pagamento. Tente novamente.");
console.error("[checkout] prepareCardPayment", err?.code, err?.message);
return null;
} finally {
setLoading(false);
}
}
// ...render do formulário, chamando pay(...) no submit; exibir `error` e `loading`.
}Pontos que evitam os erros mais comuns:
- Não importe nenhum SDK de provider você mesmo — o
legacy-pay.jsinjeta. Só ajuste o CSP. - Sempre logue
err.code/err.messagedoprepareCardPayment()— umcatchsilencioso esconde o
THREEDS_FAILEDe faz parecer que "voltoupayinCardnulo sem erro". - Não tokenize por fora quando o 3DS falha. Em erro, peça outro cartão.
Falhas mais comuns
| Causa | Solução |
|---|---|
| Comprador não consegue confirmar o desafio (timeout, código errado, app fechado). | Mostre erro amigável e ofereça outro cartão. |
| Cartão não suportado pelo emissor para 3DS. | Idem — peça outro cartão. |
| Navegador sem suporte (popups bloqueados, modo anônimo restritivo). | Oriente o comprador a permitir popups ou trocar de navegador. |
Todas essas situações chegam como LegacyPayError com err.code === "THREEDS_FAILED". Você não precisa diferenciar a sub-causa — a UX em todos os casos é a mesma: pedir outro cartão.
Quando o desafio acontece "depois"
Em alguns perfis de loja, o desafio 3DS é decidido após o /payin ser criado (a análise antifraude pode pedir o desafio só pra casos suspeitos). Nessa situação, a resposta de /payin virá com "threeDSecurePending": true, "status": "PENDING_3DS" e os campos planos threeDSecureSession, threeDSecureTransactionId e threeDSecureSdkUrl com os dados do desafio.
O SDK lida com isso automaticamente quando você usa client.processCardPayment() — ele detecta o threeDSecurePending, roda o desafio via handlePendingThreeDS() e retoma o fluxo. Se você está montando o /payin manualmente, use client.handlePendingThreeDS(payinResponse, { card, customer, amount, installments }) para completar o desafio.
Resumindo
- Não importe manualmente o SDK do provider — só carregue o
legacy-pay.js; ele injeta o SDK 3DS do provider sozinho. Garanta o CSP para osdkUrl. - Não monte payloads de 3DS — o SDK monta tudo internamente.
- Use
client.prepareCardPayment()para tokenizar o cartão e executar o 3DS automaticamente. - Não assuma
cavv/ecino browser — em fluxos pre-order vemthreeDSecure.referenceId(o backend resolvecavv/eci). Só repasse opayinCard. - Nunca tokenize sem 3DS como fallback quando
threeDS.enabled— a adquirente recusa (REJECTED_BY_RISK/INVALID_DATA). - Trate
LegacyPayError.code === "THREEDS_FAILED"com uma mensagem amigável (inclui timeout do SDK do provider) e logueerr.code/err.message. - Use
client.processCardPayment()se você quer um único método que cuida do/payin+ desafio pós-order. - Confirme a venda via webhook ou
GET /payin/{id}— o sucesso do SDK indica só autenticação 3DS, nunca aprovação da venda.
Updated about 1 month ago