Developer Documentation
Everything you need to integrate Captxa — from the three-line HTML snippet to raw API contracts, Ed25519 token verification, and language examples.
Quick Start
Three steps to a fully working integration. Total HTML
change: one
<script>
tag and one empty
<div>.
Add the script + mount point to your 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>
Render the widget once the DOM is ready
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 */ })
});
});
Validate the token on your backend — never from the browser
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
- Collects browser fingerprint (WebGL, screen dims, timezone, concurrency, memory, automation signals)
-
POSTs fingerprint to
/challenge/simp— receives encrypted challenge token + PoW target - Spawns a Web Worker that grinds SHA-256 nonces until 18 leading zero bits are found (<50 ms typical)
-
If server returns
Do_complex_captcha, fetches puzzle fromGET /challenge/complexand shows the slider UI instead -
POSTs solution (+ drag trajectory) to
/solve/simpor/solve/complex -
On success, injects the Ed25519 pass token from
the
x-captcha-tokenresponse header into a hidden<input name="captchatoken">and firesonVerify
/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
{
"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.
Public key URL
Open /keys/Ed25519.txtThe 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.
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.
/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" }
/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
}
/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.
{
"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
}
}
/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
}
/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.
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