logo
captchaAPI

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.

  1. The widget asks captchaapi.eu for a challenge. It receives a random 32-character token, a target (a 32-bit integer), and an expiry.
  2. The browser searches for a solution: an integer nonce such that SHA-256(token + nonce) — interpreted as hex — has its first 8 characters numerically ≤ target. Only brute force works.
  3. The widget injects a single hidden input captchaapi_response with the value "<token>.<solution>" and submits the form. The widget does not call /verify.
  4. Your backend reads captchaapi_response and makes one server-to-server POST to /verify with your secret key in an Authorization: Bearer header.
  5. captchaapi.eu re-hashes to confirm the solution, marks the response single-use, and returns {"success": true|false, ...}. Let the form through only when success is true.

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:

FieldMeaning
successtrue 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_codenull 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_limitInformational 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 rateTargetExpected 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.

StateMultiplierSticky window
Normal1× (no change)
Suspicious1.5×5 min
Under attack5 min
Critical16×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:

  1. Waits for user intent. Nothing happens on page load. The first pointerdown, keydown, touchstart, or input event on your form lazily triggers a challenge request.
  2. 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.
  3. 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.
  4. Updates a status element. A small [data-captcha-status] span shows the widget's current state. The data-captcha-state attribute 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 labelWhen it fires
    waitingProtection standbyPage load, lazy mode, when you've pre-placed the status element. No fetch yet.
    idlePreparing protection…First user interaction; /challenge is being requested.
    solvingVerifying form protection…Token received; PoW worker is iterating nonces.
    readyProtection activeSolution found; the response is ready to submit.
    errorVerification unavailableAny terminal failure (network, missing site key, worker crash, etc.).
    rate_limitedPlease try again in {N} secondsHTTP 429 from /challenge. Live countdown driven by the server's retry_after value.
  5. Intercepts submit. On submit, it writes the solved captchaapi_response as 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.