Ctrl K
ComunidadGuías…

WEBHOOKS.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# § Paragrafy Webhook Referenz & Spezifikation

Dieses Dokument beschreibt die Webhook-Schnittstelle von Paragrafy (v1.6.2) zur automatisierten Synchronisation von Rechtstexten (AGB, Datenschutzerklärung, Impressum etc.) mit angebundenen Web- und Mobile-Anwendungen.

**Zustellung ist asynchron:** Veröffentlichungen legen den Webhook in einer Warteschlange ab statt ihn sofort zu senden, damit ein langsamer oder nicht erreichbarer Empfänger niemals das Speichern eines Rechtstexts blockiert. Ein externer Cron-Job muss `/api/cron/webhooks` regelmäßig aufrufen (empfohlen: alle 5 Minuten), damit die Warteschlange abgearbeitet wird — ohne diesen Cron werden Webhooks nur zugestellt, wenn im Admin-Bereich manuell auf „Jetzt abarbeiten" geklickt wird. Fehlgeschlagene Zustellungen werden bis zu 5-mal mit steigendem Abstand (1 / 5 / 15 / 60 / 180 Minuten) wiederholt, mit einem Timeout von 5 Sekunden pro Versuch.

---

## 1. HTTP-Header & Authentifizierung

Jeder von Paragrafy versendete Webhook wird als `POST`-Request mit folgendem Header-Schema übermittelt:

| Header | Beschreibung | Beispiel |
| :--- | :--- | :--- |
| `Content-Type` | MIME-Type des Payloads | `application/json` |
| `User-Agent` | Client-Identifikator | `Paragrafy-Webhook/2026.9.1` |
| `X-Paragrafy-Event` | Event-Typ | `legal_text.updated` / `legal_text.scheduled` |
| `X-Paragrafy-Signature` | HMAC-SHA256 Signatur des rohen Body-Strings | `a3f8e... (hex)` *(nur wenn Secret gesetzt)* |

---

## 2. Event-Typen im Überblick

| Event | Auslöser | Einsatzzweck in deiner App |
| :--- | :--- | :--- |
| `legal_text.scheduled` | Eine Textänderung wurde für einen zukünftigen Zeitpunkt geplant. | Vorankündigungs-Banner mit **Vorschau-Link** für Nutzer anzeigen (*„AGB ändern sich zum 31.08. [Jetzt Vorab-Fassung lesen]“*). |
| `legal_text.updated` | Ein Rechtstext wurde sofort live veröffentlicht, ein geplanter Stichtag wurde erreicht, oder eine frühere Version wurde wiederhergestellt. | Neue AGB-Zustimmung im User-Account erzwingen, App-Cache invalidieren. |

---

## 3. Payload-Spezifikation

### A. Event: `legal_text.scheduled` (Vorankündigung mit Vorschau-Link)

Wird gefeuert, wenn im Editor eine zeitgesteuerte Live-Schaltung für die Zukunft geplant wird. `url`/`api_url` verweisen weiterhin auf die aktuell live sichtbare Fassung (unverändert bis zum Stichtag). Zusätzlich liefert Paragrafy `preview_url`/`preview_api_url` — unter dieser Adresse ist die **geplante Neufassung** schon vor dem Stichtag öffentlich einsehbar (dieselbe Seite hängt lediglich `/preview` an), z. B. um Nutzer:innen vorab über anstehende AGB-Änderungen zu informieren. Die Vorschau-Seite wird mit `noindex` ausgeliefert und verschwindet automatisch, sobald die Fassung live geht (der Pfad zeigt danach wieder 404, weil keine Planung mehr vorliegt).

#### JSON-Payload:
```json
{
  "event": "legal_text.scheduled",
  "timestamp": "2026-08-30T15:30:00+02:00",
  "project": {
    "id": 1,
    "name": "MeinProjekt",
    "domain": "legal.deinedomain.de"
  },
  "data": {
    "document_id": 3,
    "slug": "agb-b2c",
    "lang": "de",
    "title": "AGB (Endkunden / B2C)",
    "status": "scheduled",
    "change_note": "Aktualisierung der Zahlungsbedingungen zum 31.08.",
    "scheduled_at": "2026-08-31T00:00:00+02:00",
    "effective_date": "2026-08-31T00:00:00+02:00",
    "url": "https://legal.deinedomain.de/de/agb-b2c",
    "api_url": "https://legal.deinedomain.de/api/de/agb-b2c",
    "preview_url": "https://legal.deinedomain.de/de/agb-b2c/preview",
    "preview_api_url": "https://legal.deinedomain.de/api/de/agb-b2c/preview",
    "was_scheduled": false
  }
}
```

---

### B. Event: `legal_text.updated` (Live-Veröffentlichung)

Wird gefeuert, sobald ein Rechtstext aktiv geschaltet wurde (sofort oder nach Ablauf des Stichtags).

#### JSON-Payload:
```json
{
  "event": "legal_text.updated",
  "timestamp": "2026-08-30T15:45:00+02:00",
  "project": {
    "id": 1,
    "name": "MeinProjekt",
    "domain": "legal.deinedomain.de"
  },
  "data": {
    "document_id": 3,
    "slug": "agb-b2c",
    "lang": "de",
    "title": "AGB (Endkunden / B2C)",
    "status": "published",
    "change_note": "Aktualisierung der Zahlungsbedingungen zum Monatsende",
    "was_scheduled": true,
    "effective_date": "2026-08-30T15:45:00+02:00",
    "url": "https://legal.deinedomain.de/de/agb-b2c",
    "api_url": "https://legal.deinedomain.de/api/de/agb-b2c",
    "updated_at": "2026-08-30T15:45:00+02:00"
  }
}
```

---

## 4. Felddefinitionen (Daten-Mapping)

| Feldname | Typ | Bedeutung |
| :--- | :--- | :--- |
| `data.title` | `string` | Der Titel in der jeweiligen Zielsprache. |
| `data.slug` | `string` | Eindeutiger Bezeichner (`agb-b2c`, `datenschutz`, `impressum`). |
| `data.lang` | `string` | 2-stelliger Sprachcode (`de`, `en`, `es`, `fr` etc.). |
| `data.effective_date` | `string` (ISO 8601) | **Inkrafttretungsdatum** (bei Live sofort, bei Scheduled der Stichtag). |
| `data.scheduled_at` | `string` (ISO 8601) | Nur bei `scheduled`: Der geplante Umschaltzeitpunkt. |
| `data.url` | `string` | URL der aktuell gültigen Live-Version. |
| `data.api_url` | `string` | JSON-API URL der aktuell gültigen Live-Version. |
| `data.preview_url` | `string` | Nur bei `scheduled`: Öffentliche Vorschau-URL der geplanten Neufassung (`/preview`-Suffix), noch vor dem Stichtag abrufbar. |
| `data.preview_api_url` | `string` | Nur bei `scheduled`: JSON-API-Variante der Vorschau-URL. |
| `data.was_scheduled` | `boolean` | `true`, falls diese Veröffentlichung aus einer Planung hervorging. |
| `data.change_note` | `string` | Die vom Admin vergebene Revisionsnotiz. |

---

## 5. Implementierungsbeispiel in TypeScript / Node.js

```typescript
import express, { Request, Response } from 'express';
import crypto from 'crypto';

interface ParagrafyWebhookPayload {
  event: 'legal_text.updated' | 'legal_text.scheduled';
  timestamp: string;
  project: {
    id: number;
    name: string;
    domain: string;
  };
  data: {
    document_id: number;
    slug: string;
    lang: string;
    title: string;
    status: string;
    change_note?: string;
    was_scheduled?: boolean;
    scheduled_at?: string;
    effective_date: string;
    url: string;
    api_url: string;
    preview_url?: string;
    preview_api_url?: string;
    updated_at?: string;
  };
}

const app = express();
const WEBHOOK_SECRET = process.env.PARAGRAFY_WEBHOOK_SECRET || 'mein-webhook-secret';

app.post('/api/legal-webhook', express.raw({ type: 'application/json' }), (req: Request, res: Response) => {
  const signature = req.headers['x-paragrafy-signature'] as string;
  const rawBody = req.body.toString('utf8');

  // 1. Signatur validieren
  if (WEBHOOK_SECRET) {
    const expected = crypto.createHmac('sha256', WEBHOOK_SECRET).update(rawBody).digest('hex');
    const valid = signature && crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
    if (!valid) {
      return res.status(401).json({ error: 'Ungültige Signatur' });
    }
  }

  const payload: ParagrafyWebhookPayload = JSON.parse(rawBody);

  // 2. Event Routing
  switch (payload.event) {
    case 'legal_text.scheduled':
      // Vorankündigungs-Banner schalten mit Vorschau-Link!
      console.log(`[Vorankündigung] ${payload.data.title} ändert sich zum ${payload.data.scheduled_at}`);
      console.log(`Vorschau-Link für Nutzer: ${payload.data.preview_url}`);
      break;

    case 'legal_text.updated':
      // Neue Version ist aktiv: User-Consent anfordern
      console.log(`[Live] ${payload.data.title} ist jetzt in Kraft.`);
      break;
  }

  return res.status(200).json({ success: true });
});
```