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 round-trips: two between the visitor's browser and captchaapi.eu, and a final local HMAC check on your server.

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 posts the pair to /verify. captchaapi.eu re-hashes to confirm, then returns a short-lived signed attestation: a base64url blob payload.signature signed with your project's secret key using HMAC-SHA256.
  4. On form submit, the widget attaches the attestation as a single hidden input captcha_attestation.
  5. Your backend verifies the HMAC locally using the same secret key. No HTTP round-trip to captchaapi.eu from your server.

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.

The attestation

When PoW verification succeeds, captchaapi.eu returns a signed attestation of the form:

base64url(payload_json).base64url(hmac_sha256(payload_b64, secret_key))

The payload is a compact JSON object:

FieldMeaning
skYour project's site key — lets the backend confirm the attestation is for the right project.
iatIssued-at timestamp (unix seconds).
expExpiry timestamp (unix seconds). Default 5 minutes, configurable per project from 1 minute to 10 minutes — see Choosing an attestation TTL.
jtiRandom UUID — always present in every attestation we issue. Use it as a single-use token in your own server-side cache to reject replays (pattern); local HMAC verification means there's no central state for us to dedup against, by design.
olOver-limit flag. true when the project is past its monthly quota. Verification still succeeds; treat it as a signal to upgrade.

Every project has a site key (public, embedded in the browser) and a secret key (private, kept on your server). The secret key is shown once when you create a project, and is re-revealed later with password confirmation. It's what lets your backend verify attestations without contacting captchaapi.eu on every form submit.

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. Calls /verify and caches the attestation. As soon as the solution is found, the widget redeems it with captchaapi.eu and holds the returned attestation in memory until submit.
  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 active/verify succeeded; attestation cached.
    errorVerification unavailableAny terminal failure (network, missing site key, worker crash, etc.).
    rate_limitedPlease try again in {N} secondsHTTP 429 from /challenge or /verify. Live countdown driven by the server's retry_after value.
  5. Intercepts submit. On submit, it writes the cached captcha_attestation as a hidden input and resubmits the form. The attestation's freshness is validated server-side via the signed exp field — no client-side clock check.

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.