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",     // oder argon2id_pow, interaction_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
}

// Response
{
  "valid": true,               // false bei block UND bei step_up
  "decision": "allow",        // allow | challenge | step_up | block
  "action": "allow",          // identisch zu decision, für V1-Kompatibilität
  "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.
422 {"message": …} A required field is missing, usually token.
429 Rate limit reached. Treat it like an outage, see error handling.
5xx A fault on our side. Your fail-open or fail-closed rule applies.

Rate limits

Endpoint Limit Counted by
GET /api/v2/challenge60 / minvisitor's IP
POST /api/v2/verify600 / mincustomer's site key

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.

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

EventWhen
usage.warningMonthly quota 80 % or 90 % used
usage.limit_reachedMonthly quota reached — further verifications are rejected
site.under_attack.activatedUnder-attack mode of a site activated (manually or automatically)
site.under_attack.deactivatedUnder-attack mode of a site ended
organisation.blockedAccount blocked (e.g. unpaid invoice) — API calls are rejected
organisation.unblockedAccount unblocked again
webhook.testTest 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 remains available for now

The old interface under /api/v1 is still served but receives no new signals. New integrations should use V2 exclusively.

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.