Docs

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:

  1. Du gibst GrowWriter eine URL deiner Website, und GrowWriter gibt dir dafür einen geheimen Schlüssel.
  2. 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.
  3. Deine Website prüft die Signatur, speichert den Artikel und antwortet mit einem Erfolgsstatus (irgendein 2xx). Fertig.
Flussdiagramm in drei Schritten: Ein Klick auf Veröffentlichen in GrowWriter sendet eine signierte JSON-Anfrage, die deine Website empfängt und als veröffentlichten Beitrag speichert.
Ein Klick in GrowWriter, eine Anfrage an deine Website, ein neuer Beitrag.

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.

Die Seite Veröffentlichen in GrowWriter mit der Webhook-Karte in der Gruppe Automatisierung und ihrem Verbinden-Button.
Die Webhook-Karte findest du unter Automatisierung auf der Seite Veröffentlichen.

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:

.env
GROWWRITER_WEBHOOK_SECRET=whsec_dein_schluessel_hier

Dieser 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.

Schritt 2 des Dialogs Webhook verbinden mit dem Feld Signaturschlüssel sowie den Buttons Schlüssel kopieren und Testereignis senden.
Kopiere den Schlüssel, bevor du den Dialog schließt. Er wird nur einmal angezeigt.

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:

Payload des Testereignisses
{
  "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.

app/webhooks/growwriter/route.ts
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:

HeaderWas er dir sagt
X-GrowWriter-Eventarticle.published, article.updated oder ping
X-GrowWriter-DeliveryEindeutige Id dieser Lieferung. Wiederholungen behalten dieselbe Id, nutze sie gegen doppelte Verarbeitung
X-GrowWriter-Signaturet=<zeitstempel>,v1=<signatur> für die Prüfung von oben

Hier ein gekürztes Beispiel für den Body eines echten Artikels:

Payload von article.published (gekürzt)
{
  "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:

FeldSo nutzt du es
article.idDein stabiler Schlüssel. Speichere ihn und aktualisiere den bestehenden Beitrag, wenn dieselbe id wiederkommt
article.contentDer Artikel in drei Formaten: markdown, fertiges html und strukturierte blocks für eigene Renderer. Nimm, was zu deiner Website passt
article.titleDie Überschrift. Sie wiederholt sich nicht im Inhalt, rendere sie also selbst
article.metaDescriptionHöchstens 160 Zeichen, geschrieben für dein <meta name="description">-Tag
article.datesOrdne firstPublishedAt deinem Veröffentlichungsdatum zu und modifiedAt dem Änderungsdatum
article.coverImage / article.imagesDas 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.html und content.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 2xx zählt als Erfolg. Weiterleitungen nicht. Antwortet deine Website mit 301 oder 308, 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.

Auf dieser Seite