Migrating from Fingerprint
Ventrata is replacing the previous Fingerprint-based integration with Google reCAPTCHA Enterprise. Existing custom checkouts must remove their Fingerprint integration and adopt the reCAPTCHA actions, token locations, and retry behaviour described in this guide.
During the migration period, some legacy Fingerprint and fraudAssessment fields may continue to appear in API responses for compatibility with older clients. New integrations must ignore these fields and must not use them to determine whether fraud assessment has completed.
Google reCAPTCHA Enterprise protects checkout creation and identity lookup flows from automated abuse and provides Transaction Defense assessments for card payments.
Ventrata handles the server-side Google integration. Your custom checkout is responsible for generating browser tokens and placing them in the appropriate API requests.
Integration Overview
Retrieve the public reCAPTCHA site key from Ventrata.
Load the reCAPTCHA Enterprise browser script.
Generate a token immediately before each protected interaction.
Use the action expected by Ventrata.
Put the token in the correct request object.
Retry with a fresh token when
errorCodeisRECAPTCHA_REQUIRED.
📒 NOTE
Fingerprint is no longer required. Do not install the Fingerprint agent, create linked IDs, poll Fingerprint receipt fields, or send data in the legacy fraudAssessment object.
Prerequisites:
Use a Ventrata Checkout token.
This token is the same value as your Checkout ID and is designed for public, client-side use. It restricts access to the endpoints available to that checkout.
const API_URL = "https://checkout-api.ventrata.com/octo";
const CHECKOUT_TOKEN = "YOUR_CHECKOUT_TOKEN";Include the
ventrata/checkoutcapability and any other OCTO capabilities required by the request.Authorization: Bearer YOUR_CHECKOUT_TOKEN
Octo-Capabilities: ventrata/checkoutInclude browser credentials so the Ventrata session cookie is retained:
const commonFetchOptions = {
credentials: "include",
headers: {
Authorization: `Bearer ${CHECKOUT_TOKEN}`,
"Octo-Capabilities": "ventrata/checkout",
},
};
📒 NOTE
The examples use JavaScript and illustrate the request structure. Adapt error handling and service wrappers to your application.
Step 1: Retrieve the reCAPTCHA site key
Request the checkout configuration and read recaptchaEnterpriseSiteKey from the response.
Request the checkout configuration and read recaptchaEnterpriseSiteKey from the response.
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");
}📒 NOTE
The site key is public and safe to use in the browser. Never place a Google secret, service-account credential, or API key in client-side code. Ventrata creates the server-side assessments.
‼️ IMPORTANT
During the migration period, the response also contains fingerprintPublicKey. It is a compatibility placeholder and must be ignored by new integrations.
Step 2: Load reCAPTCHA Enterprise
Load the score-based Enterprise script once, as soon as the configuration provides the site key. Include the site key in the render parameter and wait for grecaptcha.enterprise.ready() before calling execute(). See Google’s guidance for instrumenting web pages with score-based keys.
Load the score-based Enterprise script once, as soon as the configuration provides the site key. Include the site key in the render parameter and wait for grecaptcha.enterprise.ready() before calling execute(). See Google’s guidance for instrumenting web pages with score-based keys.
If you load the script dynamically, allow for the script load event and grecaptcha.enterprise availability to occur at different times. A later attempt should be able to reuse a script that completed after an earlier timeout.
The following helper uses www.recaptcha.net, which Google supports as an alternative for environments that cannot access www.google.com. See the reCAPTCHA Enterprise FAQ.
The following helper uses www.recaptcha.net, which Google supports as an alternative for environments that cannot access www.google.com. See the reCAPTCHA Enterprise FAQ.
It keeps the load and error events as fast paths, polls for readiness until the overall timeout, and preserves a slow script after timeout so a later attempt can adopt it:
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;
}
Step 3: Generate Tokens Just in Time
Generate a token immediately before the interaction it protects. Do not generate tokens continuously in the background or reuse a token across separate requests.
Generate a token immediately before the interaction it protects. Do not generate tokens continuously in the background or reuse a token across separate requests.
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
These three token-execution attempts happen before the Checkout API request. They are separate from, and do not increase, the three-total-request limit described in Step 5.
Tokens expire after two minutes and can normally be assessed only once. Generate a fresh cart_add or purchase token for every request that requires one. The login flow has a limited reuse exception described below.
📗 TIP
For details about token lifetime and usage, see Google’s guidance for retrieving reCAPTCHA tokens.
Step 4: Use Correct Action and Payload Location
Action names are lowercase and must exactly match the value expected by Ventrata. See Google’s action-name guidance.
Action names are lowercase and must exactly match the value expected by Ventrata. See Google’s action-name guidance.
Action | Use it for | Request location |
| Creating a new order, booking, purchase, or gift | Top-level |
| Every request containing | Inside |
| Membership, check-in, concierge login, and identity lookup flows | Top-level |
Create an Order, Booking, Purchase, or Gift
Generate a fresh cart_add token for each create request and place it in the top-level recaptchaEnterprise object:
{
"currency": "USD",
"recaptchaEnterprise": {
"token": "CART_ADD_TOKEN"
}
}📒 NOTE
This applies to POST requests that create orders, bookings, purchases, and gifts. Do not add cart_add to updates or rebooking requests for records that already exist. If you repeat a create request after an error, generate a new token.
Send a Card Payment
Whenever a request contains cardPayment, generate a fresh purchase token for that request and place it directly inside cardPayment.recaptchaEnterprise. This also applies when repeating a request after a gateway refresh or intermediate response.
📒 NOTE
Do not refresh separately. Generate the purchase token as part of the actual payment request. Do not perform an extra order update only to refresh the token.
{
"currency": "USD",
"cardPayment": {
"gateway": "adyen",
"adyen": {
"sessionId": "ADYEN_SESSION_ID",
"sessionResult": "ADYEN_SESSION_RESULT"
},
"recaptchaEnterprise": {
"token": "PURCHASE_TOKEN"
}
}
}Use
purchasefor order and booking updates or confirmations that containcardPayment.Use it in Checkout and Manage My Booking payment flows.
Use it with every supported card gateway.
📒 NOTE
Ventrata requires the purchase action for requests containing cardPayment. For background information about Transaction Defense, see Google’s Transaction Defense guidance
Create an Order with a Card Payment
A create request that also contains cardPayment needs two different tokens:
cart_addat the top levelpurchaseinsidecardPayment.
One token cannot be used for both actions.
{
"currency": "USD",
"recaptchaEnterprise": {
"token": "CART_ADD_TOKEN"
},
"cardPayment": {
"gateway": "adyen",
"recaptchaEnterprise": {
"token": "PURCHASE_TOKEN"
}
}
}
Login and Identity Lookup
Generate a login token for membership, check-in, concierge login, and identity lookup requests.
{
"email": "[email protected]",
"recaptchaEnterprise": {
"token": "LOGIN_TOKEN"
}
}
Within one identity flow, you may retain the token for follow-up requests that use the same normalised email address, mobile number, or booking reference.
Discard it and generate a new token when:
Ventrata returns
RECAPTCHA_REQUIRED.The customer changes the email address, mobile number, country, or booking reference.
A new login or lookup flow begins.
Step 5: Handle RECAPTCHA_REQUIRED
A protected create or identity request can return RECAPTCHA_REQUIRED when its token is missing, expired, already assessed, or affected by a retriable browser error.
A protected create or identity request can return RECAPTCHA_REQUIRED when its token is missing, expired, already assessed, or affected by a retriable browser error.
{
"error": "BAD_REQUEST",
"errorMessage": "A new Google reCAPTCHA token is required",
"errorCode": "RECAPTCHA_REQUIRED",
"reason": "MISSING"
}Reason | Meaning |
| The request omitted the token or sent |
| The token was no longer valid. |
| The token had already been assessed. |
| Google reported a retriable browser-side error. |
Check only errorCode when deciding whether to retry. For every RECAPTCHA_REQUIRED response, discard the previous token and retry with a newly generated token. If token generation fails, send null so Ventrata can return the explicit error, then wait two seconds before trying again. Stop after three total Checkout API attempts.
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);
}
}
}
The following example applies the retry behaviour to a protected order-creation request. Each attempt generates a new cart_add token and places it in the top-level recaptchaEnterprise object.
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 },
}),
}),
);📒 NOTE
Apply the same retry behaviour to other protected creation and identity requests, using the corresponding action and payload location. If a repeated request contains cardPayment, also generate a new nested purchase token for that attempt.
Do not automatically retry:
VERIFICATION_FAILED: the assessment failed for a non-retriable reason, such as a low score or unexpected action.PAYMENT_DECLINE_LIMIT_EXCEEDED: the order has exceeded the permitted number of refused payment attempts.
Content Security Policy
Google recommends using a CSP nonce. Apply the nonce to the Enterprise script. reCAPTCHA also supports strict-dynamic in compatible browsers. See Google’s reCAPTCHA Enterprise FAQ and CSP guidance.
Google recommends using a CSP nonce. Apply the nonce to the Enterprise script. reCAPTCHA also supports strict-dynamic in compatible browsers. See Google’s reCAPTCHA Enterprise FAQ and CSP guidance.
If your policy uses host allowlists instead and the integration loads from www.recaptcha.net, allow the required script, frame, and connection sources:
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/;
Test the policy in every supported browser and monitor CSP violation reports.
Migrating from Fingerprint
Remove the previous Fingerprint integration before enabling reCAPTCHA enforcement for your checkout:
Remove the previous Fingerprint integration before enabling reCAPTCHA enforcement for your checkout:
Remove
@fingerprint/agentand Fingerprint endpoint configuration.Stop using
fingerprintPublicKey,fingerprintLinkedId, andfingerprintReceived.Remove Fingerprint polling, callbacks, webhook-related browser code, and background work.
Stop constructing
fraudAssessmentrequest objects.Remove background reCAPTCHA token intervals and legacy action names.
Do not send
siteKeyin API request bodies; send only the generated token.
‼️ IMPORTANT
You may temporarily see legacy Fingerprint or fraud-assessment fields in API responses. New integrations must ignore them and must not use them as evidence that a payment assessment exists.
Verification Checklist
Before enabling reCAPTCHA enforcement for the checkout, verify that:
The integration uses
https://checkout-api.ventrata.com/octoand retains credentials.Checkout configuration returns
recaptchaEnterpriseSiteKey.The Enterprise script loads once and
execute()waits forgrecaptcha.enterprise.ready().The loader waits for
grecaptcha.enterpriserather than relying only on the script’sloadevent.A real script error permits a later reload, while a timeout preserves the slow script for later adoption.
Actions are lowercase and exactly match
cart_add,purchase, orlogin.Only create requests receive
cart_add; updates and rebooks do not.Every request containing
cardPaymentreceives a fresh nestedpurchasetoken.Create requests containing
cardPaymentcontain two distinct tokens.Login tokens are retained only within the same identity flow.
RECAPTCHA_REQUIREDis identified usingerrorCode, and retry handling stops after three total API attempts.Token-generation failures are recorded without logging tokens, authorisation headers, or customer data.
No request depends on
fraudAssessmentor Fingerprint fields.
