Mit Webhooks veröffentlichen
Sende jeden Artikel, den du in GrowWriter veröffentlichst, direkt an deine eigene Website. Richte die Webhook-Integration Schritt für Schritt ein, mit fertigem Code.
Deine Website läuft vielleicht auf keiner der Plattformen, die GrowWriter von Haus aus anbindet. Kein Problem. Mit der Webhook-Integration liefert GrowWriter jeden Artikel direkt an deine Website, sobald du auf Veröffentlichen klickst. Eigener Stack, selbst gebautes CMS, statische Seite, ganz egal. Wenn sie eine Web-Anfrage empfangen kann, kann sie deine Artikel empfangen.
Diese Anleitung führt dich durch die komplette Einrichtung. Du brauchst keine Webhook-Erfahrung. Am Ende legt jeder veröffentlichte GrowWriter-Artikel automatisch einen Beitrag auf deiner eigenen Website an oder aktualisiert ihn.
Was ist ein Webhook?
Ein Webhook ist die einfachste Art, wie zwei Anwendungen miteinander reden: Wenn in der einen etwas passiert, schickt sie eine Nachricht an eine Web-Adresse deiner Wahl.
Stell dir einen Lieferdienst vor. Du gibst GrowWriter deine Adresse (eine URL auf deiner Website). Sobald ein Artikel fertig ist, klopft GrowWriter an diese Tür und übergibt den kompletten Artikel als strukturierte Daten. Den Rest erledigt deine Website: speichern, formatieren, auf deine Art veröffentlichen.
Warum das besser ist als die Alternativen:
- Kein Copy-Paste. Der Artikel kommt mit Titel, Inhalt, Bildern und SEO-Feldern fertig sortiert an.
- Kein ständiges Nachschauen. Deine Website muss nicht alle paar Minuten fragen, ob es etwas Neues gibt. GrowWriter meldet sich nur, wenn es etwas zu liefern gibt.
- Keine Plattform-Bindung. Webhooks sprechen reines HTTPS und JSON, und das versteht jeder Web-Stack.
So funktioniert es
Den ganzen Ablauf verstehst du in zwanzig Sekunden:
- Du gibst GrowWriter eine URL deiner Website, und GrowWriter gibt dir dafür einen geheimen Schlüssel.
- Wenn du einen Artikel veröffentlichst, sendet GrowWriter eine
POST-Anfrage an deine URL. Der Anfrage-Body ist der komplette Artikel als JSON, und die Anfrage ist mit deinem Schlüssel signiert, damit du sicher sein kannst, dass sie wirklich von GrowWriter kommt. - Deine Website prüft die Signatur, speichert den Artikel und antwortet mit
einem Erfolgsstatus (irgendein
2xx). Fertig.
Schritt für Schritt einrichten
Verbinde den Webhook in GrowWriter
Öffne die Seite Veröffentlichen in GrowWriter und suche die Karte Webhook in der Gruppe Automatisierung. Klicke auf Verbinden.
Gib deine Endpoint-URL ein. Das ist die Adresse auf deiner Website, die
die Artikel empfängt, zum Beispiel https://example.com/webhooks/growwriter.
Zwei Regeln: Sie muss mit https:// beginnen und aus dem Internet erreichbar
sein (eine localhost-Adresse funktioniert nicht).
Der Endpoint existiert noch gar nicht? Kein Problem. GrowWriter prüft die URL an dieser Stelle nicht, du kannst also erst verbinden und direkt danach den Code schreiben.
Klicke auf Verbindung erstellen.

Kopiere deinen Signaturschlüssel
Der Dialog zeigt jetzt deinen Signaturschlüssel. Er beginnt mit whsec_
und beweist, dass eine Anfrage wirklich von GrowWriter stammt.
Klicke auf Schlüssel kopieren und lege ihn sicher auf deinem Server ab, üblicherweise als Umgebungsvariable:
GROWWRITER_WEBHOOK_SECRET=whsec_dein_schluessel_hierDieser Schlüssel wird nur ein einziges Mal angezeigt
GrowWriter speichert ihn verschlüsselt und kann ihn nicht erneut anzeigen. Falls du ihn verlierst, trenne den Webhook und verbinde ihn neu, um einen frischen Schlüssel zu bekommen. Neu verbinden erzeugt immer einen neuen Schlüssel, und der alte funktioniert nicht mehr.

Baue deinen Empfänger
Füge jetzt den Endpoint zu deiner Website hinzu. Er muss vier Dinge tun: den
rohen Anfrage-Body lesen, die Signatur prüfen, schnell mit einem 2xx-Status
antworten und den Artikel speichern.
Spring zu den Code-Beispielen weiter unten und kopiere das Beispiel, das zu deinem Stack passt. Stelle es unter der URL bereit, die du in Schritt 1 eingegeben hast.
Sende ein Testereignis
Zurück im GrowWriter-Dialog klickst du auf Testereignis senden. GrowWriter
schickt einen kleinen, signierten ping an deinen Endpoint. Er sieht so aus:
{
"version": "2026-08-01",
"event": "ping",
"deliveryId": "5f0c9a1e-4d2b-4f6a-9c3e-8b7d6a5e4f3c",
"sentAt": "2026-09-01T09:30:00.000Z"
}Beachte: Es ist kein Artikel enthalten. Daran erkennt dein Code den Unterschied zwischen Test und echter Lieferung.
Wenn dein Endpoint mit einem 2xx antwortet, zeigt der Dialog Testereignis
zugestellt. Klicke auf Fertig und du bist verbunden. Siehst du
stattdessen Testereignis fehlgeschlagen, wirf einen Blick in den
Abschnitt zur Fehlerbehebung und versuche es erneut. Ein
fehlgeschlagener Test macht die Verbindung nie kaputt, du kannst es also so
oft probieren, wie du willst.
Veröffentliche einen Artikel
Öffne einen deiner fertigen Artikel und veröffentliche ihn. Wähle deinen
Webhook als Ziel, und GrowWriter liefert den kompletten Artikel als
article.published-Ereignis an deinen Endpoint.
Veröffentlichst du denselben Artikel später erneut (zum Beispiel nach einer
Überarbeitung), erhält dein Endpoint stattdessen ein
article.updated-Ereignis mit derselben Artikel-id. Über diese id weiß
deine Website, dass sie den bestehenden Beitrag aktualisieren soll, statt ein
Duplikat anzulegen.
Code-Beispiele
Beide Beispiele unten erledigen die komplette Arbeit: Signatur prüfen, auf den Test-Ping antworten, schnell reagieren und den Artikel an deine eigene Speicherlogik übergeben.
Eine Regel ist wichtiger als alle anderen: Prüfe die Signatur über den rohen Anfrage-Body, exakt so, wie er angekommen ist. Wenn dein Framework das JSON zuerst parst und du es neu serialisierst, ändern sich die Bytes und die Signaturprüfung schlägt fehl.
import { createHmac, timingSafeEqual } from 'node:crypto';
// Wie viele Sekunden eine Lieferung alt sein darf, bevor wir sie ablehnen.
// Schützt davor, dass jemand eine alte abgefangene Anfrage wieder einspielt.
const MAX_AGE_SECONDS = 5 * 60;
function verifySignature(rawBody: string, signatureHeader: string, secret: string) {
// Der Header sieht so aus: t=1756718400,v1=5257a869e7...
const parts = Object.fromEntries(
signatureHeader.split(',').map((part) => part.split('='))
);
// 1. Der Zeitstempel muss frisch sein. GrowWriter signiert jeden
// Wiederholungsversuch neu, eine frische Lieferung trägt also immer
// einen frischen Zeitstempel.
const age = Math.abs(Date.now() / 1000 - Number(parts.t));
if (!(age < MAX_AGE_SECONDS)) {
return false;
}
// 2. Berechne die Signatur nach: HMAC-SHA256 über "zeitstempel.rawBody"
// mit deinem Signaturschlüssel, hex-kodiert.
const expected = createHmac('sha256', secret)
.update(`${parts.t}.${rawBody}`)
.digest('hex');
// 3. Vergleiche in konstanter Zeit, damit die Prüfung nicht messbar ist.
const provided = Buffer.from(parts.v1 ?? '', 'hex');
const wanted = Buffer.from(expected, 'hex');
return provided.length === wanted.length && timingSafeEqual(provided, wanted);
}
export async function POST(request: Request) {
// Lies den Body ZUERST als rohen Text. Verwende request.json() nicht vor
// der Prüfung: Die Signatur deckt genau diese Bytes ab.
const rawBody = await request.text();
const signature = request.headers.get('x-growwriter-signature') ?? '';
const secret = process.env.GROWWRITER_WEBHOOK_SECRET ?? '';
if (!verifySignature(rawBody, signature, secret)) {
return new Response('Invalid signature', { status: 401 });
}
const payload = JSON.parse(rawBody);
// Der Button "Testereignis senden" schickt einen Ping ohne Artikel.
// Die 200-Antwort hier lässt den Test gelingen.
if (payload.event === 'ping') {
return new Response('pong', { status: 200 });
}
// GrowWriter wiederholt fehlgeschlagene Lieferungen mit DERSELBEN
// Liefer-Id. Hast du diese Id schon verarbeitet, antworte einfach wieder OK.
const deliveryId = request.headers.get('x-growwriter-delivery');
// if (await alreadyProcessed(deliveryId)) return new Response('OK');
const { article } = payload;
// Upsert über article.id: "article.published" heißt neu,
// "article.updated" heißt, du kennst diese id bereits.
// Speichere, was du brauchst. article.content liefert denselben Artikel
// als Markdown, als HTML und als strukturierte Blöcke. Such dir eins aus.
await saveArticle({
externalId: article.id, // stabil über Neuveröffentlichungen hinweg
title: article.title,
slug: article.slug,
html: article.content.html,
metaDescription: article.metaDescription,
publishedAt: article.dates.firstPublishedAt,
updatedAt: article.dates.modifiedAt,
});
// GrowWriter wartet höchstens 10 Sekunden. Antworte schnell; erledige
// schwere Arbeit (etwa Bilder herunterladen) danach im Hintergrund.
return new Response('OK', { status: 200 });
}Was im Payload steckt
Jede Lieferung ist ein POST mit Content-Type: application/json und drei
Headern, die dich interessieren:
| Header | Was er dir sagt |
|---|---|
X-GrowWriter-Event | article.published, article.updated oder ping |
X-GrowWriter-Delivery | Eindeutige Id dieser Lieferung. Wiederholungen behalten dieselbe Id, nutze sie gegen doppelte Verarbeitung |
X-GrowWriter-Signature | t=<zeitstempel>,v1=<signatur> für die Prüfung von oben |
Hier ein gekürztes Beispiel für den Body eines echten Artikels:
{
"version": "2026-08-01",
"event": "article.published",
"deliveryId": "9d2f7c1e-6a3b-4c8d-b5e0-1f2a3b4c5d6e",
"sentAt": "2026-09-01T09:30:00.000Z",
"publishRecord": {
"firstPublishedAt": "2026-09-01T09:30:00.000Z",
"lastPublishedAt": "2026-09-01T09:30:00.000Z"
},
"article": {
"id": "cme8x2k9r0001l7042n5q8w3v",
"language": "de",
"title": "Laufschuhe finden, die wirklich passen",
"slug": "laufschuhe-finden-die-wirklich-passen",
"metaDescription": "So findest du Laufschuhe, die zu deinem Fuß und deinem Laufstil passen, mit einfachen Checks für jedes Geschäft.",
"summary": "Ein praktischer Leitfaden für die Suche nach Laufschuhen...",
"keywords": {
"primary": "laufschuhe finden",
"secondary": ["laufschuhe passform", "laufschuh ratgeber"]
},
"takeaways": ["Passform schlägt Marke, jedes Mal", "..."],
"coverImage": {
"url": "https://cdn.example.com/articleimages/.../cover.jpg",
"altText": "Läufer schnürt seine Schuhe auf einer Parkbank"
},
"images": [
{
"url": "https://cdn.example.com/articleimages/.../gait-check.jpg",
"altText": "Seitenansicht eines Läufers mitten im Schritt",
"caption": "Ein kurzer Blick auf den Laufstil sagt mehr als jedes Datenblatt.",
"sectionSlug": "pruefe-deinen-laufstil"
}
],
"content": {
"markdown": "Die Suche nach dem richtigen Paar beginnt mit...",
"html": "<p>Die Suche nach dem richtigen Paar beginnt mit...</p>",
"blocks": [{ "type": "paragraph", "children": [{ "type": "text", "text": "..." }] }]
},
"dates": {
"createdAt": "2026-08-28T14:00:00.000Z",
"firstPublishedAt": "2026-09-01T09:30:00.000Z",
"modifiedAt": "2026-09-01T09:29:45.000Z"
},
"brand": {
"name": "Stride Lab",
"websiteUrl": "https://stridelab.example.com"
}
}
}Die Felder, die die eigentliche Arbeit machen:
| Feld | So nutzt du es |
|---|---|
article.id | Dein stabiler Schlüssel. Speichere ihn und aktualisiere den bestehenden Beitrag, wenn dieselbe id wiederkommt |
article.content | Der Artikel in drei Formaten: markdown, fertiges html und strukturierte blocks für eigene Renderer. Nimm, was zu deiner Website passt |
article.title | Die Überschrift. Sie wiederholt sich nicht im Inhalt, rendere sie also selbst |
article.metaDescription | Höchstens 160 Zeichen, geschrieben für dein <meta name="description">-Tag |
article.dates | Ordne firstPublishedAt deinem Veröffentlichungsdatum zu und modifiedAt dem Änderungsdatum |
article.coverImage / article.images | Das Titelbild plus jedes Bild im Text in Reihenfolge, mit Alt-Text und Bildunterschrift, wo vorhanden |
Gut zu wissen
- Lade die Bilder herunter. Bild-URLs sind öffentlich und funktionieren
zum Lieferzeitpunkt, aber GrowWriter ist kein dauerhafter Bild-Host.
Speichere jedes Bild (Titelbild und Inline-Bilder) in deinem eigenen
Speicher, und denk daran: Dieselben URLs stehen auch in
content.htmlundcontent.markdown, schreibe sie also auch dort um. - Lieferungen wiederholen sich selbst. Ist dein Endpoint kurz nicht
erreichbar, versucht es GrowWriter bis zu 3 Mal (mit 1 Sekunde, dann 3
Sekunden Wartezeit). Jeder Versuch wird frisch signiert und behält dieselbe
X-GrowWriter-Delivery-Id. - Jedes
2xxzählt als Erfolg. Weiterleitungen nicht. Antwortet deine Website mit301oder308, schlägt die Lieferung fehl, richte den Webhook also direkt auf die endgültige URL. - Es gibt kein Lösch-Ereignis. GrowWriter legt nur an und aktualisiert. Einen Beitrag von deiner Website zu entfernen bleibt immer deine Entscheidung.
- Eine Verbindung, ein Schlüssel. Webhook trennen und neu verbinden erzeugt einen neuen Schlüssel und zieht den alten zurück. Aktualisiere dann deine Umgebungsvariable.
Fehlerbehebung
„Dein Endpoint hat nicht rechtzeitig geantwortet": GrowWriter wartet 10
Sekunden pro Versuch. Wenn dein Code vor der Antwort Bilder herunterlädt oder
andere Dienste aufruft, verschiebe diese Arbeit in einen Hintergrund-Job und
antworte mit 200, sobald der Artikel gespeichert ist.
„Dein Endpoint hat die Zustellung abgelehnt": Dein Endpoint hat mit etwas
anderem als einem 2xx geantwortet. Häufige Ursachen: Die Signaturprüfung
schlug fehl, weil der Body vor der Prüfung geparst wurde (nutze den rohen
Body), es ist der falsche Schlüssel hinterlegt (vergleiche ihn mit dem aus dem
Dialog), oder die URL leitet weiter (verwende direkt die endgültige Adresse).
„Zustellung nach mehreren Versuchen fehlgeschlagen": GrowWriter konnte
deinen Endpoint gar nicht erreichen. Prüfe, ob die URL erreichbar ist,
https:// verwendet und öffentlich zugänglich ist. Adressen in privaten
Netzen oder localhost sind aus Sicherheitsgründen gesperrt. Während der
lokalen Entwicklung kann dir ein Tunnel-Dienst eine temporäre öffentliche
HTTPS-Adresse geben.
Der Test klappt, aber echte Artikel schlagen fehl: Der Ping ist winzig; ein kompletter Artikel kann ein paar hundert Kilobyte groß sein. Prüfe, ob dein Server JSON-Bodys dieser Größe annimmt und deine Speicherlogik den vollen Payload verarbeitet.
Kommst du nicht weiter? Schreib uns aus der App heraus, wir lösen das gemeinsam.