# SoftwaCAP — Documentación completa

> Captcha resistente a IA con análisis LLM integrado. Drop-in replacement de reCAPTCHA. Datos en la UE. Gestionable desde Claude vía MCP.

**Endpoint**: `https://captcha.softwalabs.com`
**Versión API**: v1
**Última actualización**: 2026

---

## Tabla de contenidos

1. [Introducción](#introduccion)
2. [Instalación rápida](#instalacion)
3. [Integración frontend (HTML + JS)](#frontend)
4. [Verificación server-side (siteverify)](#siteverify)
5. [Ejemplos por lenguaje](#ejemplos)
6. [Migración desde reCAPTCHA](#migracion)
7. [Configuración avanzada del sitio](#config-sitio)
8. [Adaptive Pace · Subliminal Decoys · Phantom Honeypot](#anti-bot)
9. [MCP server (Pro+)](#mcp)
10. [Planes, cuotas y límites](#planes)
11. [Rate limits](#rate-limits)
12. [Códigos de error](#errores)
13. [FAQ y troubleshooting](#faq)

---

## 1. Introducción <a id="introduccion"></a>

SoftwaCAP es un captcha que protege formularios contra bots usando **tres capas de defensa**:

1. **Constelación temporal** — el usuario memoriza la secuencia de estrellas que parpadean. La respuesta no existe en el HTML, sólo en la animación. Resistente a screenshot.
2. **Behavioral scoring server-side** — analiza timing entre taps, movimientos de ratón, fingerprint del browser y detección de headless. Bots con timing perfecto se delatan solos.
3. **Análisis IA en tiempo real** — un modelo de lenguaje propio juzga el patrón global de los signals. Único en el mercado de captchas.

Capas adicionales activables por sitio:

- **Phantom honeypot** (siempre activo) — estrella trampa que un humano nunca toca pero un bot que hace click a todo lo que ve cae instantáneamente.
- **Adaptive Pace** (Pro+) — el ritmo se ajusta automáticamente a la tasa de éxito de tus humanos.
- **Subliminal Decoys** (Pro+) — micro-flashes de 1 frame imperceptibles para humanos pero capturables por solvers visuales.

### Por qué SoftwaCAP

- Drop-in 100% compatible con reCAPTCHA (cambias 2 líneas).
- Toda la infraestructura en la UE (Hostinger).
- Sin tracking de Google ni de terceros.
- API key + MCP integrable en Claude Desktop / Claude Code.
- Política de retención de signals: **90 días**.

---

## 2. Instalación rápida <a id="instalacion"></a>

### 2.1. Crear cuenta

Visita https://captcha.softwalabs.com/signup, registra email + contraseña. Plan **Free** automático: 1.000 verificaciones/mes, 1 sitio.

### 2.2. Registrar un sitio

En el dashboard → **Sitios y claves** → `+ Añadir sitio`. Rellena:

| Campo | Descripción |
|-------|-------------|
| Nombre | Etiqueta interna para identificarlo |
| Dominios permitidos | CSV de hostnames donde se cargará el captcha. Acepta wildcards `*.midominio.com`. Ej: `midominio.com, www.midominio.com` |
| Dificultad base | `easy` (3 estrellas, ritmo lento) · `normal` (3 estrellas, ritmo medio) · `hard` (4 estrellas, ritmo rápido) |
| Adaptive Pace | (Pro+) si activo, los timings se auto-ajustan |

Tras crear: tendrás dos claves de 40 caracteres cada una con prefijo:

- **`pk_swl_…`** — sitekey, **pública**, va en el HTML del cliente.
- **`sk_swl_…`** — secret_key, **privada**, sólo en tu backend.

### 2.3. Integrar (60 segundos)

```html
<script src="https://captcha.softwalabs.com/v1/api.js" async defer></script>

<form method="post" action="/login">
    <input name="email" type="email">
    <input name="password" type="password">
    <div class="softwacap" data-sitekey="TU_SITEKEY"></div>
    <button type="submit">Entrar</button>
</form>
```

En tu backend, valida el token con `POST /siteverify`. Mira [§4](#siteverify) y [§5](#ejemplos) para detalles.

---

## 3. Integración frontend (HTML + JS) <a id="frontend"></a>

### 3.1. Snippet básico

El widget se auto-renderiza en cualquier `<div class="softwacap" data-sitekey="...">`. También acepta `class="g-recaptcha"` (alias para migración).

### 3.2. Atributos `data-*`

| Atributo | Tipo | Descripción |
|----------|------|-------------|
| `data-sitekey` | string | **Obligatorio**. Tu clave pública. |
| `data-callback` | string | Nombre de función JS global a llamar al éxito. Recibe el token como argumento. |
| `data-expired-callback` | string | Función JS que se ejecuta cuando el token caduca (TTL 120 s). |
| `data-error-callback` | string | Función JS si la verificación falla. Recibe el error como argumento. |
| `data-response-field-name` | string | Nombre del input hidden que recibe el token. Default: `g-recaptcha-response` (compatibilidad reCAPTCHA). |
| `data-theme` | string | Tema visual del widget: `auto` (sigue `prefers-color-scheme` del visitante), `light` o `dark`. Si no se indica, usa el configurado en el dashboard del sitio (default `auto`). Ver [§7.4](#tema). |
| `data-checkbox-mode` | string | `"off"` reto directo · `"click"` checkbox + análisis al pulsar · `"auto"` checkbox que se puede marcar solo. Si se omite, se usa lo configurado en el dashboard. Ver [§7.5](#checkbox). |
| `data-checkbox-first` | string | **Legacy**. `"1"` equivale a `"click"`, `"0"` a `"off"`. Usa `data-checkbox-mode` en su lugar. |

### 3.3. Callbacks

```html
<script>
function onCaptchaSuccess(token) {
    console.log('Verificado, token:', token);
}
function onCaptchaExpired() {
    console.log('Token expiró, repetir el captcha');
}
function onCaptchaError(err) {
    console.error('Error en captcha:', err);
}
</script>

<div class="softwacap"
     data-sitekey="pk_swl_..."
     data-callback="onCaptchaSuccess"
     data-expired-callback="onCaptchaExpired"
     data-error-callback="onCaptchaError"></div>
```

### 3.4. API JS pública

El widget expone una API global compatible con `grecaptcha`:

```js
// Renderizar manualmente
var widgetId = softwacap.render('mi-div', {
    sitekey: 'pk_swl_...',
    callback: function (token) { /* éxito */ }
});

// Obtener el token actual (vacío si no hay token o expiró)
var token = softwacap.getResponse(widgetId);

// Resetear y pedir nuevo challenge
softwacap.reset(widgetId);

// Esperar a que el widget esté cargado
softwacap.ready(function () {
    // listo
});
```

Aliases compatibles con reCAPTCHA: `window.grecaptcha.render(...)`, `getResponse(...)`, `reset(...)`, `ready(...)` funcionan idénticos a `softwacap.*`.

---

## 4. Verificación server-side (siteverify) <a id="siteverify"></a>

Tu backend valida el token recibido del formulario haciendo una petición POST a `/siteverify`. **Compatible drop-in con la API de Google reCAPTCHA**: misma estructura request/response.

### 4.1. Request

```
POST https://captcha.softwalabs.com/siteverify
Content-Type: application/x-www-form-urlencoded
```

| Parámetro | Tipo | Descripción |
|-----------|------|-------------|
| `secret` | string | **Obligatorio**. Tu `secret_key` (prefijo `sk_swl_`). |
| `response` | string | **Obligatorio**. El token recibido del formulario (`g-recaptcha-response`). |
| `remoteip` | string | Opcional. IP del usuario final (mejora behavioral scoring). |

También acepta `Content-Type: application/json` con el mismo body.

### 4.2. Response

```json
{
  "success": true,
  "score": 0.85,
  "action": "submit",
  "challenge_ts": "2026-04-26T10:30:00Z",
  "hostname": "midominio.com"
}
```

| Campo | Tipo | Descripción |
|-------|------|-------------|
| `success` | boolean | `true` si el token es válido y de un humano. `false` en cualquier otro caso. |
| `score` | float 0-1 | Probabilidad de ser humano. Acepta normalmente `> 0.5`. |
| `challenge_ts` | ISO8601 | Cuándo se resolvió el captcha. |
| `hostname` | string | Dominio donde se resolvió. Compáralo con tu dominio para defensa adicional. |
| `error-codes` | array | Sólo si `success=false`. Lista de motivos. Ver [§12](#errores). |

### 4.3. Validación recomendada

```pseudo
1. POST /siteverify con secret + response del usuario.
2. Si success=true Y score > 0.5 → procesar el formulario.
3. Si success=false → bloquear y mostrar el captcha de nuevo.
4. (Opcional) Verificar que hostname coincida con tu dominio para defensa en profundidad.
```

### 4.4. Token one-time-use

Cada token sólo se puede verificar **una vez**. Si lo intentas verificar de nuevo, recibes `error-codes: ["timeout-or-duplicate"]`. Esto previene replay attacks.

TTL del token: **120 segundos** desde que se emite. Si el usuario tarda más de 2 minutos en submit el formulario, el token expira y el widget muestra el captcha de nuevo.

---

## 5. Ejemplos por lenguaje <a id="ejemplos"></a>

### 5.1. PHP

```php
$response = $_POST['g-recaptcha-response'] ?? '';

$ch = curl_init('https://captcha.softwalabs.com/siteverify');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POSTFIELDS => http_build_query([
        'secret'   => $_ENV['SOFTWACAP_SECRET'],
        'response' => $response,
        'remoteip' => $_SERVER['REMOTE_ADDR'] ?? '',
    ]),
]);
$result = json_decode(curl_exec($ch), true);
curl_close($ch);

if (!empty($result['success']) && ($result['score'] ?? 0) > 0.5) {
    // Humano — procesar el formulario
} else {
    // Bloquear o pedir verificación adicional
    http_response_code(403);
}
```

### 5.2. Node.js (fetch nativo)

```js
const verifyCaptcha = async (token, ip) => {
    const r = await fetch('https://captcha.softwalabs.com/siteverify', {
        method: 'POST',
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
        body: new URLSearchParams({
            secret: process.env.SOFTWACAP_SECRET,
            response: token,
            remoteip: ip,
        }),
    });
    const data = await r.json();
    return data.success && data.score > 0.5;
};
```

### 5.3. Python

```python
import os
import requests

def verify_captcha(token: str, ip: str | None = None) -> bool:
    r = requests.post('https://captcha.softwalabs.com/siteverify', data={
        'secret': os.environ['SOFTWACAP_SECRET'],
        'response': token,
        'remoteip': ip or '',
    }, timeout=5)
    data = r.json()
    return bool(data.get('success')) and data.get('score', 0) > 0.5
```

### 5.4. Go

```go
package main

import (
    "encoding/json"
    "net/http"
    "net/url"
    "os"
    "strings"
)

type SiteVerifyResponse struct {
    Success    bool     `json:"success"`
    Score      float64  `json:"score"`
    Hostname   string   `json:"hostname"`
    ErrorCodes []string `json:"error-codes"`
}

func VerifyCaptcha(token, ip string) (bool, error) {
    body := url.Values{
        "secret":   {os.Getenv("SOFTWACAP_SECRET")},
        "response": {token},
        "remoteip": {ip},
    }
    resp, err := http.Post(
        "https://captcha.softwalabs.com/siteverify",
        "application/x-www-form-urlencoded",
        strings.NewReader(body.Encode()),
    )
    if err != nil { return false, err }
    defer resp.Body.Close()

    var v SiteVerifyResponse
    if err := json.NewDecoder(resp.Body).Decode(&v); err != nil {
        return false, err
    }
    return v.Success && v.Score > 0.5, nil
}
```

### 5.5. Ruby

```ruby
require 'net/http'
require 'uri'
require 'json'

def verify_captcha(token, ip = nil)
    uri = URI('https://captcha.softwalabs.com/siteverify')
    res = Net::HTTP.post_form(uri,
        secret:   ENV['SOFTWACAP_SECRET'],
        response: token,
        remoteip: ip.to_s
    )
    data = JSON.parse(res.body)
    data['success'] && (data['score'] || 0) > 0.5
end
```

### 5.6. .NET / C\#

```csharp
using System.Net.Http;
using System.Text.Json;

public async Task<bool> VerifyCaptchaAsync(string token, string ip)
{
    using var client = new HttpClient();
    var data = new FormUrlEncodedContent(new[]
    {
        new KeyValuePair<string, string>("secret",   Environment.GetEnvironmentVariable("SOFTWACAP_SECRET")!),
        new KeyValuePair<string, string>("response", token),
        new KeyValuePair<string, string>("remoteip", ip ?? "")
    });
    var resp = await client.PostAsync("https://captcha.softwalabs.com/siteverify", data);
    var json = await resp.Content.ReadAsStringAsync();
    using var doc = JsonDocument.Parse(json);
    return doc.RootElement.GetProperty("success").GetBoolean()
        && doc.RootElement.GetProperty("score").GetDouble() > 0.5;
}
```

### 5.7. Java

```java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class SoftwaCAPVerifier {
    public static boolean verify(String token, String ip) throws Exception {
        var body = "secret=" + System.getenv("SOFTWACAP_SECRET")
                 + "&response=" + java.net.URLEncoder.encode(token, "UTF-8")
                 + "&remoteip=" + (ip != null ? ip : "");
        var req = HttpRequest.newBuilder()
            .uri(URI.create("https://captcha.softwalabs.com/siteverify"))
            .header("Content-Type", "application/x-www-form-urlencoded")
            .POST(HttpRequest.BodyPublishers.ofString(body))
            .build();
        var resp = HttpClient.newHttpClient().send(req, HttpResponse.BodyHandlers.ofString());
        // Parsea con tu JSON lib favorita (Jackson, Gson…)
        return resp.body().contains("\"success\":true");  // simplificado
    }
}
```

---

## 6. Migración desde reCAPTCHA <a id="migracion"></a>

Si ya usas reCAPTCHA v2 o v3, la migración son **dos cambios**:

### 6.1. Frontend

```diff
- <script src="https://www.google.com/recaptcha/api.js"></script>
+ <script src="https://captcha.softwalabs.com/v1/api.js"></script>

  <div class="g-recaptcha" data-sitekey="..."></div>
  <!-- ↑ La clase g-recaptcha también funciona con SoftwaCAP -->
```

Opcional: cambia `class="g-recaptcha"` por `class="softwacap"` para mayor claridad. Ambos funcionan.

### 6.2. Backend

```diff
- https://www.google.com/recaptcha/api/siteverify
+ https://captcha.softwalabs.com/siteverify
```

**El formato del request y de la respuesta JSON es idéntico**. Tu lógica de validación no cambia. El campo `g-recaptcha-response` en el formulario se mantiene.

### 6.3. Compatibilidad de campos

| reCAPTCHA | SoftwaCAP | Notas |
|-----------|-----------|-------|
| `success` | `success` | Idéntico |
| `score` | `score` | Idéntico (0-1) |
| `action` | `action` | Soportado, valor por defecto `submit` |
| `challenge_ts` | `challenge_ts` | Idéntico (ISO 8601) |
| `hostname` | `hostname` | Idéntico |
| `error-codes` | `error-codes` | Idéntico (lista de strings) |

---

## 7. Configuración avanzada del sitio <a id="config-sitio"></a>

### 7.1. Niveles de dificultad

| Nivel | Estrellas en pantalla | Secuencia | Flash | Gap | Timeout | Replays |
|-------|----------------------|-----------|-------|-----|---------|---------|
| `easy` | 7 + 1 phantom | 3 | 550 ms | 300 ms | 15 s | 3 |
| `normal` (default) | 9 + 1 phantom | 3 | 380 ms | 180 ms | 12 s | 2 |
| `hard` | 9 + 1 phantom | 4 | 320 ms | 160 ms | 11 s | 1 |

Cambia la dificultad en `/dashboard/sites` editando el sitio.

### 7.2. Hostnames permitidos

Campo `domains` del sitio. CSV de hostnames donde el captcha puede cargarse. Si la petición viene de un dominio no listado, el endpoint `/v1/challenge` devuelve `hostname-not-allowed`.

Reglas:
- `midominio.com` → permite `midominio.com` Y todos sus subdominios.
- `*.midominio.com` → sólo subdominios.
- `*` → cualquier dominio (no recomendado en producción).

### 7.3. Rotar secret_key

Si crees que el secret está comprometido, en `/dashboard/sites` → botón **Rotar secret**. El nuevo secret se genera al instante. Tu backend deja de validar hasta que actualices `SOFTWACAP_SECRET`.

### 7.4. Tema (claro / oscuro) <a id="tema"></a>

El widget tiene tres modos de tema visual: **`auto`**, **`light`** y **`dark`**.

- **`auto`** (default) — el widget detecta `prefers-color-scheme` del navegador del visitante. Si el usuario tiene su SO/navegador en modo claro, el widget se renderiza claro; si está en oscuro, oscuro. Es el comportamiento más respetuoso con la preferencia del visitante.
- **`light`** — fuerza tema claro siempre, da igual el SO del visitante.
- **`dark`** — fuerza tema oscuro siempre.

Hay **dos vías** para configurarlo, con esta **prioridad**:

> **HTML del integrador** (`data-theme`) > **dashboard del propietario** (campo *Tema*) > **default `auto`**.

#### Vía 1 — desde el HTML (el integrador decide)

Añade el atributo `data-theme` al div del widget. Útil si el integrador quiere control fino, por ejemplo para alinear el widget con el tema concreto de su web.

```html
<!-- Auto: respeta el modo claro/oscuro del visitante -->
<div class="softwacap" data-sitekey="pk_swl_..." data-theme="auto"></div>

<!-- Forzar claro -->
<div class="softwacap" data-sitekey="pk_swl_..." data-theme="light"></div>

<!-- Forzar oscuro -->
<div class="softwacap" data-sitekey="pk_swl_..." data-theme="dark"></div>
```

#### Vía 2 — desde el dashboard (sin tocar el HTML)

Si el HTML del sitio está desplegado y no quieres modificarlo (o el integrador es un tercero), configura el tema desde tu dashboard:

`/dashboard/sites` → editar el sitio → campo **Tema del widget** → guardar.

El widget aplicará ese valor automáticamente en cuanto se cargue, sin necesidad de tocar nada en el HTML del cliente. **Si el HTML ya tiene `data-theme`**, ese gana — el dashboard solo se aplica como fallback.

#### Detección automática (modo `auto`)

El modo `auto` se basa en la media query CSS `prefers-color-scheme`, que el navegador resuelve a partir de:

1. El tema del SO (Windows / macOS / Android / iOS).
2. La preferencia explícita configurada en el navegador (Chrome/Firefox/Safari permiten forzar claro u oscuro independientemente del SO).
3. Extensiones que la fuercen (Dark Reader, etc.).

Si el visitante cambia el tema de su SO mientras tiene el widget abierto, el widget se actualiza al instante (variables CSS) sin recargar.

> **Recomendación**: deja el sitio en `auto` salvo que tu marca exija un tema concreto. Es la mejor experiencia para el visitante y reduce el riesgo de que el widget choque con tu CSS.

### 7.5. Checkbox "No soy un robot" (precheck) <a id="checkbox"></a>

Al estilo de reCAPTCHA v2, puedes hacer que el widget arranque con una **caja con checkbox "No soy un robot"** en lugar del reto de estrellas directo. Si el análisis behavioral confía en el visitante (score ≥ 0.6), se emite token sin reto. Si no convence, el mismo widget se transforma en el reto stellar.

#### 3 modos por sitio

| Modo | Comportamiento |
|------|-----------------|
| **`off`** (default) | No hay checkbox. El widget muestra el reto stellar inmediatamente. Compatible con clientes existentes. |
| **`click`** | Muestra checkbox. Al **pulsarlo**, evaluamos behavior + fingerprint con los signals frescos (incluye el timing del click y los mousemoves acumulados). Si convence → tick verde, sin reto. Si no → reto stellar en el mismo widget. |
| **`auto`** | Muestra checkbox y **además evaluamos al cargar la página**. Si los signals iniciales ya convencen, marcamos solo sin que el visitante toque nada. Si no, igual que `click`: al pulsar reintentamos. |

**Recomendación**:
- `auto` es el más conversion-friendly pero requiere que el visitante haya interactuado con la página antes de cargar el widget (mousemoves, scroll). Mejor para landing pages.
- `click` es la opción equilibrada — siempre hay un click, pero no hay reto si los signals son buenos. Ideal para formularios al final de la página.

#### Cómo funciona internamente

1. **Al montar el widget** llama a `POST /v1/precheck`. El server siempre crea un challenge en BD (por si lo necesitamos después). Si el modo es `auto`, evalúa el `behavior_score`; si es `click`, no evalúa todavía.
2. **Si `verdict=pass`** → emitimos token, marcamos el challenge como `solved`, el widget anima el tick verde.
3. **Si `verdict=challenge`** → guardamos los datos del reto en el cliente y el widget queda esperando click.
4. **Al pulsar el checkbox** el widget hace un **segundo `POST /v1/precheck`** con `challenge_id` (reusa el mismo) + `force=1` + signals frescos. Si convence → tick. Si no → el card se transforma en el reto stellar usando los datos guardados.

**Sin IA en el precheck**: solo behavior + fingerprint + UA. No consume créditos de IA aunque haya muchos visitantes.

#### Vía 1 — desde el HTML

```html
<!-- Modo click: checkbox; análisis al pulsar -->
<div class="softwacap" data-sitekey="pk_swl_..." data-checkbox-mode="click"></div>

<!-- Modo auto: checkbox que puede marcarse solo -->
<div class="softwacap" data-sitekey="pk_swl_..." data-checkbox-mode="auto"></div>

<!-- Modo off: reto directo -->
<div class="softwacap" data-sitekey="pk_swl_..." data-checkbox-mode="off"></div>
```

#### Vía 2 — desde el dashboard

`/dashboard/sites` → editar el sitio → **Modo checkbox** → elegir Off / Click / Auto → guardar.

A partir de ahí, todos los widgets que usen ese sitekey arrancan en el modo configurado, **sin tocar el HTML del integrador**.

#### Prioridad

> **HTML `data-checkbox-mode`** > **dashboard del propietario** > **default `off`**.

#### Score y umbrales

El umbral del precheck **varía con la dificultad del sitio** (`/dashboard/sites` → Dificultad base):

| Dificultad | Umbral precheck | Comportamiento |
|------------|-----------------|----------------|
| `easy`   | **0.5** | Permisivo. Auto-pase frecuente para humanos normales. |
| `normal` | **0.6** | Equilibrado. Default. |
| `hard`   | **0.7** | Estricto. Solo humanos con signals muy claros pasan sin reto. |

(Frente al `0.5` del flow con reto stellar; el precheck es igual de exigente que el reto en `normal` y más estricto en `hard`, porque sin los signals de los taps el server no puede confirmar la atención del visitante.)

- En modo `auto` el widget espera **2000 ms tras el mount** antes de hacer el primer precheck, para que los signals (mousemoves, fingerprint) tengan tiempo de llenarse. Con menos ventana casi ningún humano pasaría en el primer intento — la mayoría aún no ha movido el ratón al cargar la página.
- Cualquier headless detectado (`HeadlessChrome`, `Selenium`, `Puppeteer`, `Playwright`, `WebDriver`, etc.) se penaliza fuerte y nunca pasa el precheck → cae al reto.
- El honeypot phantom y el rate limit por IP/sitekey siguen aplicándose en ambos flujos.
- Default `off` para no romper a clientes ya integrados.

### 7.6.bis. Browser proof <a id="browser-proof"></a>

Cuando el widget pide `/v1/challenge-play` (la llamada que revela la secuencia del reto), debe demostrar que se está ejecutando en un browser real. Para ello hashea el `proof_seed` (que el server entregó en `/v1/challenge`) junto con primitivas que sólo existen en browsers reales:

- `canvas`: huella generada con `Canvas2D` (`<canvas>.toDataURL()`).
- `webgl`: renderer string vía `gl.getParameter(gl.RENDERER)` (con fallback a `WEBGL_debug_renderer_info`).
- `audio_sr`: `AudioContext.sampleRate`.
- `ua`: `navigator.userAgent`.

El server recalcula `sha256(seed | canvas | webgl | audio_sr | ua)` y solo entrega la secuencia si coincide. **Curl puro queda bloqueado**: necesita ejecutar JS en un browser (o headless con WebGL/Audio activos) para construir el hash. Es una capa transparente — no la configuras, ya está activa siempre.

### 7.6. Enlace SoftwaCAP en el footer del widget <a id="branding"></a>

El widget muestra siempre la marca **SoftwaCAP** en su pie. El comportamiento del enlace depende del plan y de la configuración del sitio:

| Plan | Sitio `Mostrar enlace` | Resultado |
|------|------------------------|-----------|
| Free | (forzado ON) | Enlace activo, anchor variado |
| Paid | ON | Enlace activo, anchor variado |
| Paid | OFF | Solo texto "SoftwaCAP", sin enlace |

#### Diversidad de anchors (anti footer-link spam)

Si todos los integradores tuvieran el mismo `<a>SoftwaCAP</a>`, Google detectaría el patrón como **footer link spam** y devaluaría los enlaces. Para evitarlo, el footer rota entre **decenas de variantes** de anchor + URL destino, repartidas en cuatro categorías:

- **brand** — "SoftwaCAP", "captcha.softwalabs.com", "Powered by SoftwaCAP"…
- **generic** — "Verificación humana", "Captcha europeo", "Anti-bots para formularios"…
- **partial** — "Protección anti-bot por SoftwaCAP", "Captcha con IA · SoftwaCAP"…
- **keyword** — "Captcha resistente a IA", "Alternativa a reCAPTCHA", "Captcha con MCP"…

#### Determinismo por sitio

A cada sitio se le asigna **siempre la misma variante** mediante `crc32(site_id) mod count(variantes_activas)`. Eso significa que:

- Tu web siempre verá el mismo anchor + URL en el footer del widget.
- Diferentes sitios verán anchors distintos → la distribución global produce un perfil natural de backlinks.
- Si revoco una variante (soft-delete), los sitios pueden cambiar de variante en ese caso particular. No es un problema: Google indexa el estado actual del crawl, no el histórico.

#### Atributos del enlace

Los `<a>` del footer se renderizan con:

```html
<a href="https://captcha.softwalabs.com/..." target="_blank" rel="noopener">{anchor}</a>
```

- **`rel="noopener"`** por seguridad de la pestaña de destino. No afecta SEO.
- **Sin `nofollow` ni `sponsored`** → es **dofollow**, pasa autoridad SEO al máximo.
- El widget aporta valor real al sitio que lo integra (verificación anti-bot), por lo que el link es legítimo.

#### Activar / desactivar (paid)

`/dashboard/sites` → editar → toggle **"Enlace SoftwaCAP en el widget"**. En Free el toggle aparece deshabilitado (siempre ON).

### 7.7. Proof-of-Work (Business+) <a id="pow"></a>

Capa adicional para clientes con tráfico de bot serio. Cuando está activa, el navegador del visitante debe **resolver un sha256 con N bits a cero** antes de que el server emita el token. Sube el coste por solve a un atacante a escala (tiene que ejecutar millones de hashes para cada captcha) sin afectar perceptiblemente a humanos.

#### 4 modos por sitio

| Modo | Bits | Tiempo cliente típico | Cuándo usar |
|------|------|------------------------|-------------|
| `off` (default) | 0 | 0 ms | Sin PoW |
| `lite` | 16 | ~10–50 ms | Coste minúsculo, frena scripts simples |
| `medium` | 20 | ~150–500 ms | Equilibrio razonable; perceptible pero no molesto |
| `hard` | 24 | ~2–10 s | Anti-scraping agresivo. Sitios de alto valor (banca, ecommerce premium) |

#### Cómo funciona

1. Al crear el challenge, el server genera un `pow_seed` aleatorio.
2. Lo devuelve junto con `pow_bits` en la respuesta de `/v1/challenge` o `/v1/precheck`.
3. El widget arranca un `solvePow(seed, bits)` en background — busca un `nonce` tal que `sha256(seed + nonce)` empiece con `bits/4` ceros hex. El bucle cede el thread cada ~12 ms para no congelar la UI.
4. Cuando el visitante resuelve el reto / pulsa el checkbox, el widget incluye `pow_nonce` en `verify-client` o `precheck` (segundo).
5. El server recalcula y, si coincide, emite el token. Si no, devuelve verdict `challenge` aunque el behavioral score fuera bueno.

#### Activación

Solo plan **Business** o **Enterprise**. El select del dashboard aparece deshabilitado en Pro o inferior. También vía MCP con `update_site` (`pow_mode: 'lite'|'medium'|'hard'`).

#### Compatibilidad con `auto`

> **Importante**: cuando `pow_mode != off` y `checkbox_mode == auto`, el visitante **pierde el auto-pase**. El precheck inicial llega antes de que el PoW esté resuelto, así que el server devuelve `challenge`. Solo tras pulsar el checkbox el widget tiene tiempo de resolver y emitir el token. Si quieres el auto-pase, deja `pow_mode = off`.

---

## 8. Adaptive Pace · Subliminal Decoys · Phantom Honeypot <a id="anti-bot"></a>

### 8.1. Phantom Honeypot (siempre activo)

Cada challenge incluye una **estrella señuelo** que el cliente renderiza igual que las demás pero **nunca se enciende**. Un humano sólo toca las que ve flashear. Un bot que escanee el DOM y haga click a todas las estrellas, cae instantáneamente con `error-codes: ["honeypot-triggered"]`.

Cero coste UX, cero configuración. **Activo por defecto en todos los sitios.**

### 8.2. Adaptive Pace (Pro+)

El motor recalcula los timings reales (`flash_ms`, `gap_ms`, `seq_len`) en función de la **tasa de éxito de tus humanos** en los últimos 7 días.

**Cómo decide el ajuste**:
- Sólo cuenta verificaciones con `behavior_score ≥ 0.6` Y `ai_score ≥ 0.5`. Los bots no envenenan la métrica.
- Mínimo **30 muestras humanas** en 7 días. Mientras tanto usa el baseline del sitio.
- Tasa de éxito humano `< 60%` → relaja: +20% al flash, +20% al gap, −1 a la secuencia (mínimo 3).
- Tasa `> 95%` → endurece: −10% al flash, −10% al gap, +1 a la secuencia (máximo 5).
- Recálculo cada 5 minutos; cache en BD por sitio.

**Cómo activarlo**:
- En `/dashboard/sites` al crear/editar un sitio: checkbox "Adaptive Pace".
- Vía MCP: `update_site` con `adaptive_pace: true`.

### 8.3. Subliminal Decoys (Pro+, opt-in por sitio)

Durante la fase de observación, el widget pinta micro-flashes de **1 frame** (~16 ms a 60Hz) en estrellas no-secuencia, **directamente en un canvas overlay** (no en el DOM).

- Para humanos: imperceptibles (flicker fusion threshold).
- Para bots con DOM access (Selenium, Playwright leyendo classes): invisibles, no impactan.
- Para bots con screen capture + VLM (GPT-4V, Gemini Vision): los frames se capturan y la secuencia que ven es errónea → fallan.

Respeta `prefers-reduced-motion`: si el usuario tiene esa preferencia activada, los decoys se desactivan automáticamente para evitar fotosensibilidad.

Activable en `/dashboard/sites` por sitio. Default off.

---

## 9. MCP server (Pro+) <a id="mcp"></a>

SoftwaCAP expone un endpoint MCP (Model Context Protocol) en `https://captcha.softwalabs.com/mcp`. Permite operar tu cuenta desde **Claude Desktop**, **Claude Code** u otros clientes IA.

### 9.1. Crear API key

1. `/dashboard/settings` → sección **API Keys (MCP)**.
2. Click "+ Crear nueva API key".
3. Pon un nombre descriptivo y elige scopes (`read`, opcionalmente `write`).
4. **Cópiala — sólo se muestra una vez**. Formato: `ak_swl_<32 chars>`.

### 9.2. Configurar el cliente MCP

Tres caminos según el cliente que uses. **Antes de configurar**, prueba la key con curl para verificar que funciona:

```bash
curl -s -X POST https://captcha.softwalabs.com/mcp \
  -H "Authorization: Bearer ak_swl_TU_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"list_sites","arguments":{}}}'
```

Si devuelve un JSON con tus sitios, la key está OK. Si responde `Unauthorized: invalid or missing Bearer API key`, revisa que la copiaste completa (39 caracteres con prefijo `ak_swl_`) y que no está revocada.

#### 9.2.A. Claude Code (CLI)

Comando único:

```bash
claude mcp add softwacap https://captcha.softwalabs.com/mcp \
  --transport http \
  --header "Authorization: Bearer ak_swl_TU_API_KEY"
```

O editando manualmente `~/.claude.json` (Linux/macOS) o `%USERPROFILE%\.claude.json` (Windows):

```json
{
  "mcpServers": {
    "softwacap": {
      "type": "http",
      "url": "https://captcha.softwalabs.com/mcp",
      "headers": {
        "Authorization": "Bearer ak_swl_TU_API_KEY"
      }
    }
  }
}
```

Reinicia Claude Code. Verifica con `/mcp` que `softwacap` aparece como conectado.

#### 9.2.B. Claude Desktop

Edita `~/.config/claude/claude_desktop_config.json` (Linux/macOS) o `%APPDATA%\Claude\claude_desktop_config.json` (Windows):

```json
{
  "mcpServers": {
    "softwacap": {
      "url": "https://captcha.softwalabs.com/mcp",
      "headers": {
        "Authorization": "Bearer ak_swl_TU_API_KEY"
      }
    }
  }
}
```

Reinicia Claude Desktop. El asistente verá las tools automáticamente.

#### 9.2.C. Otros clientes MCP

Cualquier cliente MCP-compatible funciona apuntando al endpoint `https://captcha.softwalabs.com/mcp` con `Authorization: Bearer ak_swl_...` en los headers. Transporte: **Streamable HTTP** (JSON-RPC 2.0 sobre POST). No necesita SSE.

### 9.3. Tools disponibles

#### Lectura (Pro+, requiere scope `read`)

| Tool | Descripción |
|------|-------------|
| `list_sites` | Lista todos tus sitios con sitekey, dominios, dificultad y estado. |
| `get_site` | Detalles de un sitio por id o sitekey, **incluye el secret_key**. |
| `get_quota` | Uso del mes y límite del plan. |
| `get_stats` | Estadísticas agregadas (total, % éxito, scores promedio) por periodo. |
| `recent_verifications` | Últimas verificaciones, filtrable por sitio o sólo fallos. |
| `get_billing_info` | Plan actual, ciclo, próxima renovación. |

#### Escritura (Pro+, requiere scope `write`)

| Tool | Descripción |
|------|-------------|
| `create_site` | Crea un sitio nuevo y devuelve sitekey + secret_key. |
| `update_site` | Modifica nombre, dominios, dificultad o estado activo. |
| `rotate_secret` | Invalida el secret_key actual y genera uno nuevo. |
| `delete_site` | Elimina un sitio y todo su histórico. |

#### Avanzadas (Business+)

| Tool | Descripción |
|------|-------------|
| `simulate_verify` | Simula con perfil `human` / `bot` / `borderline` y devuelve el score que daría. Útil para tunear umbrales. |
| `bulk_export_csv` | Exporta hasta 50.000 verificaciones como CSV. |

### 9.4. Ejemplos de prompts

```
Lista mis sitios y dime cuáles tuvieron mayor tasa de fallo este mes.
Crea un sitio nuevo para midominio.com en easy con Adaptive Pace activado.
Simula una verificación con perfil bot y otra con humano. Compara scores.
Exporta a CSV las verificaciones fallidas de los últimos 7 días del sitio #3.
Rota el secret del sitio #5 — sospecho que se filtró.
```

### 9.5. Plan gating

| Plan | MCP read | MCP write | MCP advanced | Campos write extra |
|------|----------|-----------|--------------|---------------------|
| Free | ❌ | ❌ | ❌ | — |
| Starter | ❌ | ❌ | ❌ | — |
| Pro | ✅ | ✅ | ❌ | `theme`, `checkbox_mode`, `show_branding`, `adaptive_pace`, `subliminal_decoys` |
| Business | ✅ | ✅ | ✅ | + `pow_mode` |
| Enterprise | ✅ | ✅ | ✅ | + `pow_mode` |

`create_site` y `update_site` aceptan estos extras como propiedades opcionales. Si un plan inferior intenta modificar un campo gated (ej. Pro intentando `pow_mode: 'lite'`), el server responde con código `-32003` y mensaje claro.

---

## 10. Planes, cuotas y límites <a id="planes"></a>

| Plan | Precio | Verif./mes | Sitios | Enlace SoftwaCAP | MCP | Adaptive Pace · Subliminal Decoys | Proof-of-Work |
|------|--------|------------|--------|------------------|-----|------------------------------------|---------------|
| **Free** | 0 € | 1.000 | 1 | Forzado (anchor variado) | ❌ | ❌ | ❌ |
| **Starter** | 9 € / mes | 50.000 | 5 | Toggle | ❌ | ❌ | ❌ |
| **Pro** | 39 € / mes | 500.000 | Ilimitado | Toggle | ✅ | ✅ | ❌ |
| **Business** | 149 € / mes | 5.000.000 | Ilimitado | Toggle | ✅ + advanced | ✅ | ✅ |
| **Enterprise** | A medida | Ilimitado | Ilimitado | Toggle | ✅ + advanced | ✅ | ✅ |

**Capacidades transversales (todos los planes)**: tema claro/oscuro/auto · checkbox "no soy un robot" (modos off/click/auto) · validación cruzada de signals · anti-farm por canvas+UA · honeypot phantom · análisis IA propio · datos en la UE.

Plan anual: **−17%** (equivalente a 2 meses gratis).

**Comportamiento al exceder cuota**: el `/siteverify` devuelve `error-codes: ["quota-exceeded"]` y `success: false` hasta el siguiente periodo o un upgrade. El widget sigue cargando para tu usuario, pero no validará.

**Aviso de cuota baja**: a partir del 80% del límite, ves una alerta en `/dashboard`. Si activas notificación por email en `/dashboard/settings`, recibes un correo al llegar al 80% y al 100%.

---

## 11. Rate limits <a id="rate-limits"></a>

| Endpoint | Límite | Ventana | Bloqueo si excede |
|----------|--------|---------|-------------------|
| `POST /v1/challenge` por IP | 30 req | 60 s | 120 s |
| `POST /v1/challenge` por sitekey | 200 req | 60 s | 60 s |
| `POST /v1/challenge-play` por IP | 60 req | 60 s | 120 s |
| `POST /v1/precheck` por IP (anti-burst) | 8 req | 5 s | 30 s |
| `POST /v1/precheck` por IP | 30 req | 60 s | 120 s |
| `POST /v1/precheck` por sitekey | 200 req | 60 s | 60 s |
| `POST /v1/verify-client` por IP | 40 req | 60 s | 180 s |
| `POST /siteverify` por IP | 200 req | 60 s | 60 s |
| `POST /api/chat` por IP | 30 req | 60 s | 120 s |
| `POST /mcp` por API key | 600 req | 60 s | 60 s |
| Login fallidos por IP+email | 8 intentos | 600 s | 900 s |

Si una IP supera estos límites, devuelve `HTTP 429` con `error-codes: ["rate-limited"]`.

---

## 12. Códigos de error <a id="errores"></a>

### 12.1. siteverify

| Código | Significado |
|--------|-------------|
| `missing-input-secret` | No se envió el parámetro `secret`. |
| `invalid-input-secret` | El `secret` no coincide con ningún sitio o el sitio está inactivo. |
| `missing-input-response` | No se envió el parámetro `response`. |
| `invalid-input-response` | Token inválido, mal firmado o expirado. |
| `timeout-or-duplicate` | Token ya usado (los tokens son one-time-use). |
| `quota-exceeded` | Has superado tu cuota mensual. |
| `rate-limited` | Demasiadas peticiones desde tu IP. |

### 12.2. Endpoints internos del widget

> **Nota**: `/v1/challenge`, `/v1/challenge-play`, `/v1/verify-client` y `/v1/precheck` son endpoints **internos** del widget. Por seguridad devuelven al cliente un genérico `{"error": "rejected"}` con HTTP 400/403/429 y nunca los códigos específicos — un atacante no puede iterar para descubrir por qué falló (sequence-wrong vs honeypot-triggered, etc.).
>
> Las tablas que siguen son **referencia interna para troubleshooting de logs**: los motivos reales se escriben en `error_log` del servidor con el prefijo `[swcap.*]` para auditoría. Si integras SoftwaCAP, sólo necesitas la tabla §12.1 — el resto pertenece al runtime del widget, no a tu código.

#### /v1/challenge (interno)

| Código en log | Significado |
|--------|-------------|
| `invalid-sitekey` | Sitekey no encontrado o sitio inactivo. |
| `hostname-not-allowed` | El hostname del request no está en los `domains` permitidos del sitio. |
| `rate-limited` | Demasiados challenges desde tu IP o sitekey. |

#### /v1/verify-client (interno)

| Código en log | Significado |
|--------|-------------|
| `challenge-not-found` | El `challenge_id` no existe. |
| `challenge-already-used` | Ya se intentó verificar este challenge. |
| `challenge-expired` | El challenge superó su TTL (5 min). |
| `sitekey-mismatch` | El sitekey del request no coincide con el del challenge. |
| `sequence-wrong` | Las taps no coinciden con la secuencia esperada. |
| `honeypot-triggered` | El cliente tocó la estrella phantom (DOM trap). |
| `pow-failed` | Sitio con PoW activo y `pow_nonce` ausente o inválido. |
| `low-score` | El score combinado quedó por debajo del umbral (incluye penalty del SignalValidator). |

#### /v1/challenge-play (interno · browser proof)

| Código en log | Significado |
|--------|-------------|
| `browser-proof-required` | El widget no envió `proof`. Suele ser un curl puro. |
| `browser-proof-invalid` | `canvas` muy corto o `ua` vacío en `proof_components`. |
| `browser-proof-mismatch` | El hash sha256 del cliente no coincide con el recalculado por el server. |

#### /v1/precheck (interno)

| Código en log | Significado |
|--------|-------------|
| `invalid-sitekey` | Sitekey desconocido o sitio inactivo. |
| `hostname-not-allowed` | El hostname del request no está en `domains` del sitio. |
| `rate-limited` | Bucket `pc:burst:*`, `pc:ip:*` o `pc:sk:*` agotado. |

### 12.3. MCP

Códigos JSON-RPC 2.0:
- `-32700` Parse error
- `-32600` Invalid request
- `-32601` Method not found
- `-32602` Invalid params
- `-32603` Internal error
- `-32001` Unauthorized (Bearer key inválida o ausente)
- `-32002` Rate limit exceeded
- `-32003` Plan does not include this feature

---

## 13. FAQ y troubleshooting <a id="faq"></a>

### El widget no aparece

- Verifica que `data-sitekey` está presente y es exactamente el sitekey de tu sitio.
- Comprueba la consola del navegador. Si ves `invalid-sitekey` o `hostname-not-allowed`, el dominio actual no está en la lista de dominios permitidos del sitio.
- Asegúrate de que `<script src="https://captcha.softwalabs.com/v1/api.js">` carga sin error 404.

### `siteverify` siempre devuelve `success: false`

- Comprueba que envías `secret` (no el sitekey) en el campo `secret`.
- Confirma que el token (`response`) se envía completo, no truncado.
- Si recibes `timeout-or-duplicate`: el token ya se verificó. Es one-time-use.
- Si recibes `invalid-input-response`: token caducado (TTL 120 s) o malformado.

### El captcha tarda mucho en cargar

- Toda la carga es desde `captcha.softwalabs.com` (api.js + fuentes self-hosted, sin Google Fonts ni terceros). ~120 KB con cache vacía.
- Tras el primer load, el `api.js` se cachea en el navegador (1 hora) y las fuentes (1 año).

### "Sesión expirada" en login/dashboard

- Tu cookie de sesión caducó. Recarga y vuelve a entrar.
- Si te ocurre constantemente, comprueba que tu navegador acepta cookies de `captcha.softwalabs.com`.

### Adaptive Pace dice "aún sin datos suficientes"

Necesita 30 sesiones humanas (con `behavior_score ≥ 0.6` y `ai_score ≥ 0.5`) en los últimos 7 días para empezar a ajustar. Mientras tanto usa el baseline del nivel de dificultad elegido.

### MCP responde 401 Unauthorized

- Comprueba que el header `Authorization: Bearer ak_swl_...` está presente.
- Verifica que tu plan incluye MCP (Pro+).
- La API key puede haber sido revocada. Genera una nueva en `/dashboard/settings`.

### ¿Cómo cancelo mi suscripción?

`/dashboard/billing` → **Gestionar facturación** → te lleva al portal de Stripe. Cancela ahí; la cancelación es efectiva al final del ciclo actual.

### ¿Self-hosted disponible?

Sí, en plan Enterprise. Stack: PHP puro + MySQL/MariaDB, sin frameworks. Contacta `info@heramarth.com`.

---

## Soporte

- Email: info@heramarth.com
- Chatbot IA en la propia web (logueado o no)
- Telegram (Plan Business+)

## Empresa

SoftwaLabs — Plataforma de servicios de IA y SaaS.
Cuenta principal en `softwalabs.com`. SoftwaCAP es uno de sus productos.

---

*Documentación versión 1.0 · 2026 · Datos en la UE*
