Passer au contenu principal

Comment intégrer Google reCAPTCHA Enterprise

Guide d’intégration pour un Checkout personnalisé

Migration depuis Fingerprint

Ventrata remplace l'intégration précédente basée sur Fingerprint par Google reCAPTCHA Enterprise. Les intégrations Custom Checkout existantes doivent supprimer leur intégration Fingerprint et adopter les actions reCAPTCHA, les emplacements des jetons (tokens) et le comportement de nouvelle tentative décrits dans ce guide.

Pendant la période de migration, certains anciens champs Fingerprint et fraudAssessment peuvent continuer à apparaître dans les réponses de l'API afin de préserver la compatibilité avec les anciens clients. Les nouvelles intégrations doivent ignorer ces champs et ne doivent pas les utiliser pour déterminer si l'évaluation de la fraude est terminée.

Google reCAPTCHA Enterprise protège les processus de création de paiement (checkout) et de recherche d'identité contre les abus automatisés, tout en fournissant des évaluations Transaction Defense pour les paiements par carte.

Ventrata prend en charge l'intégration côté serveur avec Google. Votre Custom Checkout est responsable de la génération des jetons (tokens) dans le navigateur et de leur insertion dans les requêtes API appropriées.

Présentation de l'intégration

  1. Récupérez auprès de Ventrata la clé de site publique (site key) reCAPTCHA.

  2. Chargez le script navigateur reCAPTCHA Enterprise.

  3. Générez un jeton immédiatement avant chaque interaction protégée.

  4. Utilisez l'action attendue par Ventrata.

  5. Placez le jeton dans l'objet de requête approprié.

  6. Générez un nouveau jeton et réessayez lorsque errorCode est égal à RECAPTCHA_REQUIRED.

📒 REMARQUE

Fingerprint n'est plus nécessaire. N'installez plus l'agent Fingerprint, ne créez plus d'identifiants liés (linked IDs), n'interrogez plus les champs de reçu Fingerprint et n'envoyez plus de données dans l'ancien objet fraudAssessment.

Prérequis:

  • Utilisez un Checkout Token Ventrata.

    Ce jeton correspond à la même valeur que votre Checkout ID et est destiné à une utilisation publique côté client. Il limite l'accès aux points de terminaison (endpoints) disponibles pour ce Checkout.

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

  • Incluez la capacité ventrata/checkout ainsi que toute autre capacité OCTO requise par la requête.

    Authorization: Bearer YOUR_CHECKOUT_TOKEN 
    Octo-Capabilities: ventrata/checkout

  • Incluez les identifiants du navigateur (browser credentials) afin que le cookie de session Ventrata soit conservé.

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

📒 REMARQUE

Les exemples utilisent JavaScript et illustrent la structure des requêtes. Adaptez la gestion des erreurs et les couches de services (service wrappers) à votre application.


Étape 1 : Récupérer la clé de site reCAPTCHA

Récupérez la configuration du Checkout et lisez la valeur recaptchaEnterpriseSiteKey dans la réponse.

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");
}

📒 REMARQUE

La clé de site est publique et peut être utilisée en toute sécurité dans le navigateur. Ne placez jamais un secret Google, des identifiants de compte de service ou une clé API dans le code côté client. Ventrata effectue les évaluations côté serveur.

‼️ IMPORTANT

Pendant la période de migration, la réponse contient également le champ fingerprintPublicKey. Il s'agit d'un champ de compatibilité qui doit être ignoré par les nouvelles intégrations.


Étape 2 : Charger reCAPTCHA Enterprise

Chargez le script Enterprise basé sur le score une seule fois, dès que la configuration fournit la clé de site. Incluez la clé de site dans le paramètre render et attendez que grecaptcha.enterprise.ready() soit exécuté avant d'appeler execute(). Consultez la documentation Google concernant l'intégration de clés basées sur le score dans les pages web.

Si vous chargez le script dynamiquement, prévoyez que l'événement de chargement du script et la disponibilité de grecaptcha.enterprise puissent survenir à des moments différents. Une tentative ultérieure doit pouvoir réutiliser un script qui s'est chargé après l'expiration du délai d'une tentative précédente.

L'exemple suivant utilise www.recaptcha.net, que Google prend en charge comme alternative lorsque www.google.com n'est pas accessible. Consultez la FAQ de reCAPTCHA Enterprise.

Il utilise les événements de chargement (load) et d'erreur (error) comme voies rapides, vérifie régulièrement l'état de disponibilité jusqu'à l'expiration du délai global et conserve un script lent après expiration afin qu'une tentative ultérieure puisse le réutiliser.

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;
}


Étape 3 : Générer les jetons au dernier moment

Générez un jeton immédiatement avant l'interaction qu'il protège. Ne générez pas de jetons en continu en arrière-plan et ne réutilisez pas un même jeton pour plusieurs requêtes distinctes.

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;
}

📒 REMARQUE

Ces trois tentatives de génération de jetons ont lieu avant l'appel à l'API Checkout. Elles sont indépendantes et ne comptent pas dans la limite de trois requêtes API décrite à l'étape 5.

Les jetons expirent après deux minutes et ne peuvent généralement être évalués qu'une seule fois. Générez un nouveau jeton cart_add ou purchase pour chaque requête qui en nécessite un. Le flux login bénéficie d'une exception limitée de réutilisation décrite ci-dessous.

📗 ASTUCE

Pour plus d'informations sur la durée de vie et l'utilisation des jetons, consultez la documentation Google sur la récupération des jetons reCAPTCHA.


Étape 4 : Utiliser l'action et l'emplacement appropriés

Les noms des actions sont en minuscules et doivent correspondre exactement à ceux attendus par Ventrata. Consultez les recommandations de Google concernant les noms d'action.

Action

Utilisation

Emplacement dans la requête

cart_add

Création d'une commande, d'une réservation, d'un achat ou d'un cadeau

Objet recaptchaEnterprise de premier niveau

purchase

Toute requête contenant cardPayment

cardPayment.recaptchaEnterprise

login

Flux d'adhésion, d'enregistrement, de connexion Concierge et de recherche d'identité

Objet recaptchaEnterprise de premier niveau

Créer une commande, une réservation, un achat ou un cadeau

Générez un nouveau jeton cart_add pour chaque requête de création et placez-le dans l'objet recaptchaEnterprise de premier niveau.

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

📒 REMARQUE

Cette règle s'applique aux requêtes POST qui créent des commandes, des réservations, des achats et des cadeaux. N'ajoutez pas cart_add aux requêtes de mise à jour ou de nouvelle réservation concernant des enregistrements existants. Si vous répétez une requête de création après une erreur, générez un nouveau jeton.


Envoyer un paiement par carte

Chaque fois qu'une requête contient cardPayment, générez un nouveau jeton purchase pour cette requête et placez-le directement dans cardPayment.recaptchaEnterprise. Cette règle s'applique également lorsqu'une requête est répétée après un rafraîchissement de la passerelle de paiement ou une réponse intermédiaire.

📒 REMARQUE

N'effectuez pas un rafraîchissement séparé. Générez le jeton purchase directement dans la requête de paiement. N'effectuez pas une mise à jour supplémentaire de la commande uniquement pour actualiser le jeton.

{
"currency": "USD",
"cardPayment": {
"gateway": "adyen",
"adyen": {
"sessionId": "ADYEN_SESSION_ID",
"sessionResult": "ADYEN_SESSION_RESULT"
},
"recaptchaEnterprise": {
"token": "PURCHASE_TOKEN"
}
}
}
  • Utilisez purchase pour les mises à jour ou confirmations de commandes et de réservations contenant cardPayment.

  • Utilisez-le dans les flux de paiement Checkout et Manage My Booking.

  • Utilisez-le avec toutes les passerelles de paiement par carte prises en charge.

📒 REMARQUE

Ventrata exige l'action purchase pour toutes les requêtes contenant cardPayment. Pour plus d'informations sur Transaction Defense, consultez la documentation Google correspondante.


Créer une commande avec un paiement par carte

Une requête de création contenant également cardPayment nécessite deux jetons distincts :

  • un jeton cart_add au niveau supérieur ;

  • un jeton purchase dans cardPayment.

Un même jeton ne peut pas être utilisé pour les deux actions.

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


Connexion et recherche d'identité

Générez un jeton login pour les requêtes de connexion liées aux adhésions, à l'enregistrement, au portail Concierge et aux recherches d'identité.

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

Au sein d'un même flux d'identification, vous pouvez conserver le même jeton pour les requêtes suivantes utilisant la même adresse e-mail normalisée, le même numéro de téléphone mobile ou la même référence de réservation.

Générez un nouveau jeton lorsque :

  • Ventrata renvoie RECAPTCHA_REQUIRED ;

  • le client modifie son adresse e-mail, son numéro de téléphone, son pays ou sa référence de réservation ;

  • un nouveau flux de connexion ou de recherche d'identité commence.


Étape 5 : Gérer l'erreur RECAPTCHA_REQUIRED

Une requête protégée de création ou de recherche d'identité peut renvoyer RECAPTCHA_REQUIRED lorsque le jeton est absent, expiré, déjà évalué ou affecté par une erreur navigateur pouvant être corrigée.

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

Raison

Signification

MISSING

Le jeton est absent ou la valeur null a été envoyée.

EXPIRED

Le jeton n'est plus valide.

DUPE

Le jeton a déjà été évalué.

BROWSER_ERROR

Google a signalé une erreur navigateur pouvant faire l'objet d'une nouvelle tentative.

Basez-vous uniquement sur errorCode pour déterminer si une nouvelle tentative est nécessaire. Pour chaque réponse RECAPTCHA_REQUIRED :

  • ignorez le jeton précédent ;

  • générez un nouveau jeton ;

  • réessayez la requête.

Si la génération du jeton échoue, envoyez null afin que Ventrata puisse renvoyer une erreur explicite, puis attendez deux secondes avant de réessayer.

Arrêtez-vous après un maximum de trois tentatives d'appel à l'API 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);
}
}
}

L'exemple suivant applique cette logique de nouvelle tentative à une requête protégée de création de commande. Chaque tentative génère un nouveau jeton cart_add placé dans l'objet recaptchaEnterprise de premier niveau.

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 },
}),
}),
);

📒 REMARQUE

Appliquez le même mécanisme aux autres requêtes protégées de création et de recherche d'identité en utilisant l'action et l'emplacement appropriés.

Si une requête répétée contient cardPayment, générez également un nouveau jeton purchase imbriqué pour cette tentative.

Ne relancez pas automatiquement les requêtes dans les cas suivants :

  • VERIFICATION_FAILED : l'évaluation a échoué pour une raison non récupérable, par exemple un score trop faible ou une action inattendue.

  • PAYMENT_DECLINE_LIMIT_EXCEEDED : la commande a dépassé le nombre autorisé de refus de paiement.


Content Security Policy

Google recommande d'utiliser un CSP nonce. Appliquez ce nonce au script reCAPTCHA Enterprise. reCAPTCHA prend également en charge strict-dynamic dans les navigateurs compatibles. Consultez la FAQ et la documentation CSP de reCAPTCHA Enterprise.

Si votre politique CSP utilise des listes d'autorisation d'hôtes (host allowlists) et que l'intégration charge le script depuis www.recaptcha.net, autorisez les sources nécessaires pour les scripts, les cadres (frames) et les connexions.

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/;

Testez votre politique CSP dans tous les navigateurs pris en charge et surveillez les rapports de violation CSP.


Migration depuis Fingerprint

Supprimez l'intégration Fingerprint avant d'activer l'application de reCAPTCHA sur votre Checkout.

  • Supprimez @fingerprint/agent ainsi que la configuration des points de terminaison Fingerprint.

  • N'utilisez plus fingerprintPublicKey, fingerprintLinkedId ni fingerprintReceived.

  • Supprimez le polling Fingerprint, les callbacks, le code navigateur lié aux webhooks et les traitements en arrière-plan.

  • Ne construisez plus d'objets de requête fraudAssessment.

  • Supprimez les générations périodiques de jetons reCAPTCHA en arrière-plan ainsi que les anciens noms d'action.

  • N'envoyez jamais la siteKey dans le corps des requêtes API ; envoyez uniquement le jeton généré.

‼️ IMPORTANT

Pendant la période de migration, il est possible que d'anciens champs Fingerprint ou fraudAssessment apparaissent encore dans les réponses de l'API. Les nouvelles intégrations doivent les ignorer et ne doivent pas les utiliser pour déterminer si une évaluation du paiement a été effectuée.

Liste de vérification

Avant d'activer l'application de reCAPTCHA sur votre Checkout, vérifiez que :

  • l'intégration utilise https://checkout-api.ventrata.com/octo et conserve les identifiants (credentials) ;

  • la configuration du Checkout renvoie recaptchaEnterpriseSiteKey ;

  • le script Enterprise n'est chargé qu'une seule fois et execute() attend grecaptcha.enterprise.ready() ;

  • le chargeur attend bien grecaptcha.enterprise et ne s'appuie pas uniquement sur l'événement de chargement (load) du script ;

  • une véritable erreur de chargement autorise un nouveau chargement ultérieur, tandis qu'un simple délai dépassé conserve le script pour une réutilisation ultérieure ;

  • les noms des actions sont en minuscules et correspondent exactement à cart_add, purchase ou login ;

  • seules les requêtes de création utilisent cart_add ; les mises à jour et nouvelles réservations ne l'utilisent pas ;

  • chaque requête contenant cardPayment reçoit un nouveau jeton purchase imbriqué ;

  • les requêtes de création contenant cardPayment incluent bien deux jetons distincts ;

  • les jetons login ne sont réutilisés qu'au sein d'un même flux d'identification ;

  • RECAPTCHA_REQUIRED est détecté via errorCode et les nouvelles tentatives s'arrêtent après trois appels API au maximum ;

  • les échecs de génération de jetons sont enregistrés sans consigner les jetons, les en-têtes d'autorisation ou les données clients ;

  • aucune requête ne dépend des champs fraudAssessment ou Fingerprint.

Avez-vous trouvé la réponse à votre question ?