1) Get your keys
You need a site key (public) and a secret key (server-side). Ask your admin to provision a webapp in the dashboard.
2) Load the client script
Choose the flow you want: signal-based (challenge-less) or challenge-based.
2A) Signal-based (Challenge-less)
Loads an invisible script that collects behavioral signals and issues tokens on demand. No user interaction required beyond form submission.
<!-- Vanilla JS -->
<script>
(async () => {
const siteKey = "<SITE_KEY>";
const res = await fetch(`/client/script?site=${siteKey}`);
const data = await res.json();
const enc = new TextEncoder();
const buf = await crypto.subtle.digest("SHA-256", enc.encode(data.script));
const hash = Array.from(new Uint8Array(buf)).map((b) => b.toString(16).padStart(2, "0")).join("");
if (hash !== data.hash) throw new Error("Client script integrity check failed");
const scriptEl = document.createElement("script");
scriptEl.text = data.script;
document.head.appendChild(scriptEl);
})();
</script>
// React (useEffect)
import { useEffect } from "react";
export function usePyxCaptcha(siteKey) {
useEffect(() => {
let mounted = true;
(async () => {
const res = await fetch(`/client/script?site=${siteKey}`);
const data = await res.json();
const enc = new TextEncoder();
const buf = await crypto.subtle.digest("SHA-256", enc.encode(data.script));
const hash = Array.from(new Uint8Array(buf)).map((b) => b.toString(16).padStart(2, "0")).join("");
if (hash !== data.hash) throw new Error("Client script integrity check failed");
if (!mounted) return;
const scriptEl = document.createElement("script");
scriptEl.text = data.script;
document.head.appendChild(scriptEl);
})();
return () => {
mounted = false;
};
}, [siteKey]);
}
// Angular (component)
import { Component, OnInit } from "@angular/core";
@Component({ selector: "app-root", template: "<ng-content></ng-content>" })
export class AppComponent implements OnInit {
async ngOnInit() {
const siteKey = "<SITE_KEY>";
const res = await fetch(`/client/script?site=${siteKey}`);
const data = await res.json();
const enc = new TextEncoder();
const buf = await crypto.subtle.digest("SHA-256", enc.encode(data.script));
const hash = Array.from(new Uint8Array(buf)).map((b) => b.toString(16).padStart(2, "0")).join("");
if (hash !== data.hash) throw new Error("Client script integrity check failed");
const scriptEl = document.createElement("script");
scriptEl.text = data.script;
document.head.appendChild(scriptEl);
}
}
2B) Challenge-based
Load the challenge widget script. Supports multiple challenge types: math (mathematical equations), image (character recognition), and pictures (image selection).
<!-- Vanilla JS -->
<script>
(async () => {
const siteKey = "<SITE_KEY>";
const res = await fetch(`/client/challenge/script?site=${siteKey}`);
const data = await res.json();
const enc = new TextEncoder();
const buf = await crypto.subtle.digest("SHA-256", enc.encode(data.script));
const hash = Array.from(new Uint8Array(buf)).map((b) => b.toString(16).padStart(2, "0")).join("");
if (hash !== data.hash) throw new Error("Challenge script integrity check failed");
const scriptEl = document.createElement("script");
scriptEl.text = data.script;
document.head.appendChild(scriptEl);
})();
</script>
// React (useEffect)
import { useEffect } from "react";
export function usePyxChallenge(siteKey) {
useEffect(() => {
let mounted = true;
(async () => {
const res = await fetch(`/client/challenge/script?site=${siteKey}`);
const data = await res.json();
const enc = new TextEncoder();
const buf = await crypto.subtle.digest("SHA-256", enc.encode(data.script));
const hash = Array.from(new Uint8Array(buf)).map((b) => b.toString(16).padStart(2, "0")).join("");
if (hash !== data.hash) throw new Error("Challenge script integrity check failed");
if (!mounted) return;
const scriptEl = document.createElement("script");
scriptEl.text = data.script;
document.head.appendChild(scriptEl);
})();
return () => {
mounted = false;
};
}, [siteKey]);
}
// Angular (component)
import { Component, OnInit } from "@angular/core";
@Component({ selector: "app-root", template: "<ng-content></ng-content>" })
export class AppComponent implements OnInit {
async ngOnInit() {
const siteKey = "<SITE_KEY>";
const res = await fetch(`/client/challenge/script?site=${siteKey}`);
const data = await res.json();
const enc = new TextEncoder();
const buf = await crypto.subtle.digest("SHA-256", enc.encode(data.script));
const hash = Array.from(new Uint8Array(buf)).map((b) => b.toString(16).padStart(2, "0")).join("");
if (hash !== data.hash) throw new Error("Challenge script integrity check failed");
const scriptEl = document.createElement("script");
scriptEl.text = data.script;
document.head.appendChild(scriptEl);
}
}
You can also use a direct script tag, but you lose integrity verification.
3) Request a token on submit
Pick the client flow that matches the script you loaded. Pre-fill optional parameters in the config object, or pass them when calling the methods.
3A) Signal-based (Challenge-less)
Call PyxCaptcha.getToken() on form submit. The client collects behavioral signals (mouse movement, keypress timing, interaction patterns) and requests a token from the server without user challenges. Timeout is 10 seconds.
// Minimal usage
const token = await PyxCaptcha.getToken({
site: "<SITE_KEY>",
action: "login"
});
// With response handling
try {
const token = await PyxCaptcha.getToken({
site: "<SITE_KEY>",
action: "login",
serverUrl: "https://captcha.example.com" // optional override
});
await fetch("/auth/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
"pyx-captcha-token": token
},
body: JSON.stringify({ email, password })
});
} catch (error) {
console.error("Captcha error:", error.message);
}
3B) Challenge-based
Render the widget with a specific challenge type, then retrieve the token after the user completes the challenge. Supports callbacks and custom configuration.
<div id="pyx-challenge"></div>
<script>
// Render a challenge widget (type: "math", "image", or "pictures")
const widgetId = PyxChallenge.render("#pyx-challenge", {
site: "<SITE_KEY>",
action: "login",
challengeType: "math", // or "image" or "pictures"
onSuccess: (token, id) => console.log("Challenge solved:", token),
onError: (error, id) => console.error("Challenge error:", error)
});
// On form submit:
document.getElementById("login-form").addEventListener("submit", async (e) => {
e.preventDefault();
const token = PyxChallenge.getResponse(widgetId);
if (!token) {
alert("Please complete the challenge");
return;
}
await fetch("/auth/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
"pyx-captcha-token": token
},
body: JSON.stringify({ email, password })
});
});
</script>
Tokens are short-lived and single-use. Always request a fresh token per submission. For challenge-based
flows, reset the widget with
PyxChallenge.reset(widgetId)
after successful verification to allow another challenge.
4) Verify the token on your backend
Verify the token on your backend server with your secret key. The verify endpoint is only accessible from whitelisted IP addresses. Always verify tokens server-side; never trust client-side validation alone.
curl -X POST https://your-captcha-server.com/captcha/verify \
-H "Content-Type: application/json" \
-d '{
"token": "<ENCRYPTED_JWT>",
"secret": "<SECRET_KEY>",
"action": "login",
"clientIp": "203.0.113.10"
}'
# Response on success:
# {
# "success": true,
# "score": 0.95,
# "action": "login",
# "site": "<SITE_KEY>",
# "timestamp": 1735689600000
# }
# Response on failure:
# {
# "success": false,
# "error": "Invalid token",
# "action": "login"
# }
// Node.js / TypeScript example
import axios from "axios";
async function verifyToken(token, secretKey, clientIp, action) {
try {
const response = await axios.post(
"https://your-captcha-server.com/captcha/verify",
{
token,
secret: secretKey,
action,
clientIp
},
{
headers: { "Content-Type": "application/json" },
timeout: 5000
}
);
if (response.data.success) {
const score = response.data.score; // 0.0 to 1.0
if (score > 0.5) {
// Allow the request
return { allowed: true, score };
} else {
// Trigger additional verification or block
return { allowed: false, reason: "Low trust score", score };
}
}
} catch (error) {
console.error("Token verification failed:", error.message);
return { allowed: false, reason: "Verification error" };
}
}
Important: The verify endpoint requires your server's IP to be whitelisted. Configure IP allowlist in your app settings to prevent unauthorized verification requests.
5) Challenge widget controls (Challenge-based only)
Use these methods to manage the lifecycle of challenge widgets.
// Render a widget
const widgetId = PyxChallenge.render("#pyx-challenge", {
site: "<SITE_KEY>",
action: "login",
challengeType: "math"
});
// Get the token after user completes the challenge
const token = PyxChallenge.getResponse(widgetId);
// Reset the widget to its initial state (clears verification, but keeps the DOM)
// Pass true to also reload a fresh challenge
PyxChallenge.reset(widgetId);
PyxChallenge.reset(widgetId, true); // with fresh challenge
// Destroy the widget completely (removes DOM and cleans up event handlers)
// Call this if you're removing the widget container from the page
PyxChallenge.destroy(widgetId);
Lifecycle example
// Initialize widget
let widgetId = PyxChallenge.render("#pyx-challenge", {
site: "<SITE_KEY>",
action: "login",
challengeType: "image",
onSuccess: (token) => {
console.log("Challenge completed!");
// Handle successful verification
}
});
// On form submit, after successful authentication:
function handleLoginSuccess() {
// Option 1: Keep widget for retry - reset it
PyxChallenge.reset(widgetId);
// Option 2: Remove widget - destroy it
// PyxChallenge.destroy(widgetId);
// widgetId = null;
}
Always call
destroy()
when dynamically removing the challenge widget from the DOM to prevent memory leaks and lingering event
handlers.
6) Customization: Theme, layout & callbacks
Customize the widget appearance and behavior with theme colors, compact mode, and event callbacks.
const widgetId = PyxChallenge.render("#pyx-challenge", {
// Required fields
site: "<SITE_KEY>",
action: "login",
// Challenge type: "math", "image", or "pictures"
challengeType: "pictures",
// Layout: compact (mobile-friendly) or full
compact: true,
// Theme colors (all optional, uses defaults if not specified)
theme: {
// Fonts
fontFamily: '"Space Grotesk", "IBM Plex Sans", sans-serif',
// Backgrounds
surface: "#ffffff",
// Borders
border: "#e5e7eb",
// Text colors
text: "#0f172a",
muted: "#64748b",
primaryText: "#ffffff",
// UI colors
primary: "#0f172a", // Primary button, checkbox
accent: "#2563eb", // Focus states, highlights
success: "#16a34a", // Success messages
error: "#b91c1c", // Error messages
// Advanced
overlay: "rgba(15, 23, 42, 0.45)",
shadow: "0 10px 25px rgba(15, 23, 42, 0.08)",
modalShadow: "0 20px 40px rgba(15, 23, 42, 0.2)"
},
// Optional callbacks
onSuccess: (token, widgetId) => {
console.log("Challenge completed, token:", token);
},
onError: (error, widgetId) => {
console.error("Challenge error:", error.message);
}
});
Global Configuration
Pre-configure defaults globally to avoid repeating options on every widget.
// Set global config before rendering widgets
window.PyxChallengeConfig = {
site: "<SITE_KEY>",
serverUrl: "https://your-captcha-server.com",
challengeType: "math",
theme: {
primary: "#0b1f44",
accent: "#2563eb"
}
};
// Then render with minimal options
const widgetId = PyxChallenge.render("#pyx-challenge", {
action: "register" // Only override what's needed
});
Common pitfalls & troubleshooting
- Use the correct site key: Site keys are environment-specific (dev, staging, prod). Use the key provisioned for your domain to ensure token verification works.
- Verify server-side only: Never trust client-side validation. Always verify tokens on your backend using the secret key and the verify endpoint.
- Match the action string: Use the same action string consistently: client → token → verify. Actions like "login", "register", "purchase" help diagnose issues.
- Never expose the secret key: Store your secret key securely on your backend. Never send it to the client or commit it to version control.
- Include the client IP: Pass the client's IP address in the verify request so PyxCaptcha can validate IP reputation and detect anomalies.
- Handle network timeouts: Signal collection takes up to 500ms and token requests timeout after 10 seconds. Implement retry logic for transient failures.
-
Challenge-based widget cleanup:
If you dynamically remove a challenge widget from the DOM, call
PyxChallenge.destroy(widgetId)to prevent memory leaks and orphaned event listeners. - Single-use tokens: Tokens can only be verified once. Don't retry verification with the same token; request a fresh one.
- IP whitelist for verification: The /captcha/verify endpoint is IP-restricted. Ensure your server's IP is whitelisted in your app settings.
- Browser compatibility: PyxCaptcha requires SubtleCrypto (modern browsers). For legacy browsers, plan accordingly or implement fallback verification.
Advanced: Badge, encryption & signals
Badge configuration
Optionally render a floating "Protected by PyxCaptcha" badge. Configure globally before the script loads.
// Disable badge (enabled by default)
window.PyxCaptchaConfig = {
badge: { enabled: false }
};
// Or customize its position and link
window.PyxCaptchaConfig = {
badge: {
enabled: true,
container: document.getElementById("footer"),
link: "https://pyxcaptcha.example.com/about"
}
};
Signal collection
The signal-based flow automatically collects behavioral signals to assess user legitimacy:
- Mouse movement: Speed, patterns, and trajectory analysis
- Keyboard dynamics: Typing speed and inter-key timing
- Interaction patterns: Click distribution and focus events
- Scroll behavior: Scroll frequency and direction
- Device fingerprint: Browser, OS, and hardware characteristics
- Proof-of-Work: Lightweight computational challenge to detect automation
Signal collection runs for up to 500ms before requesting a token. The server analyzes these signals and returns a trust score (0.0–1.0) along with the token.
Request encryption
If enabled, all requests to PyxCaptcha endpoints are encrypted end-to-end using AES-GCM. This is configured server-side; the client script handles encryption/decryption automatically.
// Encryption is transparent to your code.
// Just use the API normally; the script handles all encryption internally.
const token = await PyxCaptcha.getToken({
site: "<SITE_KEY>",
action: "login"
});
// If encryption is enabled, the request will be encrypted automatically.
API reference
Client-side API (JavaScript)
Request a token using behavioral signals. Returns a Promise that resolves to a token string.
await PyxCaptcha.getToken({
site: string, // Site key (required)
action: string, // Action identifier (required)
serverUrl?: string // Override server URL
})
Render a challenge widget. Returns a widget ID.
PyxChallenge.render(selector, {
site: string, // Site key (required)
action: string, // Action identifier (required)
challengeType?: string, // "math" | "image" | "pictures"
compact?: boolean, // Compact layout
theme?: object, // Theme colors
serverUrl?: string, // Override server URL
onSuccess?: (token, id) => void,
onError?: (error, id) => void
})
Get the token from a completed challenge. Returns empty string if not yet solved.
const token = PyxChallenge.getResponse(widgetId)
Reset widget to initial state. Pass reload=true to load a fresh challenge.
PyxChallenge.reset(widgetId); // Reset state
PyxChallenge.reset(widgetId, true); // Reset + reload
Remove widget and clean up all event handlers. Call when removing the widget from the DOM.
PyxChallenge.destroy(widgetId)
Server-side API (REST)
Issue a token from encrypted signals (signal-based flow).
// Request
{
"key": "...encrypted payload...",
"site": "<SITE_KEY>",
"action": "login",
"signals": { /* browser signals */ }
}
// Response
{
"token": "<JWT>"
}
POST /challenge/image/request
POST /challenge/pictures/request
Request a challenge prompt of the specified type.
// Request
{
"request": "...encrypted payload...",
"site": "<SITE_KEY>",
"action": "login"
}
// Response (encrypted or plain)
{
"challengeId": "uuid",
"prompt": "What is 5 + 3?",
"image": "data:image/png;...",
"selectCount": 1
}
Issue a token after solving a challenge.
// Request
{
"key": "...encrypted payload...",
"challengeId": "uuid",
"answer": "8", // or "8" for math, "image-id-1,image-id-2" for pictures
"site": "<SITE_KEY>",
"action": "login",
"domain": "example.com"
}
// Response
{
"token": "<JWT>"
}
Verify a token on your backend (requires IP whitelist).
// Request
{
"token": "<JWT>",
"secret": "<SECRET_KEY>",
"action": "login",
"clientIp": "203.0.113.10"
}
// Response (success)
{
"success": true,
"score": 0.95,
"action": "login",
"site": "<SITE_KEY>",
"timestamp": 1735689600000
}
// Response (failure)
{
"success": false,
"error": "Invalid token | Expired | Already used",
"action": "login"
}
Fetch the signal-based client script with integrity hash.
// Query parameters
?site=<SITE_KEY>
// Response
{
"script": "...(javascript source)...",
"hash": "abc123..." // SHA-256 hex digest
}
Fetch the challenge widget script with integrity hash.
// Query parameters
?site=<SITE_KEY>
// Response
{
"script": "...(javascript source)...",
"hash": "abc123..." // SHA-256 hex digest
}
v1.0