Your server calls siteverify to check a token produced by the OpenWAF Verify widget. There are two endpoints with the same semantics:
| Endpoint | Use it when |
|---|---|
POST https://<your host>/__waf/challenge/siteverify | Your backend can reach a name proxied by OpenWAF. Any name served by the edge can verify a token of any widget, even of another zone. |
POST https://dash.openwaf.dev/v1/challenge/siteverify | Your backend calls out directly. No API token is needed: the widget secret authenticates the call. |
A token is single use across both: one spent on the edge is spent on the control plane too, and the other way round.
Request
The body is application/x-www-form-urlencoded or, with Content-Type: application/json, a JSON object. At most 8192 bytes.
| Field | Required | Meaning |
|---|---|---|
secret | yes | The widget secret (ows_…). |
response | yes | The token from the owaf-verify-response field (v1.…, at most 2048 characters). |
remoteip | no | The visitor's IP address. When given, it must be in the same IPv4 /24 or IPv6 /64 as the browser that solved the challenge. |
idempotency_key | no | Up to 128 characters. See Idempotency. |
Response
The answer is JSON and always 200, except a malformed body (400), a method other than POST (405) and a rate limit (429 with Retry-After).
{"success": true,
"challenge_ts": "2026-10-28T10:15:03Z",
"hostname": "example.com",
"action": "signup",
"cdata": "order-1234",
"error-codes": [],
"metadata": {"mode": "managed", "interactive": false, "risk": "low"}}| Field | Meaning |
|---|---|
success | true only for a valid, unspent token of this widget. |
challenge_ts | When the token was issued (RFC 3339, UTC). |
hostname | Host name the widget was rendered on. Check that it is yours. |
action | The widget's data-action. Check that it matches the form. |
cdata | The widget's data-cdata, if any. |
error-codes | Empty on success; otherwise one of the codes below. |
metadata.mode | Widget mode: managed, non_interactive or invisible. |
metadata.interactive | true when the visitor went through the «I'm a person» step. |
metadata.risk | low, or high when interaction was required. |
metadata.result_with_testing_key | true when a test secret answered. |
A failed check answers {"success": false, "error-codes": ["…"]}.
Error codes
| Code | Meaning |
|---|---|
missing-input-secret | secret is empty. |
invalid-input-secret | The secret is malformed or isn't a current secret of the widget that issued the token. |
missing-input-response | response is empty. |
invalid-input-response | The token is malformed, has a bad signature or belongs to an unknown widget. |
timeout-or-duplicate | The token expired (300 seconds) or was already spent. |
remoteip-mismatch | remoteip is not in the network the token is bound to. |
bad-request | The body is not a valid form or JSON object, is too large, or the method is not POST. |
rate-limited | Too many calls; wait for Retry-After. |
internal-error | Single use could not be checked right now. Retry; the token was not spent. |
Idempotency
A token is spent by the first successful call, so a retry after a network error would get timeout-or-duplicate. Send an idempotency_key to make retries safe: a call with the same secret, response and key within 300 seconds returns the first answer without spending anything again.
The key is remembered by the verifier that answered, not shared: the edge node that served the first call, or the control plane. Retry against the same endpoint. internal-error answers are never remembered.
Rate limits
| Endpoint | Limits |
|---|---|
Your host (/__waf/challenge/siteverify) | 1000 calls per minute per widget secret, plus the edge's per-IP limit. |
| Control plane | 600 calls per minute per client IP and 1000 per widget secret. |
The widget's own endpoints on your host are limited too: 60 challenges and 20 solutions per minute per visitor IP.
Calls to avoid
- Calling siteverify from the browser: the secret must stay on your server. The edge endpoint sends no CORS headers.
- Accepting a form when siteverify could not be reached. Treat any answer other than
"success": trueas a failure. - Reusing a token after a failed form submission: call
owafVerify.reset(id)for a new one.