Pular para o conteúdo principal

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:

ModalidadeComo aconteceQuando
3DS2 nativo (fingerprint/challenge em iframe)O Adyen Web Component renderiza o desafio dentro do checkout, sem sair da páginaPadrão — o conector envia nativeThreeDS: 'preferred'
Redirect (3DS1 / fallback)O shopper é redirecionado ao emissor e volta pelo returnUrl do conectorQuando o emissor/Adyen não suporta o fluxo nativo
Data OnlySem 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

#RotaMétodoQuem chamaPapel no 3DS
1/payments (rota padrão PPP — authorize)POSTGateway VTEXCria o pagamento na Adyen; se a resposta for 3DS, devolve paymentAppData
2/_v/api/payment-detailsPOSTPayment App (evento onAdditionalDetails do Adyen Web) ou app headlessConclui o 3DS nativo: envia threeDSResult ao /payments/details da Adyen e aprova/nega no gateway
3/_v/api/payment/3ds/redirectGETNavegador do shopper (retorno do emissor)Conclui o 3DS por redirect: envia redirectResult ao /payments/details e aprova/nega no gateway
4/_v/api/payment-statusPOSTPayment App (polling 5s) ou app headlessConsulta o status consolidado (VBase av3settle)
5/_v3/api/webhook/notificationPOSTAdyen (webhooks)Notificações assíncronas; correlaciona pelo pspReference (VBase av3notify)
6/_v/api/payment-authorizationPOSTPayment 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' e additionalData.threeDS2DataOnly: true — não há challenge.
  • Os dados de cartão trafegam pelo secure proxy da VTEX (SecureExternalClient + header Idempotency-Key = paymentId).
  • browserInfo + origin são pré-requisito do 3DS2 nativo: sem eles a Adyen não devolve action nativa.

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 (chave paymentId). Se o gateway fizer retry do authorize, o conector reusa a mesma action em 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 VBase av3settle, seta networkTxReference na transação e chama o callback do gateway (POST https://{account}.vtexpayments.com.br/api/pvt/payment-provider/transactions/{tx}/payments/{py}/callback) com status: "approved" e tid/nsu/authorizationId = pspReference;
  • Refused / Error / Cancelled → callback com status: "denied" + refusalReasonCode;
  • Response HTTP ao chamador: string com o resultCode (ex.: "Authorised"). O Payment App usa isso para disparar transactionValidation.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:

  1. Chama POST /payments/details com { details: { redirectResult } };
  2. Se a transação já estiver Cancelled na VTEX → cancelOrRefund na Adyen;
  3. Authorised → callback approved ao gateway; recusas → callback denied;
  4. 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

BucketChaveConteúdoPara quê
rprpaymentIdThreeDSResponse da AdyenIdempotência do retry de authorize
av3notifypspReference{ paymentId, transactionId, orderId, account }Correlação dos webhooks Adyen
av3settlepaymentId / 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 browserInfo data, 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 APIPOST /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 APIPOST /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 nativoEquivalente headless
Script adyenv3 envia browserInfodeviceInfo na query string do POST .../transactions/{id}/payments
Checkout renderiza o Payment AppgatewayCallback (428) → parse do appPayload + createFromAction(action)
onAdditionalDetails → payment-detailsIgual: POST /_v/api/payment-details com {...state.data, ...authorization}
Evento transactionValidation.vtexPolling 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 envia dataOnly: true e nunca haverá action — não é possível testar challenge nessa configuração.
  • Retry idempotente: o VBase rpr garante que retries de authorize reutilizem a action original; limpar esse registro é necessário para re-testar um mesmo paymentId.
  • Webhooks: além do fluxo síncrono, a Adyen envia notificações em /_v3/api/webhook/notification; a correlação usa o VBase av3notify gravado durante o authorize/payment-details.
  • Multi-account: payment-details proxya a chamada quando merchantName difere 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?