Ir al contenido principal

Cómo integrar Google reCAPTCHA Enterprise

Guía de integración para Checkout personalizado

Migración desde Fingerprint

Ventrata está sustituyendo la integración anterior basada en Fingerprint por Google reCAPTCHA Enterprise. Los Checkouts personalizados existentes deben eliminar su integración con Fingerprint y adoptar las acciones, ubicaciones de los tokens y el comportamiento de reintento de reCAPTCHA descritos en esta guía.

Durante el período de migración, algunos campos heredados de Fingerprint y fraudAssessment pueden seguir apareciendo en las respuestas de la API para mantener la compatibilidad con clientes antiguos. Las nuevas integraciones deben ignorar estos campos y no deben utilizarlos para determinar si la evaluación de fraude se ha completado.

Google reCAPTCHA Enterprise protege los flujos de creación de checkout y búsqueda de identidad frente a abusos automatizados y proporciona evaluaciones de Transaction Defense para los pagos con tarjeta.

Ventrata gestiona la integración con Google del lado del servidor. Tu Checkout personalizado es responsable de generar los tokens en el navegador e incluirlos en las solicitudes de API correspondientes.

Descripción general de la integración

  1. Obtén la clave pública del sitio (site key) de reCAPTCHA desde Ventrata.

  2. Carga el script de reCAPTCHA Enterprise en el navegador.

  3. Genera un token inmediatamente antes de cada interacción protegida.

  4. Utiliza la acción esperada por Ventrata.

  5. Coloca el token en el objeto de solicitud correspondiente.

  6. Reintenta la solicitud con un token nuevo cuando errorCode sea RECAPTCHA_REQUIRED.

📒 NOTA

Fingerprint ya no es necesario. No instales el agente de Fingerprint, no crees linked IDs, no consultes los campos heredados de recibos de Fingerprint ni envíes datos en el objeto heredado fraudAssessment.

Requisitos previos:

  • Utiliza un token de Checkout de Ventrata.

    Este token tiene el mismo valor que tu Checkout ID y está diseñado para uso público en el lado del cliente. Restringe el acceso a los endpoints disponibles para ese Checkout.

    const API_URL = "https://checkout-api.ventrata.com/octo"; 
    const CHECKOUT_TOKEN = "YOUR_CHECKOUT_TOKEN";

  • Incluye la capacidad ventrata/checkout y cualquier otra capacidad OCTO requerida por la solicitud.

    Authorization: Bearer YOUR_CHECKOUT_TOKEN 
    Octo-Capabilities: ventrata/checkout

  • Incluye las credenciales del navegador para que se conserve la cookie de sesión de Ventrata.

    const commonFetchOptions = {
    credentials: "include",
    headers: {
    Authorization: `Bearer ${CHECKOUT_TOKEN}`,
    "Octo-Capabilities": "ventrata/checkout",
    },
    };

📒 NOTA

Los ejemplos utilizan JavaScript e ilustran la estructura de las solicitudes. Adapta el manejo de errores y los service wrappers a tu aplicación.


Paso 1: Obtener la clave del sitio de reCAPTCHA

Solicita la configuración de Checkout y lee el valor de recaptchaEnterpriseSiteKey de la respuesta.

const response = await fetch(`${API_URL}/ventrata/checkout/config`, {   ...commonFetchOptions,
});

if (!response.ok) {
throw new Error("Unable to load the Ventrata checkout configuration");
}

const checkoutConfig = await response.json();
const recaptchaSiteKey = checkoutConfig.recaptchaEnterpriseSiteKey;

if (!recaptchaSiteKey) {
throw new Error("The checkout does not provide a reCAPTCHA site key");
}

📒 NOTA

La site key es pública y puede utilizarse de forma segura en el navegador. Nunca incluyas un secreto de Google, credenciales de una cuenta de servicio ni una clave de API en el código del lado del cliente. Ventrata crea las evaluaciones del lado del servidor.

‼️ IMPORTANTE

Durante el período de migración, la respuesta también contiene fingerprintPublicKey. Se trata de un marcador de compatibilidad y las nuevas integraciones deben ignorarlo.


Paso 2: Cargar reCAPTCHA Enterprise

Carga el script de reCAPTCHA Enterprise basado en puntuación una sola vez, tan pronto como la configuración proporcione la site key. Incluye la site key en el parámetro render y espera a que grecaptcha.enterprise.ready() se complete antes de llamar a execute(). Consulta la documentación de Google sobre la integración de páginas web con claves basadas en puntuación.

Si cargas el script dinámicamente, ten en cuenta que el evento de carga del script y la disponibilidad de grecaptcha.enterprise pueden producirse en momentos distintos. Un intento posterior debe poder reutilizar un script que haya terminado de cargarse después de que un intento anterior haya agotado el tiempo de espera.

El siguiente helper utiliza www.recaptcha.net, que Google admite como alternativa para entornos que no pueden acceder a www.google.com. Consulta las Preguntas frecuentes de reCAPTCHA Enterprise.

Este helper trata los eventos load y error como rutas rápidas, comprueba periódicamente la disponibilidad hasta que se alcanza el tiempo de espera global y conserva un script cuya carga sea lenta para que un intento posterior pueda reutilizarlo.

const RECAPTCHA_SCRIPT_ID = "ventrata-recaptcha-enterprise-script";
const RECAPTCHA_LOAD_TIMEOUT_MS = 15_000;
const RECAPTCHA_READINESS_POLL_MS = 250;
const RECAPTCHA_EXECUTE_RETRY_DELAY_MS = 300;
const RECAPTCHA_EXECUTE_ATTEMPTS = 3;
const delay = (milliseconds) =>
new Promise((resolve) => setTimeout(resolve, milliseconds));
let recaptchaReadyPromise;

function getErrorMessage(error) {
return error instanceof Error ? error.message : String(error);
}

function reportRecaptchaFailure(stage, error, context = {}) {
const event = {
stage,
reason: getErrorMessage(error),
...context,
};

// Replace this with the integration's central browser telemetry.
// Never include the generated token, checkout token, or customer data.
console.warn("[reCAPTCHA]", event);
}

function recaptchaEnterpriseIsAvailable() {
return (
typeof window.grecaptcha?.enterprise?.ready === "function" &&
typeof window.grecaptcha?.enterprise?.execute === "function"
);
}

function loadRecaptchaEnterprise(siteKey, nonce) {
if (recaptchaEnterpriseIsAvailable()) {
return new Promise((resolve) =>
window.grecaptcha.enterprise.ready(resolve),
);
}

if (recaptchaReadyPromise) {
return recaptchaReadyPromise;
}

recaptchaReadyPromise = new Promise((resolve, reject) => {
let script = document.getElementById(RECAPTCHA_SCRIPT_ID);
let settled = false;
let timeout;
let readinessPoll;

const stopWaiting = () => {
clearTimeout(timeout);
clearInterval(readinessPoll);
};

const fail = (error, { keepScript = false } = {}) => {
if (settled) {
return;
}

settled = true;
stopWaiting();

if (!keepScript) {
script?.remove();
}

recaptchaReadyPromise = undefined;
reportRecaptchaFailure("recaptcha.loader_failed", error);
reject(error);
};

const adoptEnterprise = () => {
if (settled || !recaptchaEnterpriseIsAvailable()) {
return;
}

window.grecaptcha.enterprise.ready(() => {
if (settled) {
return;
}

settled = true;
stopWaiting();
recaptchaReadyPromise = undefined;
resolve();
});
};

timeout = setTimeout(
() =>
fail(new Error("reCAPTCHA Enterprise loading timed out"), {
// The browser might still be downloading this script. Leave it in
// place so a later attempt can observe or adopt it.
keepScript: true,
}),
RECAPTCHA_LOAD_TIMEOUT_MS,
);

readinessPoll = setInterval(
adoptEnterprise,
RECAPTCHA_READINESS_POLL_MS,
);

if (!script) {
script = document.createElement("script");
script.id = RECAPTCHA_SCRIPT_ID;
script.src = `https://www.recaptcha.net/recaptcha/enterprise.js?render=${encodeURIComponent(siteKey)}`;
script.async = true;
script.defer = true;

if (nonce) {
script.nonce = nonce;
}
}

script.addEventListener("load", adoptEnterprise, { once: true });
script.addEventListener(
"error",
() => fail(new Error("Unable to load reCAPTCHA Enterprise")),
{ once: true },
);

if (!script.isConnected) {
document.head.appendChild(script);
}

// Covers Enterprise loaded by another component before these listeners
// were attached.
adoptEnterprise();
});

return recaptchaReadyPromise;
}


Paso 3: Generar los tokens justo antes de usarlos

Genera un token inmediatamente antes de la interacción que vaya a proteger. No generes tokens continuamente en segundo plano ni reutilices un mismo token para solicitudes independientes.

async function createRecaptchaToken(action) {
await loadRecaptchaEnterprise(recaptchaSiteKey);

let lastError; for (
let attempt = 1;
attempt <= RECAPTCHA_EXECUTE_ATTEMPTS;
attempt++
) {
try {
const token = await window.grecaptcha.enterprise.execute(
recaptchaSiteKey,
{ action },
);

if (typeof token !== "string" || token.length === 0) {
throw new Error("reCAPTCHA Enterprise returned an empty token");
}

return token;
} catch (error) {
lastError = error;

if (attempt < RECAPTCHA_EXECUTE_ATTEMPTS) {
await delay(RECAPTCHA_EXECUTE_RETRY_DELAY_MS);
}
}
}
reportRecaptchaFailure("recaptcha.token_failed", lastError, { action });
throw lastError;
}

📒 NOTE

Estos tres intentos de generación de tokens se realizan antes de la solicitud a la API de Checkout. Son independientes y no aumentan el límite de tres solicitudes totales descrito en el Paso 5.

Los tokens caducan a los dos minutos y, normalmente, solo pueden evaluarse una vez. Genera un nuevo token cart_add o purchase para cada solicitud que lo requiera. El flujo login dispone de una excepción limitada de reutilización que se describe más adelante.

📗 CONSEJO

Para obtener información sobre la duración y el uso de los tokens, consulta la documentación de Google sobre la obtención de tokens de reCAPTCHA.


Paso 4: Utilizar la acción y la ubicación de la carga útil correctas

Los nombres de las acciones están en minúsculas y deben coincidir exactamente con el valor esperado por Ventrata. Consulta la documentación de Google sobre los nombres de las acciones.

Acción

Se utiliza para

Ubicación en la solicitud

cart_add

Crear un nuevo pedido, reserva, compra o regalo

recaptchaEnterprise de nivel superior

purchase

Todas las solicitudes que contienen cardPayment

cardPayment.recaptchaEnterprise

login

Flujos de inicio de sesión de membresías, check-in, concierge y búsqueda de identidad

recaptchaEnterprise de nivel superior

Crear un pedido, una reserva, una compra o un regalo

Genera un nuevo token cart_add para cada solicitud de creación y colócalo en el objeto recaptchaEnterprise de nivel superior.

{
"currency": "USD",
"recaptchaEnterprise": {
"token": "CART_ADD_TOKEN"
}
}

📒 NOTA

Esto se aplica a las solicitudes POST que crean pedidos, reservas, compras y regalos. No añadas cart_add a solicitudes de actualización o de nueva reserva de registros existentes. Si repites una solicitud de creación tras un error, genera un nuevo token.


Enviar un pago con tarjeta

Siempre que una solicitud contenga cardPayment, genera un nuevo token purchase para esa solicitud e inclúyelo directamente en cardPayment.recaptchaEnterprise. Esto también se aplica cuando se repite una solicitud después de una actualización del gateway o de una respuesta intermedia.

📒 NOTE

No actualices el token por separado. Genera el token purchase como parte de la propia solicitud de pago. No realices una actualización adicional del pedido únicamente para actualizar el token.

{
"currency": "USD",
"cardPayment": {
"gateway": "adyen",
"adyen": {
"sessionId": "ADYEN_SESSION_ID",
"sessionResult": "ADYEN_SESSION_RESULT"
},
"recaptchaEnterprise": {
"token": "PURCHASE_TOKEN"
}
}
}
  • Utiliza purchase para las actualizaciones o confirmaciones de pedidos y reservas que contengan cardPayment.

  • Utilízalo tanto en Checkout como en los flujos de pago de Manage My Booking.

  • Utilízalo con todos los gateways de pago con tarjeta compatibles.

📒 NOTA

Ventrata requiere la acción purchase para las solicitudes que contienen cardPayment. Para obtener información adicional sobre Transaction Defense, consulta la documentación correspondiente de Google.


Crear un pedido con pago con tarjeta

Una solicitud de creación que también incluya cardPayment necesita dos tokens diferentes:

  • cart_add en el nivel superior;

  • purchase dentro de cardPayment.

No es posible utilizar un mismo token para ambas acciones.

{
"currency": "USD",
"recaptchaEnterprise": {
"token": "CART_ADD_TOKEN"
},
"cardPayment": {
"gateway": "adyen",
"recaptchaEnterprise": {
"token": "PURCHASE_TOKEN"
}
}
}


Inicio de sesión y búsqueda de identidad

Genera un token login para las solicitudes de inicio de sesión de membresías, check-in, concierge y búsqueda de identidad.

{
"email": "[email protected]",
"recaptchaEnterprise": {
"token": "LOGIN_TOKEN"
}
}

Dentro de un mismo flujo de identidad, puedes conservar el token para las solicitudes posteriores que utilicen la misma dirección de correo electrónico normalizada, número de teléfono móvil o referencia de reserva.

Descarta el token y genera uno nuevo cuando:

  • Ventrata devuelva RECAPTCHA_REQUIRED.

  • El cliente cambie la dirección de correo electrónico, el número de teléfono móvil, el país o la referencia de la reserva.

  • Se inicie un nuevo flujo de inicio de sesión o de búsqueda.


Paso 5: Gestionar RECAPTCHA_REQUIRED

Una solicitud protegida de creación o búsqueda de identidad puede devolver RECAPTCHA_REQUIRED cuando el token falta, ha caducado, ya ha sido evaluado o está afectado por un error del navegador que permite reintentos.

{
"error": "BAD_REQUEST",
"errorMessage": "A new Google reCAPTCHA token is required",
"errorCode": "RECAPTCHA_REQUIRED",
"reason": "MISSING"
}

Motivo

Significado

MISSING

La solicitud omitió el token o envió null.

EXPIRED

El token ya no era válido.

DUPE

El token ya había sido evaluado.

BROWSER_ERROR

Google informó de un error del navegador que permite reintentos.

Comprueba únicamente errorCode para decidir si debes reintentar la solicitud. En todas las respuestas RECAPTCHA_REQUIRED, descarta el token anterior y vuelve a intentarlo con un token recién generado. Si la generación del token falla, envía null para que Ventrata devuelva el error explícito y espera dos segundos antes de volver a intentarlo. Deja de reintentar después de un máximo de tres solicitudes a la API de Checkout.

function isRecaptchaRequired(data) {
return data?.errorCode === "RECAPTCHA_REQUIRED";
}

async function withRecaptchaRetry(action, sendRequest) {
const maximumAttempts = 3;

for (let attempt = 1; attempt <= maximumAttempts; attempt++) {
let token = null;

try {
token = await createRecaptchaToken(action);
} catch (error) {
// Sending null lets Ventrata return the explicit retryable error.
reportRecaptchaFailure("recaptcha.request_token_unavailable", error, {
action,
requestAttempt: attempt,
});
}

const response = await sendRequest(token);
const data = await response.json();

if (response.ok) {
return data;
}

if (!isRecaptchaRequired(data) || attempt === maximumAttempts) {
const error = new Error(
data.errorMessage || data.error || "The request failed",
);
error.response = data;
throw error;
}

if (token === null) {
await delay(2_000);
}
}
}

El siguiente ejemplo aplica este comportamiento de reintento a una solicitud protegida de creación de pedidos. En cada intento se genera un nuevo token cart_add y se coloca en el objeto recaptchaEnterprise de nivel superior.

const order = await withRecaptchaRetry("cart_add", (token) =>
fetch(`${API_URL}/orders`, {
...commonFetchOptions,
method: "POST",
headers: {
...commonFetchOptions.headers,
"Content-Type": "application/json",
"Octo-Capabilities": "ventrata/checkout,octo/cart",
},
body: JSON.stringify({
currency: "USD",
recaptchaEnterprise: { token },
}),
}),
);

📒 NOTA

Aplica el mismo comportamiento de reintento al resto de solicitudes protegidas de creación y búsqueda de identidad, utilizando la acción y la ubicación correspondientes de la carga útil. Si una solicitud repetida contiene cardPayment, genera también un nuevo token purchase anidado para ese intento.

No reintentes automáticamente en los siguientes casos:

  • VERIFICATION_FAILED: la evaluación falló por un motivo que no admite reintentos, como una puntuación baja o una acción inesperada.

  • PAYMENT_DECLINE_LIMIT_EXCEEDED: el pedido ha superado el número máximo permitido de intentos de pago rechazados.


Política de seguridad de contenido (Content Security Policy)

Google recomienda utilizar un CSP nonce. Aplícalo al script de Enterprise. reCAPTCHA también admite strict-dynamic en los navegadores compatibles. Consulta las Preguntas frecuentes de reCAPTCHA Enterprise y la documentación de Google sobre CSP.

Si tu política utiliza listas de hosts permitidos y la integración carga recursos desde www.recaptcha.net, permite los orígenes necesarios para scripts, frames y conexiones.

script-src https://www.recaptcha.net/recaptcha/ https://www.gstatic.com/recaptcha/; 
frame-src https://www.recaptcha.net/recaptcha/ https://recaptcha.google.com/recaptcha/;
connect-src https://www.recaptcha.net/recaptcha/;

Prueba la política en todos los navegadores compatibles y supervisa los informes de infracciones de CSP.


Migración desde Fingerprint

Elimina la integración anterior con Fingerprint antes de habilitar la aplicación de reCAPTCHA en tu Checkout:

  • Elimina @fingerprint/agent y la configuración de los endpoints de Fingerprint.

  • Deja de utilizar fingerprintPublicKey, fingerprintLinkedId y fingerprintReceived.

  • Elimina el sondeo de Fingerprint, los callbacks, el código del navegador relacionado con webhooks y los procesos en segundo plano.

  • Deja de construir objetos de solicitud fraudAssessment.

  • Elimina la generación periódica de tokens de reCAPTCHA en segundo plano y los nombres de acción heredados.

  • No envíes siteKey en el cuerpo de las solicitudes de la API; envía únicamente el token generado.

‼️ IMPORTANTE

Es posible que, temporalmente, sigas viendo campos heredados de Fingerprint o fraudAssessment en las respuestas de la API. Las nuevas integraciones deben ignorarlos y no utilizarlos como prueba de que existe una evaluación de pago.

Lista de verificación

Antes de habilitar la aplicación de reCAPTCHA para el Checkout, verifica que:

  • la integración utiliza https://checkout-api.ventrata.com/octo y conserva las credenciales;

  • la configuración de Checkout devuelve recaptchaEnterpriseSiteKey;

  • el script de Enterprise se carga una sola vez y execute() espera a grecaptcha.enterprise.ready();

  • el cargador espera a grecaptcha.enterprise y no depende únicamente del evento de carga del script;

  • un error real del script permite una carga posterior, mientras que un tiempo de espera agotado conserva el script lento para que pueda reutilizarse más adelante;

  • los nombres de las acciones están en minúsculas y coinciden exactamente con cart_add, purchase o login;

  • solo las solicitudes de creación reciben cart_add; las actualizaciones y nuevas reservas no lo reciben;

  • todas las solicitudes que contienen cardPayment reciben un nuevo token purchase anidado;

  • las solicitudes de creación que contienen cardPayment incluyen dos tokens distintos;

  • los tokens login solo se conservan dentro del mismo flujo de identidad;

  • RECAPTCHA_REQUIRED se identifica mediante errorCode y el proceso de reintento se detiene después de un máximo de tres solicitudes a la API;

  • los errores en la generación de tokens se registran sin almacenar los propios tokens, las cabeceras de autorización ni los datos de los clientes;

  • ninguna solicitud depende de los campos fraudAssessment o de Fingerprint.

¿Ha quedado contestada tu pregunta?