Les webhooks de consentement poussent chaque décision vers votre serveur sous forme de POST HTTP signé : consent.recorded quand un visiteur accorde au moins une catégorie, consent.withdrawn quand il refuse ou se rétracte. C'est ce qui rend le droit de retrait applicable côté serveur : quand un visiteur se rétracte, votre serveur l'apprend et peut purger ou cesser le traitement.
Créer un webhook
Deux chemins, même contrat :
- Dans l'app : l'assistant de configuration sur la page Intégration du Builder : saisissez l'URL de réception (endpoint), avec la possibilité de limiter le webhook à une seule bannière (par défaut : tout l'espace de travail).
- Via MCP : l'outil
configure_consent_webhook, si vous pilotez FlowConsent depuis Claude ou un autre client MCP.
Dans les deux cas, vous recevez un secret de signature au format whsec_….
La signature
Chaque livraison porte un en-tête X-FlowConsent-Signature, sur le modèle de Stripe :
X-FlowConsent-Signature: t=1722326400,v1=5f8a1c…
où t est un horodatage Unix et v1 = HMAC-SHA256(secret, "<t>.<corps brut>"). L'horodatage limite le rejeu : rejetez les livraisons dont le t est trop ancien.
Vérifier avec le SDK serveur
@flowconsent/server-sdk fournit verifyWebhookSignature : comparaison en temps constant, tolérance de rejeu de 300 s par défaut :
import { verifyWebhookSignature } from '@flowconsent/server-sdk'
export async function POST(request: Request) {
const rawBody = await request.text() // corps brut, avant tout 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') // événement de test du wizard : jamais un vrai consentement
if (event.type === 'consent.withdrawn') {
// arrêter les traitements pour event.visitor_id, purger les systèmes aval…
}
return new Response('ok')
}Les événements
{
"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"
}typevautconsent.recordedquand au moins une des catégoriesanalytics,marketing,preferencesest accordée ; sinonconsent.withdrawn.bannerest le code de licence public de la bannière : les identifiants internes ne sont jamais exposés.visitor_idest l'identifiant pseudonyme que l'on retrouve dans les journaux de consentement et dans ledecision.visitorIddu SDK serveur.occurred_atest en ISO 8601.
Tester la livraison
Le « test d'envoi » de l'assistant envoie un événement consent.test à votre URL de réception, signé avec votre vrai secret et portant un marqueur explicite "test": true : ne le traitez jamais comme un consentement réel. Il confirme, de bout en bout, que votre serveur reçoit, vérifie et répond. Le résultat de livraison (statut HTTP, durée) s'affiche dans l'app.
Livraison et réessais
- 3 tentatives par livraison, avec un délai croissant entre elles (250 ms, 500 ms, 1 s) et un délai maximal de 5 s chacune.
- Une réponse
4xx(hors429) arrête les réessais : réessayer n'y changerait rien. - Répondez
2xxvite et traitez en asynchrone : tout le reste compte comme une livraison échouée. - Le statut et l'heure de la dernière livraison sont visibles sur chaque webhook dans l'app.