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.
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>" }'{ "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
{ "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:
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.