API Integration
If one of our pre-built integration modules aren't suitable, you can write your own by using with the VerifiedVisitors API directly.
This API returns an access control decision for a given request. Each visitor is treated according to its risk and the policies you've defined in VerifiedVisitors. For example, risky behaviour such as ATO can be hard blocked, or served CAPTCHA first to prevent false positives.
All access control decisions can be viewed and audited in the VerifiedVisitors command & control view.
Overview
Server-side integrations work in conjunction with the required JS agent to display challenge pages when necessary.
The integration sits between the visitor and the origin (e.g, in a proxy or app authentication middleware), forwards parts of the request to VerifiedVisitors, and enforces any access control decisions by forwarding headers and body payloads to the client for the JS agent to use. Here's a diagram outlining some integration strategies.
VerifiedVisitors assigns a unique visitor ID to each visitor. This should be persisted using a cookie, and is read by the JS agent to correctly display challenges and associate client-side telemetry with the visitor.
Remember, automation/bot detection isn't always immediate, and a visitor may be issued a mitigation any time after they've landed on the page, including for API calls.
Programatic calls are monitored by the JS agent in order to automatically display challenge pages for them. This behaviour can be configured. See JS Agent for more info.
Server-side integration
Server-side integrations intercept visitor requests and call the VerifiedVisitors API to make access control decisions. It should sit between the visitor and the protected resource.
For a working example, see our CloudFront Lambda or Cloudflare Worker implementations.
Request processing overview
To protect your site, the server-side module should be installed into an appropriate point in the request lifecycle, and perform the following steps:
- Read the visitor-id value from the cookie if present
- Package information from the request to send to the VerifiedVisitors API
- Query the API and parse the response for an access-control decision
- Set any VerifiedVisitors headers and/or pass-through any content from the API
- Set the visitor-id cookie in the response headers if necessary
- Send the mitigation response or allow the request through
The visitor ID cookie
In order for the JS agent to work properly and display challenge pages, the
visitor ID given by the VerifiedVisitors API must be set in a cookie named
vv_vid
for every request:
`set-cookie: vv_vid=VISITOR_ID; path=/; SameSite=Lax; domain=ROOT_DOMAIN; expires=EXPIRY_DATE`.
The HttpOnly
option must not be set on the cookie in order for the
client-side JavaScript agent to read it.
Querying the access control API
The access control API spec can be found here and is accessible at the
following URL: https://api.verifiedvisitors.com/vac/verify
.
We recommend including as many of the fields as possible, but the following minimal example is enough to get started:
curl -XPOST https://api.verifiedvisitors.com/vac/verify \
-H 'Content-Type: application/json' \
-H 'Authorization: bearer API_TOKEN' \
-d @- <<'EOF'
{
"timestamp": 1683643245768,
"visitorId": {
"vid": "9a20079bc4597fce683c583c18797156",
"ip": "203.0.113.0",
"ua": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36"
},
"host": "your-website.com",
"uri": "https://your-website.com/example?param=value",
"method": "GET",
}
EOF
{
"action": "captcha",
"visitorId": "9a20079bc4597fce683c583c18797156",
"rootDomain": "your-website.com"
"countryCode": "US",
"htmlBody": "<html>..."
}
A more detailed example can be found in the spec.
The host
value is used to associate requests with a site in VerifiedVisitors.
This value must match the domain name specified when setting up a site.
API calls are authenticated via HTTP bearer auth:
Authorization: bearer API_TOKEN
. Tokens can be generated from the
VerifiedVisitors
profile settings page.
When querying the API, the module should include as much of the requested information as possible in order to improve threat detection and visitor categorisation in VerifiedVisitors.
This information includes the IP, UA, non-sensitive header values as well as a list of included headers, and the URL. We recommend including the querystring portion with the URL, though this is optional. The VisitorID from the cookie must be included with the request if one exists.
Response include the access control decision (allow
, captcha
,
js_challenge
, or block
), as well as a visitor ID and cookie domain (if any).
A htmlBody
field is also included for use when responding with a mitigation to
HTML GET requests.
Your integration should account for cases where the API unexpectedly returns a non-OK status code or takes too long to respond. To avoid disruption, we recommend failing-open in these cases and setting a timeout of around 1 second.
Returning a response and enforcing mitigations
Allow decisions should pass-through the request to the origin or next step in the request lifecycle. Allowed requests must still ensure that the visitor ID cookie is set.
For mitigating decisions, the integration should return an error code (e.g. 403)
to block the request. Challenge responses must additionally set the vv-action
header to the appropriate action (i.e. vv-action: captcha
or
vv-action: js_challenge
).
The API includes a htmlBody
field with challenge actions containing a HTML
page with the JS agent loaded and configured to display the challenge
immediately. This should be included in the response to the client for GET
requests to HTML resources.
Challenges are presented by the JS agent within an iframe. For this to work, the
JS agent must be loaded in the client's browser, and the server-side integration
must include a vv-action
header in the response.
legacy/in-line challenge presentation
Challenges may be served directly by the server-side integration without requiring the JS agent.
This approach has been deprecated in favour of the JS agent driven iframe approach, but is included here for legacy reasons.
To serve and process challenges, the integration must send a HTML response with the embedded challenge, and forward any responses for the challenge to the API.
- A
captcha
decision is returned from the API, including the hCaptcha key - The integration returns the prepared challenge page along with the key
- The visitor completes the CAPTCHA, sending the result back to the origin with the CAPTCHA token and visitor ID
- The integration parses and extracts the CAPTCHA token from the request and
includes it in the call to the API in the
hCaptchaToken
property. - At this point, the CAPTCHA result is associated with the Visitor ID and if
the attempt was successful, further requests should yield
allow
decisions. - The CAPTCHA page reloads or navigates to allow the visitor to continue
View a sequence diagram of this CAPTCHA flow.
Here's an exmaple of a CAPTCHA challenge page templated using JavaScript:
const siteKey = 'RETURNED_FROM_ACCCESS_CONTROL_API'
/* This should be read from subsequent requests and sent to the access control API */
const captchaResponseField = 'h-captcha-response'
/* The action to take upon completing the CAPTCHA. For POST requests we navigate
backwards so that the user can re-attempt the form. */
const onComplete =
originalMethod === 'POST'
? 'history.go(-1)'
: 'location.href = location.search'
const html = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex, nofollow">
<title>Verify</title>
<script
src="https://hcaptcha.com/1/api.js?recaptchacompat=off"
async defer
></script>
<style>
body {
margin: 0;
font-family: sans-serif;
}
.container {
margin-top: 200px;
display: flex;
flex-direction: column;
align-items: center;
gap: 45px;
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>We've detected something unusual about your visit.</h1>
<p>Please complete the CAPTCHA to continue.</p>
</header>
<form id="challenge-form" action="" method="POST">
<div
class="h-captcha"
data-sitekey="${siteKey}"
data-callback="onComplete"
></div>
</form>
<script>
async function onComplete(response) {
const body = new URLSearchParams({
'${captchaResponseField}': response
})
const r = await fetch('/', {
method: 'POST',
body,
})
${onComplete}
}
</script>
</div>
</body>
</html>`