WEBHOOKS.md
Rama main · réplica de lectura del repositorio original.
# § 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 });
});
```