Webhooks
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
Abschnitt betitelt „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
Abschnitt betitelt „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
Abschnitt betitelt „3. Payload-Spezifikation“A. Event: legal_text.scheduled (Vorankündigung mit Vorschau-Link)
Abschnitt betitelt „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).
{ "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)
Abschnitt betitelt „B. Event: legal_text.updated (Live-Veröffentlichung)“Wird gefeuert, sobald ein Rechtstext aktiv geschaltet wurde (sofort oder nach Ablauf des Stichtags).
{ "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)
Abschnitt betitelt „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
Abschnitt betitelt „5. Implementierungsbeispiel in TypeScript / Node.js“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 });});