# Siteverify reference

> Request and response fields, error codes, idempotency, remoteip check and rate limits of the OpenWAF Verify siteverify endpoints.

Your server calls siteverify to check a token produced by the [OpenWAF Verify widget](https://developers.openwaf.dev/waf/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](#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`).

```json
{"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 human» step. |
| `metadata.risk` | `low`, or `high` when interaction was required. |
| `metadata.result_with_testing_key` | `true` when a [test secret](https://developers.openwaf.dev/waf/verify-widget/#test-keys) 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": true` as a failure.
- Reusing a token after a failed form submission: call `owafVerify.reset(id)` for a new one.
