API Reference

Developer Documentation

Everything you need to integrate Captxa — from the three-line HTML snippet to raw API contracts, Ed25519 token verification, and language examples.

Base URL: https://api.captxa.com TLS 1.2 / 1.3 only Content-Type: application/json

Quick Start

Three steps to a fully working integration. Total HTML change: one <script> tag and one empty <div>.

1

Add the script + mount point to your HTML

index.html
<!-- Add before </body> — ~3 KB, zero external dependencies -->
<script src="https://cdn.jsdelivr.net/gh/HelloCaptxa/PublicJS@latest/script.js"></script>

<!-- Widget mount-point — place it inside your <form> -->
<div id="captcha-widget"></div>
2

Render the widget once the DOM is ready

app.js
Captxa.render('captcha-widget', {
  serverUrl:      'https://api.captxa.com',
  form:           '#my-form',       // CSS selector — # required
  tokenFieldName: 'captchatoken',   // name attr of the injected hidden <input>
  onVerify: () => {
    // PoW (or puzzle) passed — safe to enable your submit button
    document.getElementById('submit-btn').disabled = false;
  },
  onError: () => {
    // Verification error — ask the user to refresh
    console.error('Captxa verification failed');
  }
});

// On submit, the token is already in the hidden field — just read it
document.getElementById('my-form').addEventListener('submit', async e => {
  e.preventDefault();
  const token = e.target.querySelector('input[name="captchatoken"]')?.value;
  await fetch('/api/my-action', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ captchatoken: token /* + other fields */ })
  });
});
3

Validate the token on your backend — never from the browser

server.js (Node.js)
const res  = await fetch('https://api.captxa.com/api/validate', {
  method:  'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    captcha_token: req.body.captchatoken,
    secret_key:    process.env.CAPTXA_SECRET_KEY
  })
});
const data = await res.json();

if (!data.Is_Correct) {
  return reply.status(403).json({ error: 'Verification failed' });
}
// data.requests tells you how many times this token has been used

Widget API — Captxa.render()

The widget is a push model: call render() once and it handles everything — PoW in a Web Worker, puzzle UI if triggered, and automatic token injection into your form.

Option Type Required Description
serverUrl string required Base URL of the Captxa API. Always https://api.captxa.com
form string required CSS selector of the form to protect. Must include the # prefix for IDs. Example: '#login-form'
tokenFieldName string optional Name attribute of the hidden <input> injected into the form. Default: captcha_token
onVerify () => void optional Callback fired when verification passes (PoW solved or puzzle completed). Use this to enable your submit button.
onError () => void optional Callback fired when the widget encounters a network or verification error. Prompt the user to refresh.

How the widget works internally

  1. Collects browser fingerprint (WebGL, screen dims, timezone, concurrency, memory, automation signals)
  2. POSTs fingerprint to /challenge/simp — receives encrypted challenge token + PoW target
  3. Spawns a Web Worker that grinds SHA-256 nonces until 18 leading zero bits are found (<50 ms typical)
  4. If server returns Do_complex_captcha, fetches puzzle from GET /challenge/complex and shows the slider UI instead
  5. POSTs solution (+ drag trajectory) to /solve/simp or /solve/complex
  6. On success, injects the Ed25519 pass token from the x-captcha-token response header into a hidden <input name="captchatoken"> and fires onVerify
POST

/api/validate

Server-to-server only. Verify a pass token your frontend received and count its usages. Never expose your secret_key to the browser.

Request body

JSON
{
  "captcha_token": "<Ed25519 pass token>",
  "secret_key":    "<your secret key>"
}

HTTP 200 — valid

{ "Is_Correct":true, "RequestLimit":false, "requests":1 }

HTTP 403 — invalid / tampered

{ "Is_Correct":false, "reason":"invalid_token" }

HTTP 429 — token reused beyond limit

{ "Is_Correct":true, "RequestLimit":true, "requests":12 }
Field Type Meaning
Is_Correct boolean Whether the token is cryptographically valid and not expired. Your primary check.
RequestLimit boolean true when the token has been validated too many times — treat this the same as a failure (HTTP 429).
requests uint32 Total number of times this token has been submitted to /api/validate. Normally 1.
reason string? Present only on failure. Value: "invalid_token".

Ed25519 Public Key

Every pass token issued by the server is signed with an Ed25519 keypair. The public key is published at a stable URL so you can verify tokens locally — without a round-trip to /api/validate. This is useful for high-throughput backends or edge deployments.

https://api.captxa.com/keys/Ed25519.txt

The file contains the raw Base64-encoded 32-byte Ed25519 public key. Fetch it once and cache it — the key only changes if the server is re-provisioned, in which case we will post advance notice.

Pass token wire format

The token returned in the x-captcha-token header (and stored in the hidden form field) is a base64url-encoded blob with the following layout:

signature

64 bytes

Ed25519 sig over payload

payload

variable

JSON: domain, ts, score, mobile

Base64url(signature[64] ‖ payload) — split at byte 64 after decoding.

Self-verification (skip the API call)

If you need maximum throughput or zero external dependencies at validation time, verify the Ed25519 signature locally. Fetch the public key once at startup and cache it in memory.

verify.js — Node.js (built-in crypto)
import { createPublicKey, verify } from 'node:crypto';

// Fetch once at startup and cache — key is stable between deploys
const PUB_KEY_URL = 'https://api.captxa.com/keys/Ed25519.txt';
let pubKey;

async function loadPublicKey() {
  const b64 = await (await fetch(PUB_KEY_URL)).text();
  const raw = Buffer.from(b64.trim(), 'base64');         // 32 bytes
  pubKey = createPublicKey({ key: raw, format: 'der', type: 'spki' });
  // Or construct the DER envelope manually for raw Ed25519 bytes:
  // const der = Buffer.concat([Buffer.from('302a300506032b6570032100','hex'), raw]);
  // pubKey = createPublicKey({ key: der, format: 'der', type: 'spki' });
}

function verifyCaptchaToken(tokenB64url) {
  const raw = Buffer.from(tokenB64url, 'base64url');
  const sig     = raw.subarray(0, 64);         // first 64 bytes
  const payload = raw.subarray(64);            // remainder
  return verify(null, payload, pubKey, sig);
}

⚠ When to prefer /api/validate instead

Self-verification skips the request-count check — it cannot detect token replay at the API level. If you need to enforce single-use tokens, call /api/validate which increments a server-side counter and returns HTTP 429 on overuse.

Full API Reference

The endpoints below are used internally by the JS widget. You typically do not call them directly — they are documented here for transparency and for developers building custom integrations.

POST /challenge/simp Step 1 of simple path

Submit a browser fingerprint. The server runs triage (IP bloom, JA4 match, rate-limiter, bot-detector). On pass, returns an encrypted challenge token + PoW target. On fail, returns Do_complex_captcha to escalate.

Request body

{
  "webglrenderer":          "NVIDIA RTX 4090/PCIe",
  "timezone":               "Europe/Barcelona",
  "hardwareconcurrency":    16,
  "innerw": 1920,  "innerh": 1080,
  "availw": 1920,  "availh": 1040,
  "devicememory":           8,
  "webdriver":              false,
  "ischromeruntimemissing": false,
  "errorstacktripwire":     false
}

200 response

{
  "challenge_token": "<base64url>",
  "pow_challenge":   "a3f81c...<32 hex chars>",
  "pow_difficulty":  18
}

// 403 escalation signal:
{ "valid":false, "error":"Do_complex_captcha" }
POST /solve/simp Step 2 of simple path

Submit the PoW solution and mouse trajectory. Server decrypts and authenticates the token, verifies IP + JA4 binding, checks Bloom replay filter, validates PoW, and runs trajectory analysis (bot score must be < 0.30).

Request body

{
  "challenge_token": "<token from /challenge/simp>",
  "pow_solution":    3471829,
  "trajectory": [
    [120, 340, 0],
    [124, 341, 16],
    // ... [x, y, timestamp_ms]
  ]
}

200 response + header

// Response header (pass token):
x-captcha-token: <Ed25519 signed token>

// Response body:
{
  "valid":  true,
  "score":  0.08,
  "mobile": false
}
GET /challenge/complex Step 1 of complex path

Returns a randomised sliding puzzle image (background + piece as base64 JPEG/PNG), a 19-bit PoW challenge, and an encrypted token that embeds the correct solution coordinates. The client never learns the answer — it is sealed inside the token.

200 response
{
  "challenge_token": "<base64url — contains COMP|ip|ja4|ts|pow|sol_x|sol_y>",
  "pow_challenge":   "b7e3a2...<32 hex chars>",
  "pow_difficulty":  19,
  "puzzle": {
    "background":   "<base64 JPEG — full puzzle image>",
    "piece":        "<base64 PNG — draggable piece with alpha>",
    "piece_start_x": 0,
    "width":         300,
    "height":        150,
    "piece_size":    50
  }
}
POST /solve/complex Step 2 of complex path

Submit the PoW solution, the pixel position the user dragged the puzzle piece to, and the full drag trajectory. Server verifies the solution within ±7 px tolerance and runs trajectory ML analysis (bot score must be < 0.50).

Request body

{
  "challenge_token": "<token from /challenge/complex>",
  "pow_solution":    7294013,
  "puzzle_x":        187,   // pixels from left
  "puzzle_y":        62,    // pixels from top
  "trajectory": [
    [0, 62, 0], [45, 63, 33],
    // ... [x, y, timestamp_ms]
  ]
}

200 response + header

// Response header:
x-captcha-token: <Ed25519 signed token>

// Response body:
{
  "valid":  true,
  "score":  0.21,
  "mobile": false
}
GET /api/stats Dashboard

Returns aggregated verification statistics for your registered domain. Used by the Captxa dashboard. Requires authentication via your secret key.

Error Codes

All error responses share the shape {"valid": false, "error": "<code>"} and are returned with HTTP 403. Codes are fixed strings — safe to match programmatically.

error string Endpoint Meaning
Do_complex_captcha /challenge/simp Triage failed — client is redirected to the complex path. Never logged as a hard block.
environment_inconsistency /solve/simp, /solve/complex Browser environment signals indicate a non-human environment (webdriver, missing Chrome runtime, error stack tripwire).
integrity_filters /solve/* Trajectory too short, static, or zero-duration.
burstiness_failed /solve/* Temporal pattern of mouse events indicates scripted injection.
sample_entropy_failed /solve/* Velocity signal lacks the complexity of natural human movement.
fitts_law_failed /solve/* Movement does not conform to Fitts' psychomotor law.
velocity_check_failed /solve/* Velocity coefficient-of-variation too low (constant-speed bot movement).
bot_score_exceeded /solve/* Aggregated bot score ≥ 0.30 (simple) or ≥ 0.50 (complex).
trajectory_too_short /solve/complex Drag trajectory has fewer points than the required minimum.
invalid_token /solve/* Token MAC verification failed — token is malformed or tampered.
token_expired /solve/* Token age exceeds 180 seconds, or timestamp is in the future.
ip_mismatch /solve/* Client IP differs from the IP bound into the challenge token.
ja4_mismatch /solve/* JA4 or JA4o TLS fingerprint differs from the challenge-time value.
pow_failed /solve/* SHA-256(challenge ‖ nonce) does not have the required leading zero bits.
puzzle_wrong /solve/complex Submitted puzzle position deviates more than ±7 px from the correct answer.
wrong_token_type /solve/* Token type prefix mismatch (e.g. a COMP token sent to /solve/simp).
final_features_failed /solve/* Combined feature vector check failed during trajectory ML pipeline.

Validate — More Language Examples

All examples call POST /api/validate with a JSON body. Swap in your framework's HTTP client.

Node.js
async function validateCaptcha(token) {
  const res  = await fetch('https://api.captxa.com/api/validate', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      captcha_token: token,
      secret_key:    process.env.CAPTXA_SECRET_KEY
    })
  });
  if (res.status === 429) return false;   // over request limit
  const data = await res.json();
  return data.Is_Correct === true;
}

Rate Limits & Limits

All limits apply per registered domain. The CAPTCHA endpoints are protected by a Count-Min Sketch keyed on ip | ja4 | ja4o | domain — exceeding it silently escalates to the complex path rather than hard-blocking.

Endpoint Limit Behaviour on exceed
/challenge/simp, /solve/simp CMS per-key threshold Silent escalation to complex path (HTTP 403 Do_complex_captcha)
/api/validate Per-token call count HTTP 429 with RequestLimit: true — treat as failure
Request body size 8 KB (/challenge), 128 KB (/solve) HTTP 400 bad_request
Challenge TTL 180 seconds HTTP 403 token_expired
Bloom filter reset Every 1 hour Replay-prevention state cleared; old solved tokens can be replayed only within the same hour window

Monthly verification limits by plan are listed on the pricing page. Questions? hello@captxa.com