# Consent-Webhooks: signierte Ereignisse an Ihren Server

Empfangen Sie consent.recorded und consent.withdrawn auf Ihrem Server, prüfen Sie die HMAC-Signatur mit dem SDK und testen Sie die signierte Zustellung.

> Canonical: https://www.flowconsent.com/de/doc/consent-webhooks
> Last updated: 2026-07-30
Consent-Webhooks übermitteln jede Einwilligungsentscheidung als signierten HTTP-POST an Ihren Server: `consent.recorded`, wenn ein Besucher mindestens eine Kategorie erteilt, `consent.withdrawn`, wenn er ablehnt oder widerruft. Das macht das Widerrufsrecht serverseitig durchsetzbar — widerruft ein Besucher, erfährt es Ihr Backend und kann löschen oder die Verarbeitung stoppen.

## Einen Webhook anlegen

Zwei Wege, derselbe Vertrag:

- **In der App** — der Einrichtungsassistent auf der Integrationsseite des Builders: Geben Sie Ihre Endpoint-URL ein, optional beschränkt auf ein einzelnes Banner (Standard: der ganze Workspace).
- **Über MCP** — das Tool `configure_consent_webhook`, wenn Sie FlowConsent aus Claude oder einem anderen MCP-Client steuern.

In beiden Fällen erhalten Sie ein Signatur-Secret im Format `whsec_…`.

> [!IMPORTANT]
> Das Secret wird nur **einmal** angezeigt, bei der Erstellung. Legen Sie es sofort in Ihrem Secret-Manager ab — geht es verloren, löschen Sie den Webhook und legen einen neuen an.

## Die Signatur

Jede Zustellung trägt einen `X-FlowConsent-Signature`-Header, im Stripe-Stil:

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

Dabei ist `t` ein Unix-Zeitstempel und `v1 = HMAC-SHA256(secret, "<t>.<roher Body>")`. Der Zeitstempel begrenzt Replay-Angriffe: Weisen Sie Zustellungen mit zu altem `t` ab.

## Mit dem Server-SDK prüfen

`@flowconsent/server-sdk` liefert `verifyWebhookSignature` — Vergleich in konstanter Zeit, standardmäßig 300 s Replay-Toleranz:

```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() // roher Body, vor jedem 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') // Test-Ereignis des Assistenten — nie eine echte Einwilligung

  if (event.type === 'consent.withdrawn') {
    // Verarbeitung für event.visitor_id stoppen, nachgelagerte Systeme bereinigen…
  }
  return new Response('ok')
}
```

> [!WARNING]
> Prüfen Sie die Signatur gegen den **rohen** Request-Body. Ein erneutes Serialisieren des JSON (`JSON.stringify(await request.json())`) verändert die Bytes, und die Prüfung schlägt fehl.

## Die Ereignisse

```json title="consent.recorded — exaktes Zustellformat"
{
  "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` ist `consent.recorded`, wenn mindestens eine der Kategorien `analytics`, `marketing`, `preferences` erteilt wurde; andernfalls `consent.withdrawn`.
- `banner` ist der öffentliche **Lizenzcode** des Banners — interne IDs werden nie offengelegt.
- `visitor_id` ist die pseudonyme Kennung, die auch in den Einwilligungsprotokollen und im `decision.visitorId` des Server-SDK erscheint.
- `occurred_at` ist ISO 8601.

> [!NOTE]
> Regressions-Alarme der [kontinuierlichen Überwachung](/de/doc/compliance-monitoring) kommen über dieselben Webhooks, als `compliance.regression`-Ereignisse — behandeln Sie unbekannte `type`-Werte fehlertolerant.

## Die Zustellung testen

Der „Test senden" des Assistenten schickt ein **`consent.test`**-Ereignis an Ihren Endpoint, signiert mit Ihrem echten Secret und mit einem expliziten Marker `"test": true` — behandeln Sie es nie als echte Einwilligung. Es bestätigt Ende-zu-Ende, dass Ihr Endpoint empfängt, prüft und antwortet. Das Zustellergebnis (HTTP-Status, Dauer) wird in der App angezeigt.

## Zustellung und Wiederholungen

- **3 Versuche** pro Zustellung, exponentielles Backoff (250 ms, 500 ms, 1 s zwischen den Versuchen), je 5 s Timeout.
- Eine `4xx`-Antwort (außer `429`) beendet die Wiederholungen — ein erneuter Versuch würde nichts ändern.
- Antworten Sie schnell mit `2xx` und verarbeiten Sie asynchron: Alles andere zählt als fehlgeschlagene Zustellung.
- Status und Zeitpunkt der letzten Zustellung sind bei jedem Webhook in der App sichtbar.
