Client integration guide

Copy-paste snippets to integrate the CAPTCHA client on any web form.
v1.0

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)

PyxCaptcha.getToken(params)

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
})
PyxChallenge.render(selector, params)

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
})
PyxChallenge.getResponse(widgetId)

Get the token from a completed challenge. Returns empty string if not yet solved.

const token = PyxChallenge.getResponse(widgetId)
PyxChallenge.reset(widgetId, reload?)

Reset widget to initial state. Pass reload=true to load a fresh challenge.

PyxChallenge.reset(widgetId);      // Reset state
PyxChallenge.reset(widgetId, true); // Reset + reload
PyxChallenge.destroy(widgetId)

Remove widget and clean up all event handlers. Call when removing the widget from the DOM.

PyxChallenge.destroy(widgetId)

Server-side API (REST)

POST /captcha/token

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/math/request
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
}
POST /challenge/token

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>"
}
POST /captcha/verify

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"
}
GET /client/script

Fetch the signal-based client script with integrity hash.

// Query parameters
?site=<SITE_KEY>

// Response
{
  "script": "...(javascript source)...",
  "hash": "abc123..." // SHA-256 hex digest
}
GET /client/challenge/script

Fetch the challenge widget script with integrity hash.

// Query parameters
?site=<SITE_KEY>

// Response
{
  "script": "...(javascript source)...",
  "hash": "abc123..." // SHA-256 hex digest
}