Verifying on your backend

The server-to-server call, the risk band, and what failover means for your accept decision.

The token the widget writes is opaque. Read the field, forward the string verbatim, and never parse it — it already carries the challenge id, the nonce, the honeypot and the behavioural telemetry.

Reject the submission if captcha-token is missing, if /v1/verify does not return 200, or if it returns success: false.

bash
curl -s -X POST https://api.bollwark.eu/v1/verify \
  -H "Authorization: Bearer YOUR_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "token": "<captcha-token value>" }'
json
{ "success": true, "failover": false, "risk": "low", "challenge_id": "…" }

Challenges are single-use — a second submit with the same challenge_id fails.

Forward the visitor's address

json
{ "token": "…", "remote_ip": "203.0.113.7" }

remote_ip is optional. When present, the service compares it with the address the puzzle was issued to (IPv6 at /64). A mismatch is the shape of a token farm — puzzles solved on one machine, tokens handed to a fleet — and lands the submission in risk: "elevated". It is never a refusal on its own.

Pass the address you would rate-limit on. Behind a proxy or CDN that is the resolved client address, not the proxy's — otherwise every submission you send reads as elevated.

Only the top-level field counts. A remote_ip inside the opaque token is ignored, since the token is written by the visitor's own browser.

The risk band

success is the enforcement decision. risk is the reason behind it, so you can step up on a submission the service accepted but did not like.

risk success Meaning Reasonable handling
low true Solved, nothing suspicious at submit time Accept
elevated true Solved, but verify-time signals landed in the shadow band Accept, and step up where the action warrants it
high false Solved, but refused on risk Reject, or queue for review
high true Would have been refused — the site is in monitor mode Log it. This is what flipping to enforce will start rejecting.

The band is absent when there was no risk verdict to report, such as an invalid proof of work. Treat a missing band as low: a field nothing acts on shouldn't be able to fail a verification.

Failover

failover: true means success was granted without a solved puzzle, because the service was attestably unreachable when the visitor loaded your form. It is off unless the operator enabled client failover.

Read success alone and you get availability during outages by default. If you'd rather accept and flag, this is the hook:

js
if (!result.success) return res.status(400).send("CAPTCHA failed");
if (result.failover) {
  // Verified only in the weak sense — no proof of work was possible. The
  // honeypot and behavioural signals were still checked.
  queueForReview(submission);
}

Server-to-server callers

A caller that builds the request without the widget can send the explicit fields instead of token: challenge_id and nonce, plus optional honeypot and behavior.

Forward the widget's behavior blob verbatim or omit it entirely — never synthesise a constant one. Identical activity-claiming blobs repeating on a site are scored as scripted traffic, while an omitted blob is neutral.