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:

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