logo
captchaAPI

API reference

captchaapi.eu exposes two JSON endpoints. Both accept Content-Type: application/json and return JSON. The base URL is https://captchaapi.eu/api/v1.

In a standard integration the widget calls both endpoints from the browser. You usually don't need to call either from your backend — you just verify the captcha_attestation the widget produces. See Backend examples.

POST /api/v1/captcha/challenge

Issues a new PoW challenge bound to the caller's IP. The returned token lives for 2 minutes and can be redeemed exactly once.

Request

FieldTypeRequiredDescription
site_keystringyesYour project's public site key.

Origin validation: if your project has Allowed Domains configured, the request must carry an Origin or Referer header that matches one of them. Projects with no configured domains accept any origin.

curl -X POST https://captchaapi.eu/api/v1/captcha/challenge \
    -H 'Content-Type: application/json' \
    -H 'Origin: https://your-site.com' \
    -d '{"site_key":"sk_abc123..."}'

Response (200 OK)

{
    "token":      "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
    "target":     1048575,
    "expires_at": 1765456789
}
FieldTypeDescription
tokenstring (32)Opaque challenge identifier. Echo back on verify.
targetinteger (uint32)Solve condition: first 8 hex chars of sha256(token + solution) must be ≤ target.
expires_atinteger (unix ts)Token invalid after this time. Always 2 minutes from issue.

Errors

Statuserror_codeMeaning
422invalid_site_keyThe site_key field is missing or doesn't match any project.
402account_suspendedThe project's owning account is suspended due to a billing failure (paid tier, dunning exhausted). Returned in preference to project_inactive so the integrator can route the visitor to a billing-reactivation flow rather than a generic "project disabled" page.
403free_tier_limit_reachedThe project's owner is on the Free tier and has exhausted the monthly request quota. The account is in the Free-tier hard-cap state and all of its projects are paused until the owner upgrades or the next billing cycle resets. Returned in preference to project_inactive so the integrator can surface an upgrade CTA.
403project_inactiveThe project exists but is disabled in the dashboard (manual disable; no billing implication).
403domain_not_allowedThe request's Origin/Referer isn't in the project's allowed domains.
429rate_limitedPer-IP or per-project rate limit exceeded — see Rate limits. Response includes retry_after (seconds).
500internal_server_errorUnexpected server-side failure. Logged; safe to retry with backoff.

Error response shape:

{"success": false, "error_code": "rate_limited", "retry_after": 42}

POST /api/v1/captcha/verify

Validates a submitted solution against an existing token and, on success, returns a signed attestation your backend can verify locally. Tokens are invalidated after the first call, pass or fail.

Request

FieldTypeRequiredDescription
tokenstringyesThe token returned by /challenge.
solutionstringyesThe nonce found by the client (a decimal integer serialised as a string).
curl -X POST https://captchaapi.eu/api/v1/captcha/verify \
    -H 'Content-Type: application/json' \
    -d '{"token":"a1b2c3...","solution":"47821"}'

Response (200 OK)

{
    "success":                true,
    "attestation":            "eyJzayI6InNr...Q.6ab9c4...",
    "attestation_expires_at": 1765457089,
    "error_code":             null,
    "over_limit":             false
}
FieldTypeDescription
successbooleantrue iff the solution satisfies the target and the project is still valid.
attestationstring|nullSigned proof on success, null otherwise. See Attestation format.
attestation_expires_atinteger|nullUnix timestamp after which the attestation is stale. Convenience field — also encoded inside the payload as exp.
error_codestring|nullnull on success, otherwise one of the codes below.
over_limitbooleantrue if the project is past its monthly quota. The token was issued on the same baseline PoW curve as in-quota traffic (no soft-serve shortcut to a trivial target) and verification still succeeds — treat it as a signal to upgrade, not a hard failure. Mirrored in the payload as ol.

Note: most verify failures still return HTTP 200 — check success, not the status code. Only rate-limiting returns a 4xx status.

Errors

Statuserror_codeMeaning
200invalid_tokenToken missing, expired, already used, or never existed. Also returned if the project was deleted between challenge and verify.
200ip_mismatchVerify call came from a different IP than the one that received the challenge. The token is discarded to prevent replay.
200invalid_solutionHash of token + solution didn't clear the target.
429rate_limitedMore than 200 verify requests from one IP in 60 seconds.

Attestation format

The attestation returned by /verify is a string of two base64url-encoded segments separated by a dot:

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

The payload is a compact JSON object with these fields:

FieldTypeDescription
skstringYour site key. Verify it matches your project.
iatintIssued-at unix timestamp.
expintExpiry unix timestamp. Reject attestations past this time. Configured per project (1 min – 10 min, default 5 min) in the dashboard.
jtistring (uuid)Unique identifier — always present. Cache it with an atomic SET NX for the remaining exp - now() seconds to reject replays (pattern); local HMAC verification means there's no central state for us to dedup against, by design.
olboolOver-limit flag, matching the top-level over_limit.

Encoding details

  • Algorithm: HMAC-SHA256.
  • Key: the raw secret key string as shown in your project dashboard.
  • Message: the base64url payload string (not the decoded JSON).
  • Encoding: base64url (-_ alphabet) with padding stripped. Standard base64 (+/ with = padding) will not verify — you must translate first.

Verification steps

  1. Split the attestation at . — two parts.
  2. Recompute hmac_sha256(payload_b64, secret_key) and compare to the second part using a constant-time equality check.
  3. If equal, base64url-decode the payload and parse as JSON.
  4. Check exp >= now() and sk == your_site_key.
  5. Recommended: atomically claim jti in a cache (e.g. Redis SET NX) with TTL exp - now() — see backend examples.

Rate limits

All limits are tracked per hashed IP (sha256(ip + secret)) over a rolling 60-second window unless stated otherwise. When a limit is tripped, the response is HTTP 429 with {"success": false, "error_code": "rate_limited", "retry_after": N} — the retry_after value (seconds) is what the widget surfaces to the visitor as a live countdown.

CAPTCHA API

EndpointLimitWindowNotes
POST /api/v1/captcha/challenge 100 / IP 60 s Standard limit for accounts within their monthly quota.
POST /api/v1/captcha/challenge (over-limit accounts) 500 / IP 60 s Once the project's monthly quota is exhausted, paid plans keep serving on the same baseline PoW curve as in-quota traffic — visitors are never blocked. The per-IP cap is raised here so legitimate bursts continue to be served while still bounding abuse.
POST /api/v1/captcha/verify 200 / IP 60 s Roughly 2× the challenge ceiling so the verify side never bottlenecks a healthy challenge stream.

Within the monthly quota, a per-project aggregate cap bounds challenge traffic at 2 000 requests per project per 60-second window, combined across every IP. It catches a distributed flood that keeps each individual IP under the 100 / IP cap above, and sits far above any legitimate per-minute volume — a project within its quota does not meet it under normal use.

A separate per-project daily ceiling bounds over-limit traffic at 2 000 000 requests per project per day, anchored to absorb a 100× viral surge for the largest plan with headroom. This is an anti-DoS bound for over-limit traffic only — it does not affect accounts within their monthly quota.

Authentication & signup forms

SurfaceLimitWindowKey
Sign-in (POST /login) 5 60 s Per (email + IP). Standard Fortify throttling.
Two-factor challenge (POST /two-factor-challenge) 5 60 s Per session.

All thresholds above are configured via environment variables and may be tuned with reasonable notice in line with the Terms of Service Section 11 change-policy. If you see rate_limited regularly from legitimate traffic, you are likely sharing an egress IP — get in touch.

Error code glossary

error_codeEndpointWhat to do
invalid_site_keychallengeCheck you're passing the right key for the environment (staging vs. production are separate projects).
account_suspendedchallengeThe project's owner has an unpaid invoice past the grace window. Surface a "billing issue — please contact site admin" page to your visitors and get the account holder to settle billing in the dashboard.
free_tier_limit_reachedchallengeThe project's Free-tier owner has used all monthly challenges. Surface an upgrade CTA, or wait for the next billing cycle. Distinct from project_inactive so you can show a quota-exhausted message rather than a generic disabled page.
project_inactivechallengeRe-enable the project in the dashboard.
domain_not_allowedchallengeAdd the domain to the project's allowed list. Include the port for non-standard ports (e.g. localhost:3000).
rate_limitedbothWait retry_after seconds. If you see this regularly from legitimate traffic, you're likely sharing an egress IP — contact me.
invalid_tokenverifyToken expired or was already used. Request a fresh challenge.
ip_mismatchverifyUsually means a proxy/CDN in front of the widget is changing the client IP between challenge and verify. Make sure both calls use the same edge.
invalid_solutionverifyThe widget should prevent this. If you see it, either someone is submitting without solving or the form was tampered with.
internal_server_errorchallengeRetry with backoff.

Need copy-paste code? See Backend examples.