# Webhooks de consentement : des événements signés vers votre serveur

Recevez les événements consent.recorded et consent.withdrawn sur votre serveur, vérifiez la signature HMAC avec le SDK et testez la livraison signée.

> Canonical: https://www.flowconsent.com/fr/doc/consent-webhooks
> Last updated: 2026-07-30
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 backend 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 votre endpoint, avec la possibilité de limiter le webhook à une seule bannière (par défaut : tout le workspace).
- **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_…`.

> [!IMPORTANT]
> Le secret n'est montré qu'**une fois**, à la création. Rangez-le immédiatement dans votre gestionnaire de secrets — si vous le perdez, supprimez le webhook et créez-en un nouveau.

## La signature

Chaque livraison porte un en-tête `X-FlowConsent-Signature`, style Stripe :

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

où `t` est un horodatage Unix et `v1 = HMAC-SHA256(secret, "<t>.<corps brut>")`. L'horodatage borne 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 :

```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() // 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')
}
```

> [!WARNING]
> Vérifiez la signature sur le corps **brut** de la requête. Re-sérialiser le JSON (`JSON.stringify(await request.json())`) change les octets, et la vérification échouera.

## Les événements

```json title="consent.recorded — format exact de livraison"
{
  "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` vaut `consent.recorded` quand au moins une des catégories `analytics`, `marketing`, `preferences` est accordée ; sinon `consent.withdrawn`.
- `banner` est le **code licence** public de la bannière — les identifiants internes ne sont jamais exposés.
- `visitor_id` est l'identifiant pseudonyme que l'on retrouve dans les journaux de consentement et dans le `decision.visitorId` du SDK serveur.
- `occurred_at` est en ISO 8601.

> [!NOTE]
> Les alertes de régression de la [surveillance continue](/fr/doc/compliance-monitoring) arrivent sur les mêmes webhooks, sous forme d'événements `compliance.regression` — traitez les valeurs de `type` inconnues sans erreur.

## Tester la livraison

Le « test d'envoi » de l'assistant envoie un événement **`consent.test`** à votre endpoint, 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 endpoint 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, backoff exponentiel (250 ms, 500 ms, 1 s entre les tentatives), timeout de 5 s chacune.
- Une réponse `4xx` (hors `429`) arrête les réessais — réessayer n'y changerait rien.
- Répondez `2xx` vite 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.
