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:
| Modalidad | Cómo sucede | Cuándo |
|---|---|---|
| 3DS2 nativo (fingerprint/challenge en iframe) | El Adyen Web Component renderiza el desafío dentro del checkout, sin salir de la página | Predeterminado — el conector envía nativeThreeDS: 'preferred' |
| Redirect (3DS1 / fallback) | El shopper es redirigido al emisor y vuelve por el returnUrl del conector | Cuando el emisor/Adyen no soporta el flujo nativo |
| Data Only | Sin 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
| # | Ruta | Método | Quién llama | Rol en el 3DS |
|---|---|---|---|---|
| 1 | /payments (ruta estándar PPP — authorize) | POST | Gateway VTEX | Crea el pago en Adyen; si la respuesta es 3DS, devuelve paymentAppData |
| 2 | /_v/api/payment-details | POST | Payment App (evento onAdditionalDetails de Adyen Web) o app headless | Completa el 3DS nativo: envía threeDSResult a /payments/details de Adyen y aprueba/deniega en el gateway |
| 3 | /_v/api/payment/3ds/redirect | GET | Navegador 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-status | POST | Payment App (polling 5s) o app headless | Consulta el estado consolidado (VBase av3settle) |
| 5 | /_v3/api/webhook/notification | POST | Adyen (webhooks) | Notificaciones asíncronas; correlaciona por pspReference (VBase av3notify) |
| 6 | /_v/api/payment-authorization | POST | Payment 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'yadditionalData.threeDS2DataOnly: true— no hay challenge. - Los datos de tarjeta viajan por el secure proxy de VTEX (
SecureExternalClient+ headerIdempotency-Key = paymentId). browserInfo+originson prerrequisito del 3DS2 nativo: sin ellos, Adyen no devuelveactionnativa.
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(clavepaymentId). Si el gateway reintenta elauthorize, el conector reutiliza la mismaactionen 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 VBaseav3settle, establecenetworkTxReferenceen la transacción y llama al callback del gateway (POST https://{account}.vtexpayments.com.br/api/pvt/payment-provider/transactions/{tx}/payments/{py}/callback) constatus: "approved"ytid/nsu/authorizationId = pspReference;Refused/Error/Cancelled→ callback constatus: "denied"+refusalReasonCode;- Respuesta HTTP al llamador: string con el
resultCode(ej.:"Authorised"). El Payment App usa esto para disparartransactionValidation.vtex. - Si
merchantNamedifiere 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:
- Llama a
POST /payments/detailscon{ details: { redirectResult } }; - Si la transacción ya está
Cancelleden VTEX →cancelOrRefunden Adyen; Authorised→ callbackapprovedal gateway; rechazos → callbackdenied;- 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
| Bucket | Clave | Contenido | Para qué |
|---|---|---|---|
rpr | paymentId | ThreeDSResponse de Adyen | Idempotencia del retry de authorize |
av3notify | pspReference | { paymentId, transactionId, orderId, account } | Correlación de los webhooks de Adyen |
av3settle | paymentId / 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
browserInfodata, 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 API — POST /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 API — POST /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 nativo | Equivalente headless |
|---|---|
El script adyenv3 envía browserInfo | deviceInfo en el query string del POST .../transactions/{id}/payments |
| El checkout renderiza el Payment App | gatewayCallback (428) → parse del appPayload + createFromAction(action) |
onAdditionalDetails → payment-details | Igual: POST /_v/api/payment-details con {...state.data, ...authorization} |
Evento transactionValidation.vtex | Polling 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íadataOnly: truey nunca habráaction— no es posible probar el challenge en esa configuración. - Retry idempotente: el VBase
rprgarantiza que los retries deauthorizereutilicen laactionoriginal; limpiar ese registro es necesario para volver a probar un mismopaymentId. - Webhooks: además del flujo síncrono, Adyen envía notificaciones a
/_v3/api/webhook/notification; la correlación usa el VBaseav3notifyregistrado durante elauthorize/payment-details. - Multi-account:
payment-detailsproxya la llamada cuandomerchantNamedifiere 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?