Saltar al contenido principal

Flujo de Pago 3DS (Tarjeta)

Este documento explica cómo funciona la autenticación 3D Secure (3DS/3DS2) para pagos con tarjeta de crédito/débito usando el conector adyen.payment-provider-v3 en VTEX: el flujo completo, las APIs involucradas y los payloads que su aplicación necesita enviar y manejar. También cubre los pasos adicionales necesarios en una tienda headless.

Contexto

El 3DS es el protocolo de autenticación del titular de la tarjeta (challenge del banco emisor). En el conector, funciona en tres modalidades:

ModalidadCómo sucedeCuándo
3DS2 nativo (fingerprint/challenge en iframe)El Adyen Web Component renderiza el desafío dentro del checkout, sin salir de la páginaPredeterminado — el conector envía nativeThreeDS: 'preferred'
Redirect (3DS1 / fallback)El shopper es redirigido al emisor y vuelve por el returnUrl del conectorCuando el emisor/Adyen no soporta el flujo nativo
Data OnlySin challenge — solo se envían datos a la marca (frictionless)Cuando la AppSetting serviceAuthentication = 'Data Only', tarjeta de crédito y moneda BRL

Diagramas de Flujo

Tarjeta sin 3DS (flujo simple)

Tarjeta con 3DS2 nativo (challenge en el checkout)

Tarjeta con 3DS vía redirect (fallback)

APIs del Conector en el Flujo 3DS

#RutaMétodoQuién llamaRol en el 3DS
1/payments (ruta estándar PPP — authorize)POSTGateway VTEXCrea el pago en Adyen; si la respuesta es 3DS, devuelve paymentAppData
2/_v/api/payment-detailsPOSTPayment App (evento onAdditionalDetails de Adyen Web) o app headlessCompleta el 3DS nativo: envía threeDSResult a /payments/details de Adyen y aprueba/deniega en el gateway
3/_v/api/payment/3ds/redirectGETNavegador del shopper (retorno del emisor)Completa el 3DS por redirect: envía redirectResult a /payments/details y aprueba/deniega en el gateway
4/_v/api/payment-statusPOSTPayment App (polling 5s) o app headlessConsulta el estado consolidado (VBase av3settle)
5/_v3/api/webhook/notificationPOSTAdyen (webhooks)Notificaciones asíncronas; correlaciona por pspReference (VBase av3notify)
6/_v/api/payment-authorizationPOSTPayment App (flujos wallet/componente — Google Pay, Blik, etc.)No participa del flujo de tarjeta estándar; se menciona aquí para evitar confusión con el /payments del PPP

Datos Enviados y Recibidos por API

POST /payments (PPP authorize) → Adyen POST /checkout/v72/payments

El gateway envía el AuthorizationRequest del protocolo VTEX (tarjeta tokenizada PCI: numberToken, cscToken, holderToken, expiration, además de miniCart, merchantSettings, transactionId, paymentId, returnUrl...).

⚠️ Punto crítico del flujo 3DS: Adyen solo devuelve la action de 3DS2 nativo si el conector logra construir el browserInfo del shopper (dispositivo/navegador). Sin ese dato, el pago se procesa frictionless (sin challenge), silenciosamente. El CardService construye ese browserInfo a partir del campo deviceInfo registrado en la transacción — y para eso, la app necesita enviar deviceInfo en la propia llamada de pago:

POST https://{account}.vtexpayments.com.br/api/pub/transactions/{transactionId}/payments
?orderId={orderGroup}&redirect=false&deviceInfo={base64}

Donde {base64} es el base64 de:

sw={screen.width}&sh={screen.height}&cd={screen.colorDepth}&tz={new Date().getTimezoneOffset()}&lang={navigator.language}&java={navigator.javaEnabled()}

orderGroup proviene de la respuesta de creación del pedido. Este es el mismo mecanismo usado por el checkout nativo de VTEX — vea Integración Headless más abajo para el paso a paso completo en una integración headless.

Cuerpo enviado a Adyen (campos relevantes para 3DS):

{
"merchantAccount": "MiTienda_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.mitienda.com",
"browserInfo": { "...": "recolectado en el checkout" },
"shopperInteraction": "Ecommerce",
"shopperEmail": "joao@email.com",
"shopperIP": "200.1.2.3",
"shopperReference": "<vtexUserId>",
"shopperConversionId": "<paymentId>",
"installments": { "value": 1 },
"billingAddress": { "...": "..." },
"deliveryAddress": { "...": "..." }
}
  • En el modo Data Only: dataOnly: true, nativeThreeDS: 'disabled' y additionalData.threeDS2DataOnly: true — no hay challenge.
  • Los datos de tarjeta viajan por el secure proxy de VTEX (SecureExternalClient + header Idempotency-Key = paymentId).
  • browserInfo + origin son prerrequisito del 3DS2 nativo: sin ellos, Adyen no devuelve action nativa.

Respuesta de Adyen cuando se exige 3DS (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 posibles en la rama 3DS: RedirectShopper, ChallengeShopper, IdentifyShopper (para redirect, action.type: "redirect" con url, method y data { MD, PaReq }).

Respuesta del conector al gateway (Authorizations.redirect) — esto es lo que dispara la renderización del 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 sin apiKey...},\"action\":{...},\"clientKey\":\"test_...\",\"environment\":\"test|live\",\"merchantName\":\"...\",\"shopperEmail\":\"...\",\"shopperReference\":\"...\",\"countryCode\":\"BR\",\"lineItems\":[...],\"denyUrl\":\"/_v/api/cancel-payment?paymentId=...\"}"
}
}

Idempotencia: la respuesta 3DS se guarda en VBase rpr (clave paymentId). Si el gateway reintenta el authorize, el conector reutiliza la misma action en lugar de crear un nuevo pago en Adyen.

POST /_v/api/payment-details

Llamado por el Payment App en el onAdditionalDetails de Adyen Web, después de que el shopper completa el fingerprint/challenge. El cuerpo es la fusión del state.data del componente con el objeto authorization del payload:

{
"details": { "threeDSResult": "eyJ0cmFuc1N0YXR1cyI6IlkifQ==" },
"paymentData": "...",
"transactionId": "...",
"paymentId": "...",
"orderId": "...",
"merchantName": "mitienda",
"card": { "bin": "411111" },
"miniCart": { "buyer": { "id": "..." } }
}

El conector reenvía solo body.details al POST /payments/details de Adyen, y con la respuesta:

  • Authorised → guarda VBase av3settle, establece networkTxReference en la transacción y llama al callback del gateway (POST https://{account}.vtexpayments.com.br/api/pvt/payment-provider/transactions/{tx}/payments/{py}/callback) con status: "approved" y tid/nsu/authorizationId = pspReference;
  • Refused / Error / Cancelled → callback con status: "denied" + refusalReasonCode;
  • Respuesta HTTP al llamador: string con el resultCode (ej.: "Authorised"). El Payment App usa esto para disparar transactionValidation.vtex.
  • Si merchantName difiere de la account (multi-tienda), la llamada se proxya hacia la account dueña de la configuración.

GET /_v/api/payment/3ds/redirect

Query string: account, paymentId, transactionId (fijados en el returnUrl) + redirectResult (anexado por Adyen en el retorno del emisor).

El middleware:

  1. Llama a POST /payments/details con { details: { redirectResult } };
  2. Si la transacción ya está Cancelled en VTEX → cancelOrRefund en Adyen;
  3. Authorised → callback approved al gateway; rechazos → callback denied;
  4. Siempre finaliza con 302 hacia la página de retorno del 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" } mientras no haya conclusión, o { "status": true|false } cuando el pago fue concluido (leído de VBase av3settle). El Payment App hace polling cada 5s como red de seguridad del challenge.

Persistencia (VBase) Usada por el 3DS

BucketClaveContenidoPara qué
rprpaymentIdThreeDSResponse de AdyenIdempotencia del retry de authorize
av3notifypspReference{ paymentId, transactionId, orderId, account }Correlación de los webhooks de Adyen
av3settlepaymentId / orderId{ pspReference, success, step }Cortocircuito de authorize repetido + payment-status

Integración Headless

Contexto del 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."

En un front headless los scripts del checkout de VTEX no se ejecutan, por lo que browserInfo/origin nunca llegan al conector y el Payment App no se renderiza automáticamente. La app headless necesita asumir esos dos roles.

📚 Referencia general de integración headless: Headless Cart and Checkout — Complete order.

Paso a Paso

1. Crear el pedido y el pago, enviando deviceInfo (Checkout API / Transaction API — flujo do-payment: orderForm → order → transactions → payments). El gateway llama al authorize del conector por su cuenta; nada cambia en este flujo, con una excepción crítica:

⚠️ Sin deviceInfo, Adyen no devuelve el challenge nativo — el pago se procesa frictionless, silenciosamente. Envíe deviceInfo como query string en la llamada de pago (formato completo en Datos Enviados y Recibidos por API):

POST https://{account}.vtexpayments.com.br/api/pub/transactions/{transactionId}/payments
?orderId={orderGroup}&redirect=false&deviceInfo={base64}

📚 Referencia: Payments Gateway APIPOST /api/payments/transactions/{transactionId}/payments.

2. Llamar al gatewayCallback y detectar el 3DS por la respuesta. Después de enviar el pago, llame a:

POST https://{account}.{environment}.com.br/api/checkout/pub/gatewayCallback/{orderGroup}

📚 Referencia: Checkout APIPOST /api/checkout/pub/gatewayCallback/{orderGroup}.

⚠️ 428 Precondition Required es la respuesta esperada cuando hay un 3DS pendiente — no es un error. Trátela como un resultado válido, no como un fallo de la llamada. El cuerpo lleva paymentAuthorizationAppCollection, un elemento por app de pago pendiente:

{
"paymentAuthorizationAppCollection": [
{
"appName": "adyen.payment-provider-v3",
"appPayload": "{\"authorization\":{...},\"action\":{...},\"clientKey\":\"...\",\"environment\":\"test\"}"
}
]
}

Si la respuesta es 204 No Content, el pago ya fue resuelto (frictionless) — no hay challenge que renderizar, siga directamente al paso 4 (polling).

La app headless debe localizar el elemento con appName: "adyen.payment-provider-v3" y hacer el parse de appPayload (es una cadena JSON) para extraer action, clientKey, environment y authorization.

3. Renderizar el challenge con Adyen Web (rol que cumple el Payment App en el checkout nativo):

import { AdyenCheckout } from '@adyen/adyen-web'
import '@adyen/adyen-web/styles/adyen.css'

// appPayload vino del gatewayCallback (428) en el paso 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) => {
// Completar el 3DS en el 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' ...

// ⚠️ OBLIGATORIO: el "advanced flow" de Adyen Web exige resolver/rechazar
// este callback vía `actions`. Sin esto, el estado interno del componente
// nunca finaliza — en la práctica el challenge "se cierra" (el ACS/emisor
// cierra su propio iframe) pero no pasa nada más, porque el SDK nunca
// fue avisado de que el flujo terminó.
actions.resolve({ resultCode })
} catch (err) {
actions.reject()
}
},
})

checkout
.createFromAction(payload.action, { challengeWindowSize: '02' })
.mount('#adyen-container')

4. Completar y confirmar. El propio conector aprueba/deniega el pago en el gateway vía callback — la app headless no necesita llamar al gateway. Para confirmar, use el polling:

POST https://{account}.myvtex.com/api/io/_v/api/payment-status
{ "paymentId": "<paymentId>" }
→ { "status": "pending" } … → { "status": true }

o consulte el estado de la transacción/pedido mediante las APIs de VTEX.

5. Cuando action.type === "redirect". El shopper sale hacia el emisor y vuelve por el propio returnUrl del conector (/_v/api/payment/3ds/redirect), que finaliza el pago y redirige a la página de retorno del gateway en {account}.vtexpayments.com.br. Como ese returnUrl está fijado por el backend, la app headless no controla la página de destino final — maneje este escenario abriendo el redirect en un popup/iframe y monitoreando payment-status, o priorice el flujo nativo (que es el preferido por el conector vía nativeThreeDS: 'preferred' y que solo ocurre si el paso 1 se hizo correctamente).

Resumen del Contrato Headless

Responsabilidad del checkout nativoEquivalente headless
El script adyenv3 envía browserInfodeviceInfo en el query string del POST .../transactions/{id}/payments
El checkout renderiza el Payment AppgatewayCallback (428) → parse del appPayload + createFromAction(action)
onAdditionalDetails → payment-detailsIgual: POST /_v/api/payment-details con {...state.data, ...authorization}
Evento transactionValidation.vtexPolling POST /_v/api/payment-status

Pruebas

Para validar la implementación del flujo de 3D Secure (3DS) antes de llevarla a producción, es posible utilizar un merchantAccount de prueba de Adyen junto con las tarjetas de prueba que Adyen proporciona.

Adyen ofrece una lista de tarjetas de prueba y escenarios de autenticación que permiten validar diferentes comportamientos del flujo de 3DS, incluyendo:

  • Flujo Challenge;
  • Flujo Frictionless;
  • Escenarios de autenticación con éxito y con fallo;
  • Casos avanzados de prueba, como timeouts, errores y diferentes valores de transStatus.

Para consultar la lista completa de tarjetas de prueba, credenciales y escenarios disponibles, acceda a la documentación oficial de Adyen:

https://docs.adyen.com/development-resources/testing/3d-secure-2-authentication

Esa documentación contiene todas las tarjetas de prueba, los comportamientos esperados para cada escenario y la información necesaria para validar correctamente la implementación del 3DS en el entorno de pruebas de Adyen.

Observaciones y Limitaciones

  • Data Only desactiva el challenge: con serviceAuthentication = 'Data Only' (AppSettings), crédito y BRL, el conector envía dataOnly: true y nunca habrá action — no es posible probar el challenge en esa configuración.
  • Retry idempotente: el VBase rpr garantiza que los retries de authorize reutilicen la action original; limpiar ese registro es necesario para volver a probar un mismo paymentId.
  • Webhooks: además del flujo síncrono, Adyen envía notificaciones a /_v3/api/webhook/notification; la correlación usa el VBase av3notify registrado durante el authorize/payment-details.
  • Multi-account: payment-details proxya la llamada cuando merchantName difiere de la account que recibió la solicitud.
  • Wallets: Google Pay/Apple Pay con DPAN envían mpiData (cryptogram/eci) y no pasan por el challenge; el flujo 3DS de este documento aplica a tarjetas (FPAN) — vea el RFC de Google Pay para los escenarios FPAN × DPAN.

¿Esta página fue útil?