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 /challenge from the browser, and your backend calls /verify server-to-server with the captchaapi_response form field. The widget never calls /verify. 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":"pk_live_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 challenge 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.
403account_deactivatedThe project's Free-tier owner has hit the monthly cap repeatedly and the account is now locked. Unlike free_tier_limit_reached, this does not clear on the next billing cycle - the owner must upgrade to restore service.
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

Called from your backend, server-to-server, with the captchaapi_response form field. Authenticates with your project's secret key in an Authorization: Bearer header. A given response verifies exactly once - single-use and replay protection are enforced here, on our side.

Request

Send your secret key (format sk_live_..., kept only on the server) as a Bearer token. The body carries the value of the captchaapi_response form field:

Header / FieldTypeRequiredDescription
AuthorizationheaderyesBearer <your secret key>. Never expose this in the browser.
responsestringyesThe value of the captchaapi_response hidden input the widget added to the form.
curl -X POST https://captchaapi.eu/api/v1/captcha/verify \
    -H 'Authorization: Bearer sk_live_...' \
    -H 'Content-Type: application/json' \
    -d '{"response":"a1b2c3....47821"}'

Response (200 OK)

{
    "success":    true,
    "error_code": null,
    "over_limit": false,
    "risk": {
        "score":   0,
        "signals": { "headless": false }
    },
    "network": { "datacenter": false, "tor": false, "abuse": false }
}
FieldTypeDescription
successbooleantrue iff the response is valid and its solution satisfies the target. Allow the form through only on true.
error_codestring|nullnull on success, otherwise one of the codes below.
over_limitbooleantrue if the project is past its monthly quota. The challenge 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.
riskobjectAdvisory. A score (0-100) and per-category flags from our headless / automation signals, e.g. signals.headless. Present when the response matched a challenge token (so on success and invalid_solution, not on invalid_token / invalid_secret). Optional to use - success already reflects the proof-of-work. Use it for extra friction or logging on suspicious traffic, never as the only gate. The signals are soft and forgeable by design.
networkobjectAdvisory connection facts, separate from risk and never folded into its score. Each enabled source is a boolean - datacenter (the visitor's IP belongs to a hosting / datacenter network), tor (a known Tor exit node), and abuse (the IP is on a public abuse blocklist - confirmed attack / spam / compromised-host traffic, not a VPN detector). Plenty of real visitors browse via VPNs, Tor and cloud egress, so threshold these against your own threat model rather than reading them as our verdict. Present under the same conditions as risk. false means "checked, clean", which is distinct from a source being absent (not evaluated).

Note: a normal pass or fail returns HTTP 200 - check success, not the status code. As with every CAPTCHA provider, HTTP 200 does not mean pass. Only a bad or missing secret (401), rate-limiting (429), and unexpected server errors (500) return a non-200 status.

Errors

Statuserror_codeMeaning
200invalid_tokenThe response is unknown, expired, or already used. Also returned if the project was deleted between challenge and verify.
200invalid_solutionThe proof-of-work in the response didn't satisfy the target.
401invalid_secretThe Authorization: Bearer secret is missing, malformed, or doesn't match the project.
429rate_limitedMore than 200 verify requests from one IP in 60 seconds.
500internal_server_errorUnexpected server-side failure. Logged; safe to retry with backoff.

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 Keyed on your backend's egress IP (verify is a server-to-server call). Sits well above a healthy submission stream; if a single server fronts very high form volume, contact me to raise it.

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.
account_deactivatedchallengeThe project's Free-tier owner has been locked out after repeated cap hits. Waiting for the next cycle won't help; surface an upgrade CTA and get the account holder to upgrade in the dashboard.
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_tokenverifyThe response is unknown, expired, or already used. The visitor must solve a fresh challenge.
invalid_solutionverifyThe widget should prevent this. If you see it, either someone is submitting without solving or the form was tampered with.
invalid_secretverifyCheck the secret key in your server config. It must be the project's sk_live_... key, sent as Authorization: Bearer.
internal_server_errorbothRetry with backoff.

Need copy-paste code? See Backend examples.