API reference
Endpoints, response fields, status codes, rate limits and our stability commitments.
API reference (V2)
Base URL: https://api.captchacore.eu — every endpoint answers with JSON. If you use the supplied widget you never call /challenge yourself; the widget does that. For your own plugin only /verify is mandatory.
Authentication
| Endpoint | Key | Used where |
|---|---|---|
| GET /api/v2/challenge | cc_pub_… | in the browser — may be public |
| POST /api/v2/verify | cc_sec_… | server side only — never ship it |
| GET /api/v2/status | cc_sec_… | server side only |
The key always goes in the X-CaptchaCore-Key header, never in the URL.
GET /api/v2/challenge
Requests a computation together with its policy. Optional query parameter: form_type (see form policies). Answers with HTTP 200.
{
"challenge_id": "ch_a45cd15b0d6d2dc6",
"nonce": "a45cd15b…1d50", // einmalig, 5 Minuten gültig
"expires_at": 1788860066,
"policy": {
"mode": "adaptive",
"challenge_type": "sha256_pow", // Zusatzprüfung steht ggf. unter "step_up"
"challenge_types": ["sha256_pow"],
"difficulty": 4, // führende Nullen im Hash
"algorithm": "sha256",
"time_budget_ms": 500,
"requires_interaction": false,
"requires_stepup_on_submit": false,
"memory_hard_enabled": false,
"form_type": "contact",
"site_profile": "balanced",
"uam_level": 0 // 0–3, Under-Attack-Stufe
},
"bindings": { // bindet den Token an Site, Origin und Formular
"site_id": 2,
"origin": "https://example.com",
"form_type": "contact",
"issued_at": 1788859766,
"expires_at": 1788860066,
"token_ttl_sec": 300
},
"bindings_signature": "CBkATjVc…RDg==", // Ed25519, unveränderlich mitschicken
"signing_key_id": "k_397d99d39156c5f5",
"widget": { /* im Admin gepflegtes Erscheinungsbild */ },
"widget_version": "3.0.0"
}
Important for your own clients: bindings and bindings_signature must travel back into the token unchanged. If they are missing or altered, /verify answers with block.
POST /api/v2/verify
Checks the token from the form. As a rule it answers with HTTP 200, including for a rejection. Evaluate the body, not the status code.
// Request { "token": "<Wert des Feldes captchacore_token>", "form_type": "contact", // optional, steuert die Policy "page_url": "https://…", // optional, nur für Auswertungen "client_ip": "203.0.113.42" // empfohlen: IP des Besuchers } // Response { "valid": true, // false bei block UND bei step_up "decision": "allow", // allow | challenge | step_up | block "action": "allow", // identisch zu decision (älterer Feldname) "request_id": "…", // für Rückfragen an den Support "risk_score": 8, // 0–100, höher = riskanter "confidence": 0.94, // 0.0–1.0 Signalabdeckung "reasons": ["pow_valid", "behavior_human_like"], "step_up": null // bei decision=step_up: Daten für die Zusatzprüfung }
How to handle decision
| decision | valid | Recommended behaviour |
|---|---|---|
| allow | true | Process the form normally. |
| challenge | true | Let it through but log it. Optionally add a check of your own, such as a confirmation email. |
| step_up | false | Do NOT reject outright. The widget performs the additional check itself; return the form to the user with a friendly message. |
| block | false | Reject. Show an error without details so attackers learn nothing. |
GET /api/v2/status
Availability and the site's current under-attack level. Suitable for a health check inside your plugin.
{ "status": "ok", "under_attack": false, "version": "2.x" }
HTTP status codes
| Code | Body | Cause and response |
|---|---|---|
| 200 | {"valid": …} | Normal case, including a rejection. Evaluate the body. |
| 401 | {"error":"Missing API key"} | The X-CaptchaCore-Key header is missing. |
| 401 | {"error":"Invalid API key"} | Key wrong, revoked or for the wrong site. Also the case when the public key is used on /verify. |
| 403 | {"error": …} | The origin is not among the site's allowed domains. |
| 200 | {"valid": false, "reasons": ["invalid_request"]} | Required field token is missing or unusable. |
| 200 | {"valid": false, "reasons": ["rate_limited"]} | The site's verify limit has been reached. Reject, do not let through. |
| 429 | — | Only on /challenge (limit per visitor IP). Above the limit, verify answers with 200 and "rejected". |
| 401 | {"error":"Invalid API key"} | Also when the organisation has been deleted or deactivated: its keys are then no longer valid. |
| 503 | {"error":"Account suspended"} | Account suspended because of an outstanding payment. Only /verify and /status; challenges keep working. Protection is suspended, and your integration applies its fail-open rule so that you can still log in. |
| 5xx | — | A fault on our side. Your fail-open or fail-closed rule applies. |
Rate limits
| Endpoint | Limit | Counted by |
|---|---|---|
| GET /api/v2/challenge | 300 / min | Visitor network and site (IPv6 per /64); 1200 / min per network across all sites |
| POST /api/v2/verify | 600 / min | customer's site key |
| POST /api/v2/verify | 30 / min | Site key and visitor IP (client_ip, IPv6 per /64) |
Verify is counted per site, not per IP. Your server may therefore check as many forms as your site has traffic for, without locking itself out.
Only tokens for a challenge that was actually issued count towards the per-site limit. Junk requests from an attacker hit the per-visitor-IP limit and do not use up the budget of your real visitors. For this, send client_ip, as all official integrations do.
Webhooks
CaptchaCore notifies your own systems when something happens you want to react to: quota reached, under-attack mode triggered, account blocked or unblocked. Set it up in the customer area under Sites → Webhooks (from the Professional plan).
Events
| Event | When |
|---|---|
| usage.warning | Monthly quota 80 % or 90 % used |
| usage.limit_reached | Monthly quota reached — further verifications are rejected |
| site.under_attack.activated | Under-attack mode of a site activated (manually or automatically) |
| site.under_attack.deactivated | Under-attack mode of a site ended |
| organisation.blocked | Account blocked (e.g. unpaid invoice) — API calls are rejected |
| organisation.unblocked | Account unblocked again |
| webhook.test | Test event from the customer area |
What arrives
A POST with a JSON body. Three headers help you match and verify it:
POST /ihr-endpunkt HTTP/1.1
Content-Type: application/json
X-CaptchaCore-Event: usage.limit_reached
X-CaptchaCore-Delivery: 5f1c2c1e-… // eindeutig je Zustellung, gleich bei Wiederholungen
X-CaptchaCore-Timestamp: 1758196800
X-CaptchaCore-Signature: sha256=3b2a…
{
"event": "usage.limit_reached",
"occurred_at": "2026-09-18T14:00:00+02:00",
"organisation_id": 42,
"data": { "threshold_percent": 100, "used": 10000, "limit": 10000, "plan": "pro", "period": "2026-09" }
}Verifying the signature
The signature is an HMAC-SHA256 over “timestamp.body” with the secret from the customer area. Compare in constant time and reject timestamps older than five minutes — that prevents replays of intercepted requests.
$secret = getenv('CAPTCHACORE_WEBHOOK_SECRET'); $body = file_get_contents('php://input'); $timestamp = $_SERVER['HTTP_X_CAPTCHACORE_TIMESTAMP'] ?? ''; $expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $body, $secret); if (abs(time() - (int) $timestamp) > 300 || ! hash_equals($expected, $_SERVER['HTTP_X_CAPTCHACORE_SIGNATURE'] ?? '')) { http_response_code(401); exit; } $event = json_decode($body, true); // … verarbeiten, dann schnell mit 2xx antworten http_response_code(204);
Delivery and retries
- Answer with a 2xx status within 5 seconds — process anything expensive asynchronously.
- On errors or timeout, delivery is retried up to three times (after 30 seconds and after 5 minutes) with the same delivery ID — make your processing idempotent with it.
- After ten consecutive failures the webhook is switched off; in the customer area you see the reason and can switch it on again.
- Every delivery with response code, duration and response excerpt is listed under “Deliveries” — also for troubleshooting on your side.
Versioning and stability
Anyone maintaining a plugin needs to know what they can rely on. These are our commitments.
The path version stays stable
Within /api/v2 we neither remove nor rename response fields. New fields may appear at any time — read responses tolerantly and do not fail on unknown keys.
V1 is deprecated
The old verification interface under /api/v1 and the widget captchacore.min.js keep running for older integrations only; the widget shows visitors a notice to update. Please switch to V2 — we will announce the shutdown date in good time.
The widget updates itself
Through the CDN path captchacore-v2.min.js you always get the maintained build. Do not ship your own copy, or you will miss improvements to bot detection.
We announce changes
Anything that could affect existing integrations is announced in advance on the public system status and by email to the technical address on file.