Fluxo de Pagamento 3DS (Cartão)
Este documento explica como funciona a autenticação 3D Secure (3DS/3DS2) para pagamentos com cartão de crédito/débito usando o conector adyen.payment-provider-v3 na VTEX: o fluxo completo, as APIs envolvidas e os payloads que sua aplicação precisa enviar e tratar. Também cobre os passos extras necessários em uma loja headless.
Contexto
O 3DS é o protocolo de autenticação do portador do cartão (challenge do banco emissor). No conector, ele funciona em três modalidades:
| Modalidade | Como acontece | Quando |
|---|---|---|
| 3DS2 nativo (fingerprint/challenge em iframe) | O Adyen Web Component renderiza o desafio dentro do checkout, sem sair da página | Padrão — o conector envia nativeThreeDS: 'preferred' |
| Redirect (3DS1 / fallback) | O shopper é redirecionado ao emissor e volta pelo returnUrl do conector | Quando o emissor/Adyen não suporta o fluxo nativo |
| Data Only | Sem challenge — apenas dados enviados à bandeira (frictionless) | Quando a AppSetting serviceAuthentication = 'Data Only', cartão de crédito e moeda BRL |
Diagramas de Fluxo
Cartão sem 3DS (fluxo simples)
Cartão com 3DS2 nativo (challenge no checkout)
Cartão com 3DS via redirect (fallback)
APIs do Conector no Fluxo 3DS
| # | Rota | Método | Quem chama | Papel no 3DS |
|---|---|---|---|---|
| 1 | /payments (rota padrão PPP — authorize) | POST | Gateway VTEX | Cria o pagamento na Adyen; se a resposta for 3DS, devolve paymentAppData |
| 2 | /_v/api/payment-details | POST | Payment App (evento onAdditionalDetails do Adyen Web) ou app headless | Conclui o 3DS nativo: envia threeDSResult ao /payments/details da Adyen e aprova/nega no gateway |
| 3 | /_v/api/payment/3ds/redirect | GET | Navegador do shopper (retorno do emissor) | Conclui o 3DS por redirect: envia redirectResult ao /payments/details e aprova/nega no gateway |
| 4 | /_v/api/payment-status | POST | Payment App (polling 5s) ou app headless | Consulta o status consolidado (VBase av3settle) |
| 5 | /_v3/api/webhook/notification | POST | Adyen (webhooks) | Notificações assíncronas; correlaciona pelo pspReference (VBase av3notify) |
| 6 | /_v/api/payment-authorization | POST | Payment App (fluxos wallet/componente — Google Pay, Blik etc.) | Não participa do fluxo de cartão padrão; citado aqui para evitar confusão com o /payments do PPP |
Dados Enviados e Recebidos por API
POST /payments (PPP authorize) → Adyen POST /checkout/v72/payments
O gateway envia o AuthorizationRequest do protocolo VTEX (cartão tokenizado PCI: numberToken, cscToken, holderToken, expiration, além de miniCart, merchantSettings, transactionId, paymentId, returnUrl...).
⚠️ Ponto crítico do fluxo 3DS: a Adyen só devolve a action de 3DS2 nativo se o conector conseguir montar o browserInfo do shopper (device/navegador). Sem esse dado, o pagamento é processado frictionless (sem challenge), silenciosamente. O CardService monta esse browserInfo a partir do campo deviceInfo gravado na transação — e para isso, a app precisa enviar deviceInfo na própria chamada de pagamento:
POST https://{account}.vtexpayments.com.br/api/pub/transactions/{transactionId}/payments
?orderId={orderGroup}&redirect=false&deviceInfo={base64}
onde {base64} é o base64 de:
sw={screen.width}&sh={screen.height}&cd={screen.colorDepth}&tz={new Date().getTimezoneOffset()}&lang={navigator.language}&java={navigator.javaEnabled()}
orderGroup vem da resposta da criação do pedido. Esse é o mesmo mecanismo usado pelo checkout nativo da VTEX — ver Integração Headless abaixo para o passo a passo completo em uma integração headless.
Corpo enviado à Adyen (campos relevantes para 3DS):
{
"merchantAccount": "MinhaLoja_BR",
"reference": "1268540098765",
"amount": { "currency": "BRL", "value": 10000 },
"paymentMethod": {
"type": "scheme",
"number": "<token PCI>",
"expiryMonth": "03",
"expiryYear": "2030",
"cvc": "<token PCI>",
"holderName": "JOAO S SILVA",
"fundingSource": "credit"
},
"returnUrl": "https://{workspace}--{account}.myvtex.com/_v/api/payment/3ds/redirect?account={account}&paymentId={paymentId}&transactionId={transactionId}",
"authenticationData": {
"threeDSRequestData": { "dataOnly": false, "nativeThreeDS": "preferred" }
},
"channel": "web",
"origin": "https://www.minhaloja.com",
"browserInfo": { "...": "coletado no checkout" },
"shopperInteraction": "Ecommerce",
"shopperEmail": "joao@email.com",
"shopperIP": "200.1.2.3",
"shopperReference": "<vtexUserId>",
"shopperConversionId": "<paymentId>",
"installments": { "value": 1 },
"billingAddress": { "...": "..." },
"deliveryAddress": { "...": "..." }
}
- No modo Data Only:
dataOnly: true,nativeThreeDS: 'disabled'eadditionalData.threeDS2DataOnly: true— não há challenge. - Os dados de cartão trafegam pelo secure proxy da VTEX (
SecureExternalClient+ headerIdempotency-Key = paymentId). browserInfo+originsão pré-requisito do 3DS2 nativo: sem eles a Adyen não devolveactionnativa.
Resposta da Adyen quando o 3DS é exigido (ThreeDSResponse):
{
"additionalData": {
"threeds2.threeDS2Token": "***",
"threeds2.threeDSServerTransID": "***",
"threeds2.threeDSMethodURL": "***",
"threeds2.cardEnrolled": "true",
"cardBin": "411111"
},
"pspReference": "ZXC7Q4JV5SRPDKV5",
"resultCode": "IdentifyShopper",
"action": {
"type": "threeDS2",
"subtype": "fingerprint",
"paymentMethodType": "scheme",
"paymentData": "***",
"token": "***"
}
}
resultCode possíveis no ramo 3DS: RedirectShopper, ChallengeShopper, IdentifyShopper (para redirect, action.type: "redirect" com url, method e data { MD, PaReq }).
Resposta do conector ao gateway (Authorizations.redirect) — é isso que dispara a renderização do Payment App:
{
"paymentId": "...",
"status": "undefined",
"delayToCancel": 21600,
"connectorMetadata": [
{ "name": "Currency", "value": "BRL" },
{ "name": "OriginalReference", "value": "<pspReference>" }
],
"paymentAppData": {
"appName": "adyen.payment-provider-v3",
"payload": "{\"authorization\":{...request original sem apiKey...},\"action\":{...},\"clientKey\":\"test_...\",\"environment\":\"test|live\",\"merchantName\":\"...\",\"shopperEmail\":\"...\",\"shopperReference\":\"...\",\"countryCode\":\"BR\",\"lineItems\":[...],\"denyUrl\":\"/_v/api/cancel-payment?paymentId=...\"}"
}
}
Idempotência: a resposta 3DS é salva no VBase
rpr(chavepaymentId). Se o gateway fizer retry doauthorize, o conector reusa a mesmaactionem vez de criar um novo pagamento na Adyen.
POST /_v/api/payment-details
Chamado pelo Payment App no onAdditionalDetails do Adyen Web, após o shopper completar o fingerprint/challenge. O corpo é a fusão do state.data do componente com o objeto authorization do payload:
{
"details": { "threeDSResult": "eyJ0cmFuc1N0YXR1cyI6IlkifQ==" },
"paymentData": "...",
"transactionId": "...",
"paymentId": "...",
"orderId": "...",
"merchantName": "minhaloja",
"card": { "bin": "411111" },
"miniCart": { "buyer": { "id": "..." } }
}
O conector repassa apenas body.details para o POST /payments/details da Adyen, e com a resposta:
Authorised→ grava VBaseav3settle, setanetworkTxReferencena transação e chama o callback do gateway (POST https://{account}.vtexpayments.com.br/api/pvt/payment-provider/transactions/{tx}/payments/{py}/callback) comstatus: "approved"etid/nsu/authorizationId = pspReference;Refused/Error/Cancelled→ callback comstatus: "denied"+refusalReasonCode;- Response HTTP ao chamador: string com o
resultCode(ex.:"Authorised"). O Payment App usa isso para disparartransactionValidation.vtex. - Se
merchantName≠ account (multi-loja), a chamada é proxyada para a account dona da configuração.
GET /_v/api/payment/3ds/redirect
Query string: account, paymentId, transactionId (fixados no returnUrl) + redirectResult (anexado pela Adyen no retorno do emissor).
O middleware:
- Chama
POST /payments/detailscom{ details: { redirectResult } }; - Se a transação já estiver
Cancelledna VTEX →cancelOrRefundna Adyen; Authorised→ callbackapprovedao gateway; recusas → callbackdenied;- Sempre finaliza com 302 para a página de retorno do gateway:
https://{account}.vtexpayments.com.br/payment-provider/transactions/{transactionId}/payments/{paymentId}/return?accountName={account}.
POST /_v/api/payment-status
Request: { "paymentId": "..." } → Response: { "status": "pending" } enquanto não há conclusão, ou { "status": true|false } quando o pagamento foi concluído (lido do VBase av3settle). O Payment App faz polling a cada 5s como rede de segurança do challenge.
Persistência (VBase) Usada pelo 3DS
| Bucket | Chave | Conteúdo | Para quê |
|---|---|---|---|
rpr | paymentId | ThreeDSResponse da Adyen | Idempotência do retry de authorize |
av3notify | pspReference | { paymentId, transactionId, orderId, account } | Correlação dos webhooks Adyen |
av3settle | paymentId / orderId | { pspReference, success, step } | Curto-circuito de authorize repetido + payment-status |
Integração Headless
Contexto do RFC: "Merchants that use a headless checkout (Whirlpool/Beko) may be affected, since the API will start depending on
browserInfodata, which is currently not being sent."
Em um front headless os scripts do checkout VTEX não rodam, logo browserInfo/origin nunca chegam ao conector e o Payment App não é renderizado automaticamente. A app headless precisa assumir esses dois papéis.
📚 Referência geral de integração headless: Headless Cart and Checkout — Complete order.
Passo a Passo
1. Criar o pedido e o pagamento, enviando deviceInfo (Checkout API / Transaction API — fluxo do-payment: orderForm → order → transactions → payments). O gateway chama o authorize do conector sozinho; nada muda nesse fluxo, com uma exceção crítica:
⚠️ Sem deviceInfo, a Adyen não devolve o challenge nativo — o pagamento é processado frictionless, silenciosamente. Envie deviceInfo como query string na chamada de pagamento (formato completo em Dados Enviados e Recebidos por API):
POST https://{account}.vtexpayments.com.br/api/pub/transactions/{transactionId}/payments
?orderId={orderGroup}&redirect=false&deviceInfo={base64}
📚 Referência: Payments Gateway API — POST /api/payments/transactions/{transactionId}/payments.
2. Chamar o gatewayCallback e detectar o 3DS pela resposta. Depois de submeter o pagamento, chame:
POST https://{account}.{environment}.com.br/api/checkout/pub/gatewayCallback/{orderGroup}
📚 Referência: Checkout API — POST /api/checkout/pub/gatewayCallback/{orderGroup}.
⚠️ 428 Precondition Required é a resposta esperada quando há um 3DS pendente — não é um erro. Trate-o como um resultado válido, não como falha da chamada. O corpo carrega paymentAuthorizationAppCollection, um item por app de pagamento pendente:
{
"paymentAuthorizationAppCollection": [
{
"appName": "adyen.payment-provider-v3",
"appPayload": "{\"authorization\":{...},\"action\":{...},\"clientKey\":\"...\",\"environment\":\"test\"}"
}
]
}
Se a resposta for 204 No Content, o pagamento já foi resolvido (frictionless) — não há challenge a renderizar, siga direto para o passo 4 (polling).
A app headless deve localizar o item com appName: "adyen.payment-provider-v3" e fazer o parse do appPayload (é uma string JSON) para extrair action, clientKey, environment e authorization.
3. Renderizar o challenge com o Adyen Web (papel que o Payment App faz no checkout nativo):
import { AdyenCheckout } from '@adyen/adyen-web'
import '@adyen/adyen-web/styles/adyen.css'
// appPayload veio do gatewayCallback (428) no passo anterior
const payload = JSON.parse(appPayload)
const checkout = await AdyenCheckout({
clientKey: payload.clientKey,
environment: payload.environment, // 'test' | 'live'
countryCode: 'BR',
onAdditionalDetails: async (state, component, actions) => {
// Concluir o 3DS no conector
try {
const { data: resultCode } = await axios.post(
'https://{account}.myvtex.com/api/io/_v/api/payment-details',
{ ...state.data, ...payload.authorization }
)
// resultCode: 'Authorised' | 'Refused' | 'Error' | 'Cancelled' ...
// ⚠️ OBRIGATÓRIO: o "advanced flow" do Adyen Web exige resolver/rejeitar
// este callback via `actions`. Sem isso, o componente nunca finaliza o
// próprio estado interno — na prática o challenge "fecha" (o ACS/emissor
// encerra o próprio iframe) mas nada mais acontece, porque o SDK nunca
// foi avisado de que o fluxo terminou.
actions.resolve({ resultCode })
} catch (err) {
actions.reject()
}
},
})
checkout
.createFromAction(payload.action, { challengeWindowSize: '02' })
.mount('#adyen-container')
4. Concluir e confirmar. O próprio conector aprova/nega o pagamento no gateway via callback — a app headless não precisa chamar o gateway. Para confirmar, use o polling:
POST https://{account}.myvtex.com/api/io/_v/api/payment-status
{ "paymentId": "<paymentId>" }
→ { "status": "pending" } … → { "status": true }
ou consulte o status da transação/pedido pelas APIs da VTEX.
5. Caso action.type === "redirect". O shopper sai para o emissor e volta no returnUrl do conector (/_v/api/payment/3ds/redirect), que finaliza o pagamento e redireciona para a página de retorno do gateway em {account}.vtexpayments.com.br. Como esse returnUrl é fixo (montado no backend), a app headless não controla o pouso final — trate esse cenário abrindo o redirect em popup/iframe e monitorando o payment-status, ou priorize o fluxo nativo (que é o preferido pelo conector via nativeThreeDS: 'preferred' e só existe se o passo 1 for feito corretamente).
Resumo do Contrato Headless
| Responsabilidade do checkout nativo | Equivalente headless |
|---|---|
Script adyenv3 envia browserInfo | deviceInfo na query string do POST .../transactions/{id}/payments |
| Checkout renderiza o Payment App | gatewayCallback (428) → parse do appPayload + createFromAction(action) |
onAdditionalDetails → payment-details | Igual: POST /_v/api/payment-details com {...state.data, ...authorization} |
Evento transactionValidation.vtex | Polling POST /_v/api/payment-status |
Testes
Para validar a implementação do fluxo de 3D Secure (3DS) antes de colocá-la em produção, é possível utilizar um merchantAccount de teste da Adyen em conjunto com os cartões de teste disponibilizados pela Adyen.
A Adyen fornece uma lista de cartões de teste e cenários de autenticação que permitem validar diferentes comportamentos do fluxo de 3DS, incluindo:
- Fluxo Challenge;
- Fluxo Frictionless;
- Cenários de autenticação com sucesso e falha;
- Casos avançados de teste, como timeouts, erros e diferentes valores de
transStatus.
Para consultar a lista completa de cartões de teste, credenciais e cenários disponíveis, acesse a documentação oficial da Adyen:
https://docs.adyen.com/development-resources/testing/3d-secure-2-authentication
Essa documentação contém todos os cartões de teste, os comportamentos esperados para cada cenário e as informações necessárias para validar corretamente a implementação do 3DS no ambiente de testes da Adyen.
Observações e Limitações
- Data Only desliga o challenge: com
serviceAuthentication = 'Data Only'(AppSettings), crédito e BRL, o conector enviadataOnly: truee nunca haveráaction— não é possível testar challenge nessa configuração. - Retry idempotente: o VBase
rprgarante que retries deauthorizereutilizem aactionoriginal; limpar esse registro é necessário para re-testar um mesmopaymentId. - Webhooks: além do fluxo síncrono, a Adyen envia notificações em
/_v3/api/webhook/notification; a correlação usa o VBaseav3notifygravado durante oauthorize/payment-details. - Multi-account:
payment-detailsproxya a chamada quandomerchantNamedifere da account que recebeu a requisição. - Wallets: Google Pay/Apple Pay com DPAN enviam
mpiData(cryptogram/eci) e não passam pelo challenge; o fluxo 3DS deste documento aplica-se a cartão (FPAN) — ver o RFC de Google Pay para os cenários FPAN × DPAN.
Esta página foi útil?