Menú de la documentación

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.

Ver como Markdown
Actualizado el

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_….

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:

app/api/flowconsent-webhook/route.ts
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')
}

Los eventos

consent.recorded — formato exacto de entrega
json
{
  "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.

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.