Zum Inhalt springen

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.

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)
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.
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
}
}
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"
}
}
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 });
});