# Webhooks de consentimiento: eventos firmados hacia tu servidor

Recibe los eventos consent.recorded y consent.withdrawn en tu servidor, verifica la firma HMAC con el SDK y prueba la entrega con un evento firmado.

> Canonical: https://www.flowconsent.com/es/doc/consent-webhooks
> Last updated: 2026-07-30
Los webhooks de consentimiento envían cada decisión a tu servidor como un POST HTTP firmado: `consent.recorded` cuando un visitante otorga al menos una categoría, `consent.withdrawn` cuando rechaza o se retracta. Esto es lo que hace aplicable el derecho de retirada en el lado servidor — cuando un visitante se retracta, tu backend se entera y puede purgar o detener el tratamiento.

## Crear un webhook

Dos caminos, el mismo contrato:

- **En la app** — el asistente de configuración en la página Integración del Builder: introduce la URL de tu endpoint, con la opción de limitar el webhook a un solo banner (por defecto: todo el workspace).
- **Vía MCP** — la herramienta `configure_consent_webhook`, si manejas FlowConsent desde Claude u otro cliente MCP.

En ambos casos recibes un secreto de firma con el formato `whsec_…`.

> [!IMPORTANT]
> El secreto se muestra **una sola vez**, al crearlo. Guárdalo de inmediato en tu gestor de secretos — si lo pierdes, elimina el webhook y crea uno nuevo.

## La firma

Cada entrega lleva una cabecera `X-FlowConsent-Signature`, al estilo Stripe:

```
X-FlowConsent-Signature: t=1722326400,v1=5f8a1c…
```

donde `t` es una marca de tiempo Unix y `v1 = HMAC-SHA256(secreto, "<t>.<cuerpo bruto>")`. La marca de tiempo acota el replay: rechaza las entregas cuyo `t` sea demasiado antiguo.

## Verificar con el SDK de servidor

`@flowconsent/server-sdk` incluye `verifyWebhookSignature` — comparación en tiempo constante, tolerancia de replay de 300 s por defecto:

```ts title="app/api/flowconsent-webhook/route.ts"
import { verifyWebhookSignature } from '@flowconsent/server-sdk'

export async function POST(request: Request) {
  const rawBody = await request.text() // cuerpo bruto, antes de cualquier JSON.parse
  const check = await verifyWebhookSignature(
    rawBody,
    request.headers.get('X-FlowConsent-Signature'),
    process.env.FLOWCONSENT_WEBHOOK_SECRET!,
  )
  if (!check.valid) {
    // check.reason: 'missing_header', 'malformed_header',
    // 'timestamp_out_of_tolerance', 'signature_mismatch'
    return new Response('invalid signature', { status: 400 })
  }

  const event = JSON.parse(rawBody)
  if (event.test) return new Response('ok') // evento de prueba del asistente — nunca un consentimiento real

  if (event.type === 'consent.withdrawn') {
    // detener el tratamiento para event.visitor_id, purgar los sistemas aguas abajo…
  }
  return new Response('ok')
}
```

> [!WARNING]
> Verifica la firma sobre el cuerpo **bruto** de la petición. Reserializar el JSON (`JSON.stringify(await request.json())`) cambia los bytes y la verificación fallará.

## Los eventos

```json title="consent.recorded — formato exacto de entrega"
{
  "id": "5f0e0a1c-…",
  "type": "consent.recorded",
  "banner": "FC-A1B2C3",
  "visitor_id": "v_8f3d…",
  "action": "accept_all",
  "categories": {
    "functional": true,
    "analytics": true,
    "marketing": false,
    "preferences": false
  },
  "services": { "google-analytics": true },
  "policy_version": "3",
  "occurred_at": "2026-07-30T09:41:22.000Z"
}
```

- `type` vale `consent.recorded` cuando al menos una de las categorías `analytics`, `marketing`, `preferences` está otorgada; en caso contrario, `consent.withdrawn`.
- `banner` es el **código de licencia** público del banner — los identificadores internos nunca se exponen.
- `visitor_id` es el identificador seudónimo que también aparece en los registros de consentimiento y en el `decision.visitorId` del SDK de servidor.
- `occurred_at` está en ISO 8601.

> [!NOTE]
> Las alertas de regresión de la [vigilancia continua](/es/doc/compliance-monitoring) llegan por los mismos webhooks, como eventos `compliance.regression` — gestiona sin error los valores de `type` desconocidos.

## Probar la entrega

El «enviar prueba» del asistente manda un evento **`consent.test`** a tu endpoint, firmado con tu secreto real y con un marcador explícito `"test": true` — nunca lo trates como un consentimiento real. Confirma, de extremo a extremo, que tu endpoint recibe, verifica y responde. El resultado de la entrega (estado HTTP, duración) se muestra en la app.

## Entrega y reintentos

- **3 intentos** por entrega, backoff exponencial (250 ms, 500 ms, 1 s entre intentos), timeout de 5 s cada uno.
- Una respuesta `4xx` (salvo `429`) detiene los reintentos — reintentar no cambiaría nada.
- Responde `2xx` rápido y procesa en asíncrono: cualquier otra cosa cuenta como entrega fallida.
- El estado y la hora de la última entrega son visibles en cada webhook dentro de la app.
