CAPTCHA Integration Guide
This guide explains how to integrate the browser challenge CAPTCHA into your own website to protect against bots and automated abuse.
Challenge Modes
The CAPTCHA can run in one of two modes:
- Interactive (default): The widget shows a checkbox that the user must click to verify.
- Managed: The widget verifies the browser in the background and only falls back to a click challenge if the browser is marked as suspicious. This means the challenge can often be solved without any user interaction.
Because a managed challenge can be solved without a click, its passage
token must be verified with "mode": "managed" during
server-side verification.
Otherwise, the token will be rejected. See
Server-Side Verification for
details.
Client-Side Integration
1. Add the Challenge Script
Include the main challenge script in your HTML page. It is recommended
to place this in the <head> with the
defer attribute or at the end of the
<body>.
<script src="https://challenge.xewdy.systems/static/js/main.js" defer></script>
You can also add the following links to your <head>
to improve performance:
<link rel="preconnect" href="https://challenge.xewdy.systems" crossorigin>
<link rel="dns-prefetch" href="//challenge.xewdy.systems">
Script Options
By default, the script automatically renders a widget into the
challenge-container element (see below) as soon as the page
loads. You can adjust this behavior with the following options, each of
which can be set as a query parameter on the script's
src URL or as a data-* attribute on the script
tag. When both are present, the query parameter takes precedence.
| Query Parameter | Attribute | Description |
|---|---|---|
render |
data-render |
Set to explicit to disable automatic rendering so
you can create and control widgets yourself with the
JavaScript API.
|
onload |
data-onload |
The name of a global function to call once the script has loaded
and the passage API is available.
|
Both options can be set on the script's URL:
<script src="https://challenge.xewdy.systems/static/js/main.js?render=explicit&onload=onPassageReady" defer></script>
Or as data attributes on the script tag:
<script src="https://challenge.xewdy.systems/static/js/main.js" data-render="explicit" data-onload="onPassageReady" defer></script>
If no onload function is specified, the script will call a
global onloadPassageCallback function if one is defined.
2. Add the Challenge Container
Add a div element with the ID
challenge-container where you want the CAPTCHA widget to
appear. To run the challenge in managed mode, add a
data-mode="managed" attribute.
<div id="challenge-container" data-mode="managed"></div>
Customization Options
You can customize the appearance and behavior of the CAPTCHA widget
using data attributes on the challenge-container element.
| Attribute | Description |
|---|---|
data-mode |
The challenge mode: interactive (default) or
managed.
|
data-width |
Sets the width of the widget. Accepts a number (interpreted as
pixels) or any valid CSS width value. Defaults to
400px, and is capped to the width of its container.
|
data-color |
Sets the background color of the widget. Accepts any valid CSS color value (e.g., hex, rgb, color name). |
data-text-color |
Sets the text color of the widget. Accepts any valid CSS color value (e.g., hex, rgb, color name). |
data-unsupported-message |
Overrides the message shown when the browser doesn't support WebAssembly. |
<div id="challenge-container" data-color="#2a2a2a" data-text-color="#ffffff"></div>
3. Handling the Passage Token
When the user successfully passes the challenge, the script provides a
passage token. By default, the script will create a
hidden input field named passage_token inside the challenge
container and set its value to the token.
You can optionally define your own input element to receive the token by
giving it the ID passage-token. This is useful if you want
to integrate it into an existing form structure.
<input type="hidden" id="passage-token" name="my_custom_token_name">
4. Client-Side Callbacks
When the widget is rendered automatically, you can define a global
onChallengeComplete function to handle challenge results
programmatically. On success, it receives the passage token; on failure,
it receives an Error object and an error code.
window.onChallengeComplete = function(success, token, error, errorCode) {
if (success) {
console.log(`Challenge passed! Passage token: ${token}`);
} else {
console.error(`Challenge failed: ${error.message} (Code: ${errorCode})`);
}
};
You can also define a global onClickChallengeRequired
function, which is called when a managed challenge escalates and asks
the user to click the checkbox to continue.
window.onClickChallengeRequired = function() {
console.log("Please click the checkbox to continue");
};
Both functions can be asynchronous if you need to perform async operations. For finer-grained control, see the JavaScript API below.
JavaScript API
For more advanced use cases, the script exposes a global
passage object that lets you render and manage widgets
programmatically. This pairs well with
data-render="explicit" on the script tag, which disables
automatic rendering.
Methods
| Method | Description |
|---|---|
passage.render(container, params) |
Renders a widget into the given element (an element or a selector/ID string) and starts the challenge. Returns a widget ID. |
passage.execute(container, params) |
Renders a widget and runs the challenge on demand, returning a promise that resolves with the passage token. |
passage.reset(widgetId) |
Resets the widget and runs a fresh challenge. |
passage.remove(widgetId) |
Removes the widget and restores the container. |
passage.getResponse(widgetId) |
Returns the current passage token, or undefined if
the challenge hasn't been solved.
|
passage.isExpired(widgetId) |
Returns true if the widget's token has expired.
|
The widgetId argument is optional; when omitted, these
methods act on the most recently rendered widget.
Parameters
The params object accepts the following options, which
mirror the container's data attributes:
| Option | Description |
|---|---|
mode |
The challenge mode: interactive (default) or
managed.
|
width |
Sets the width of the widget (a number is treated as pixels).
Defaults to 400px.
|
color |
Sets the background color of the widget. |
textColor |
Sets the text color of the widget. |
callback |
Function called with the passage token when the challenge is solved. |
errorCallback |
Function called with an Error object and an error
code when the challenge fails.
|
expiredCallback |
Function called when the passage token expires. |
clickCallback |
Function called when a managed challenge escalates to requiring a user click. |
responseField |
Whether to create and populate the hidden token input. Defaults
to true.
|
responseFieldName |
The name of the hidden input field. Defaults to
passage_token.
|
refreshExpired |
auto (default) re-runs the challenge when the token
expires; manual leaves it expired.
|
retry |
auto (default) retries automatically on network or
load errors; manual rejects instead.
|
retryInterval |
Milliseconds to wait between automatic retries. Defaults to
8000.
|
unsupportedMessage |
The message shown when the browser doesn't support WebAssembly. |
Example
window.onloadPassageCallback = () => {
const widgetId = passage.render("#challenge-container", {
mode: "managed",
callback: (token) => {
console.log(`Challenge passed! Passage token: ${token}`);
},
errorCallback: (error, errorCode) => {
console.error(`Challenge failed: ${error.message} (Code: ${errorCode})`);
},
});
};
Alternatively, use passage.execute to run the challenge on
demand and await the token directly.
const token = await passage.execute("#challenge-container", { mode: "managed" });
Server-Side Verification
Once the user submits the form with the passage token, you need to verify it on your server by making an API call to the verification endpoint.
API Endpoint
POST
https://challenge.xewdy.systems/capi/captcha/verify-token
Request Payload
Send a JSON object with the following fields:
| Field Name | Required | Description |
|---|---|---|
passage_token |
Yes | The token received from the client. |
ip_address |
No | The IP address of the user. It's usually not recommended to provide this unless you need very strict verification. |
user_agent |
No | The User-Agent string of the user's browser. It's recommended to include this for better verification. |
mode |
No |
Set to managed if the token was obtained using the
managed challenge mode. This is required to verify tokens that
were solved without a user click.
|
Example Request
{
"passage_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
"ip_address": "192.168.1.1",
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) ..."
}
Response
The API returns a JSON response indicating whether the verification was successful.
Success Response (200 OK):
{
"success": true
}
Error Response (400 Bad Request):
{
"code": 322,
"error": "Invalid or expired passage token",
"success": false
}
Possible Error Codes
| Code | Message |
|---|---|
| 62660 | IP address mismatch |
| 28685 | Duplicate passage token |
| 14260 | User agent mismatch |
| 60813 | Challenge was not solved in the correct mode |
| 322 | Invalid or expired passage token |
| 259 | Internal server error |