How captchaapi.eu works
captchaapi.eu is a proof-of-work (PoW) captcha. Instead of showing your visitors a puzzle, it asks their browser to do a small amount of CPU work — invisibly, in a Web Worker — before they submit a form. Real users don't notice; bots paying to run thousands of submissions across thousands of IPs do.
The flow, end to end
Each captcha verification has three HTTP calls: two between the visitor's browser and captchaapi.eu, and a final server-to-server verify call from your backend.
Throughout this page, /challenge and /verify are shorthand for
/api/v1/captcha/challenge and /api/v1/captcha/verify. See the
API reference for full request and response shapes.
- The widget asks captchaapi.eu for a challenge. It receives a random 32-character
token, atarget(a 32-bit integer), and an expiry. - The browser searches for a
solution: an integernoncesuch thatSHA-256(token + nonce)— interpreted as hex — has its first 8 characters numerically≤ target. Only brute force works. - The widget injects a single hidden input
captchaapi_responsewith the value"<token>.<solution>"and submits the form. The widget does not call/verify. - Your backend reads
captchaapi_responseand makes one server-to-server POST to/verifywith your secret key in anAuthorization: Bearerheader. - captchaapi.eu re-hashes to confirm the solution, marks the response single-use, and returns
{"success": true|false, ...}. Let the form through only whensuccessistrue.
The challenge token is single-use and stored in Redis with a 2-minute TTL. It's bound to
the requesting IP (hashed, not stored raw), so it can't be solved on one machine and
redeemed from another. Single-use and replay protection are enforced on our side at the
verify call: a given captchaapi_response verifies exactly once.
The verify call
Your backend verifies a submission with one HTTP call. Send the
captchaapi_response field from the form as the response body, with
your project's secret key in the Authorization header:
POST https://captchaapi.eu/api/v1/captcha/verify
Authorization: Bearer sk_live_...
Content-Type: application/json
{"response": "<value of the captchaapi_response form field>"}
The response is a compact JSON object:
| Field | Meaning |
|---|---|
success | true when the response is valid and the proof-of-work satisfied the target. Allow the form through only on true. As with every CAPTCHA provider, HTTP 200 does not mean pass — read this field. |
error_code | null on success. Otherwise invalid_token (unknown, expired, or already-used response), invalid_secret (bad or missing Bearer secret, HTTP 401), or invalid_solution (proof-of-work didn't satisfy the target). |
over_limit | Informational boolean. true when the project is past its monthly quota. success stays true — serve the visitor and treat it as a signal to upgrade. |
Every project has a site key (public, embedded in the browser, format
pk_live_...) and a secret key (private, kept on your server,
format sk_live_...). The secret key is shown once when you create a project,
and is re-revealed later with password confirmation. It authenticates your backend's verify
calls and never leaves your server.
Difficulty curve
A smaller target means fewer qualifying hashes, which means more work for the solver.
The base difficulty rises gently with how often a single IP hits the challenge endpoint — so
a one-off visitor lands at the easiest end of the curve while a scripted attacker hitting the
same endpoint hundreds of times per minute slides toward the hardest single-IP target before
eventually hitting the rate-limit cap.
The curve is the same on every plan. Tiering applies to monthly quotas, project counts, and support — not to PoW difficulty. A paying customer's sitekey is never a cheaper target for an attacker than a Free one.
| Per-IP rate | Target | Expected hashes |
|---|---|---|
| r = 1 (first request in window) | 0x000FFFFF | ~4,096 |
| r = 99 (just below the 100/min cap) | 0x0000FFFF | ~65,536 |
65,536 SHA-256 hashes is roughly 40 ms of CPU on a modern laptop. A human filling out one form sits at the easy end; a botnet issuing a million submissions per day pays for the hard end at scale.
Adaptive hardening during attacks
On top of the per-IP curve, captchaapi.eu runs sitekey-wide anomaly detection. When a sitekey is being attacked — sudden RPS spike, unusual failed-verify ratio, datacenter-ASN concentration, cross-sitekey IP reputation — the base target is divided by a multiplier so every visitor of that sitekey solves a harder PoW until the attack ends.
| State | Multiplier | Sticky window |
|---|---|---|
| Normal | 1× (no change) | — |
| Suspicious | 1.5× | 5 min |
| Under attack | 8× | 5 min |
| Critical | 16× | 10 min |
The sticky window prevents oscillation: once a state is observed, it persists for that window even if the next minute looks calm. A higher fresh score overwrites immediately; a lower one ages out on the existing TTL. Legitimate visitors of an attacked sitekey pay the multiplier for the duration of the window — that is the cost of protecting the sitekey itself from downstream form abuse.
What captcha.js does on your page
A single <script> tag on your page does four things:
- Waits for user intent. Nothing happens on page load. The first
pointerdown,keydown,touchstart, orinputevent on your form lazily triggers a challenge request. - Preloads a challenge and solves it in a Web Worker. The solver is a pure-JS SHA-256 that returns only the top 32 bits of each hash, so comparison is a single integer check per nonce.
- Holds the solved response. As soon as the solution is found, the widget keeps the
"<token>.<solution>"string in memory until submit. It does not call/verify— that's your backend's job. -
Updates a status element.
A small
[data-captcha-status]span shows the widget's current state. Thedata-captcha-stateattribute carries the internal state name (use this to target with CSS); the text content carries a localised label. Six states exist:data-captcha-stateDefault English label When it fires waitingProtection standby Page load, lazy mode, when you've pre-placed the status element. No fetch yet. idlePreparing protection… First user interaction; /challengeis being requested.solvingVerifying form protection… Token received; PoW worker is iterating nonces. readyProtection active Solution found; the response is ready to submit. errorVerification unavailable Any terminal failure (network, missing site key, worker crash, etc.). rate_limitedPlease try again in {N} seconds HTTP 429 from /challenge. Live countdown driven by the server'sretry_aftervalue. - Intercepts submit. On submit, it writes the solved
captchaapi_responseas a hidden input and resubmits the form. Your backend then verifies that value with one server-side call.
What leaves the user's browser
Two values reach captchaapi.eu per verification: the visitor's IP (hashed with a server-side salt and held in cache only — up to 2 minutes for rate limiting, up to 24 hours as a cross-sitekey abuse-reputation counter, never persisted) and the challenge token. No cookies, no fingerprinting, no tracking. All processing stays in the EU (Hetzner, Nuremberg, Germany).
Ready to try it? The 5-minute integration walks through dropping it into a real form. The API reference describes the two endpoints in detail.