# Referencia de siteverify

> Campos de petición y respuesta, códigos de error, idempotencia, comprobación de remoteip y límites de los endpoints siteverify de OpenWAF Verify.

Tu servidor llama a siteverify para comprobar un token producido por el [widget OpenWAF Verify](https://developers.openwaf.dev/es/waf/verify-widget/). Hay dos endpoints con la misma semántica:

| Endpoint | Úsalo cuando |
|---|---|
| `POST https://<tu host>/__waf/challenge/siteverify` | Tu backend llega a un nombre con proxy de OpenWAF. Cualquier nombre servido por el borde puede verificar un token de cualquier widget, incluso de otra zona. |
| `POST https://dash.openwaf.dev/v1/challenge/siteverify` | Tu backend sale directamente. No hace falta token de API: el secreto del widget autentica la llamada. |

Un token es de un solo uso entre los dos: uno gastado en el borde también está gastado en el plano de control, y al revés.

## Petición

El cuerpo es `application/x-www-form-urlencoded` o, con `Content-Type: application/json`, un objeto JSON. Como mucho 8192 bytes.

| Campo | Obligatorio | Significado |
|---|---|---|
| `secret` | sí | El secreto del widget (`ows_…`). |
| `response` | sí | El token del campo `owaf-verify-response` (`v1.…`, como mucho 2048 caracteres). |
| `remoteip` | no | La dirección IP del visitante. Si se envía, tiene que estar en la misma /24 IPv4 o /64 IPv6 que el navegador que resolvió el reto. |
| `idempotency_key` | no | Hasta 128 caracteres. Consulta [Idempotencia](#idempotency). |

## Respuesta

La respuesta es JSON y siempre `200`, salvo un cuerpo mal formado (`400`), un método distinto de `POST` (`405`) y un límite de peticiones (`429` con `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"}}
```

| Campo | Significado |
|---|---|
| `success` | `true` solo para un token válido y sin gastar de este widget. |
| `challenge_ts` | Cuándo se emitió el token (RFC 3339, UTC). |
| `hostname` | Nombre de host donde se mostró el widget. Comprueba que es tuyo. |
| `action` | El `data-action` del widget. Comprueba que corresponde al formulario. |
| `cdata` | El `data-cdata` del widget, si lo hay. |
| `error-codes` | Vacío si todo va bien; si no, uno de los códigos de abajo. |
| `metadata.mode` | Modo del widget: `managed`, `non_interactive` o `invisible`. |
| `metadata.interactive` | `true` cuando el visitante pasó por el paso «Soy humano». |
| `metadata.risk` | `low`, o `high` cuando hizo falta interacción. |
| `metadata.result_with_testing_key` | `true` cuando respondió un [secreto de prueba](https://developers.openwaf.dev/es/waf/verify-widget/#test-keys). |

Una comprobación fallida responde `{"success": false, "error-codes": ["…"]}`.

## Códigos de error

| Código | Significado |
|---|---|
| `missing-input-secret` | `secret` está vacío. |
| `invalid-input-secret` | El secreto está mal formado o no es un secreto vigente del widget que emitió el token. |
| `missing-input-response` | `response` está vacío. |
| `invalid-input-response` | El token está mal formado, tiene una firma incorrecta o es de un widget desconocido. |
| `timeout-or-duplicate` | El token caducó (300 segundos) o ya se gastó. |
| `remoteip-mismatch` | `remoteip` no está en la red a la que va ligado el token. |
| `bad-request` | El cuerpo no es un formulario ni un objeto JSON válido, es demasiado grande, o el método no es `POST`. |
| `rate-limited` | Demasiadas llamadas; espera lo que indique `Retry-After`. |
| `internal-error` | Ahora mismo no se pudo comprobar el uso único. Reintenta; el token no se gastó. |

## Idempotencia

Un token se gasta en la primera llamada correcta, así que un reintento tras un error de red recibiría `timeout-or-duplicate`. Envía un `idempotency_key` para que los reintentos sean seguros: una llamada con el mismo secreto, respuesta y clave en los 300 segundos siguientes devuelve la primera respuesta sin gastar nada otra vez.

La clave la recuerda el verificador que respondió, no se comparte: el nodo de borde que atendió la primera llamada, o el plano de control. Reintenta contra el mismo endpoint. Las respuestas `internal-error` nunca se recuerdan.

## Límites

| Endpoint | Límites |
|---|---|
| Tu host (`/__waf/challenge/siteverify`) | 1000 llamadas por minuto por secreto de widget, más el límite por IP del borde. |
| Plano de control | 600 llamadas por minuto por IP de cliente y 1000 por secreto de widget. |

Los endpoints del propio widget en tu host también tienen límite: 60 retos y 20 soluciones por minuto por IP del visitante.

## Errores habituales

- Llamar a siteverify desde el navegador: el secreto tiene que quedarse en tu servidor. El endpoint del borde no envía cabeceras CORS.
- Aceptar un formulario cuando no se pudo llegar a siteverify. Trata cualquier respuesta distinta de `"success": true` como un fallo.
- Reutilizar un token tras un envío fallido: llama a `owafVerify.reset(id)` para obtener uno nuevo.
