Webhooki

Automatycznie wysyłaj wyniki pod własny adres HTTP.

Skonfigurujesz webhook na kanale Adres HTTP, przetestujesz go bez ryzyka i przygotujesz połączony system na ponowne wysłanie tego samego wyniku.

Co robi webhook

Webhook wysyła wynik z SemanticHuba pod wskazany adres internetowy. Dzięki temu system redakcyjny, własna aplikacja albo narzędzie takie jak Zapier, Make czy n8n mogą automatycznie odebrać gotowe dane.

Do konfiguracji zwykle potrzebujesz pomocy osoby, która utrzymuje stronę lub aplikację odbierającą wyniki. Przekaż jej adres tej strony dokumentacji oraz przykładowe dane poniżej.

Włącz go bezpiecznie

  1. W kroku Akcja wybierz kafelek Adres HTTP.
  2. W polu Tryb wysyłki zostaw Testowy.
  3. Kliknij Wyślij test. Jedno przykładowe zdarzenie pójdzie tą samą drogą co prawdziwa dostawa i od razu zobaczysz odpowiedź odbiorcy. Działa też na niezapisanym adresie, więc literówkę wychwycisz przed zapisem.
  4. Przygotuj aplikację tak, aby nie tworzyła duplikatu po ponownym odebraniu tego samego wyniku.
  5. Wpisz prawdziwy Adres URL odbiorcy i opcjonalny Sekret.
  6. Przestaw Tryb wysyłki na Produkcja.
  7. Wyślij jeden zaakceptowany wynik i sprawdź go w aplikacji docelowej.
  8. Dopiero potem wybierz Wyślij od razu.

Sekret

Sekret jest opcjonalny. Po zapisie SemanticHub go nie pokazuje: widzisz tylko informację Sekret ustawiony z datą oraz przyciski Zmień i Usuń.

Przy każdej wysyłce ten sam sekret służy do dwóch rzeczy:

  • idzie w nagłówku Authorization: Bearer …;
  • jest kluczem podpisu HMAC w nagłówku X-SH-Signature.

Odbiorca sprawdza ten sposób, który obsługuje. Bez sekretu żądanie idzie bez obu nagłówków.

Ponawianie wysyłki

Jedna dostawa to najwyżej trzy próby: pierwsza, a po niepowodzeniu jeszcze dwie, po około 1 minucie i po około 5 minutach. Jedna próba może trwać najwyżej 30 sekund.

Ponowienie następuje po problemie z siecią, przekroczeniu czasu, odpowiedzi 429 lub błędzie serwera 5xx. Pozostałe błędy 4xx zwykle wymagają poprawienia adresu, danych albo uwierzytelnienia i nie są ponawiane. Po trzeciej nieudanej próbie dostawa jest oznaczana jako nieudana i możesz ponowić ją ręcznie z panelu wyniku.

Uzgadnianie możliwości odbiorcy

Odbiorca może zgłosić SemanticHubowi, co potrafi złożyć: jakie rodzaje sekcji i elementów treści renderuje. Robi to na przykład wtyczka WordPress, ale własna aplikacja też może się w ten sposób przedstawić.

  • Pierwsze zgłoszenie na celu, który nie ma jeszcze ustalonego kontraktu treści, ustawia ten kontrakt na podstawie możliwości odbiorcy. Nie musisz go konfigurować ręcznie.
  • Kolejne zgłoszenia tylko zapisują nowe możliwości. Zgłoszenie nigdy nie wstrzymuje dostawy.
  • Sekcja, której odbiorca nie renderuje sam, idzie w formie zapasowej jako zwykłe bloki treści.
  • Raz w tygodniu dostajesz e-mail, jeśli ręcznie ustawiony kontrakt rozjechał się z możliwościami odbiorcy albo wtyczka przestała się zgłaszać.

Informacje dla programisty

SemanticHub wysyła żądanie HTTP POST z danymi w formacie JSON. Każdy status 2xx potwierdza poprawne odebranie wyniku.

Nagłówki

  • Content-Type: application/json
  • Idempotency-Key: stały identyfikator danego wyniku
  • X-SH-Delivery: identyfikator serii prób; ponowienie tej samej treści niesie ten sam identyfikator
  • X-SH-Timestamp: czas wysłania w sekundach od 1970 roku
  • Authorization: Bearer … i X-SH-Signature: sha256=…, jeśli ustawisz sekret

Podpis to HMAC-SHA256(sekret, X-SH-Timestamp + "." + surowe body) zapisany szesnastkowo. Licz go z bajtów żądania, zanim zamienisz je na obiekt JSON.

Prosty przykład odbioru

import express from 'express'

const app = express()
app.use(express.json())

app.post('/semantic-hub', async (req, res) => {
  const deliveryId = req.header('X-SH-Delivery')

  // Zapisz identyfikator razem z danymi. Jeśli identyfikator już istnieje,
  // nie twórz drugiej publikacji.
  await enqueueOnce(deliveryId, req.body)

  res.status(202).end()
})

Najważniejsze przesyłane pola

Każdy odbiorca dostaje ten sam kształt danych, payload_version: 5. Nie wybierasz wersji i nie ustawiasz własnego szablonu JSON.

{
  "payload_version": 5,
  "mode": "article",
  "event": "workflow.action",
  "result_id": "id-wyniku",
  "revision": 1,
  "workflow_execution": "id-wyniku",
  "workflow_name": "Nazwa celu",
  "title": "Tytuł wyniku",
  "lead": "Zajawka do zapowiedzi i opisu meta.",
  "executed_at": "2026-09-04T10:00:00+00:00",
  "published_at": "2026-09-04T10:00:00+00:00",
  "source_lang": "pl",
  "articles_count": 3,
  "articles": [],
  "clusters": [],
  "llm_response": "## Nagłówek\n\nTreść w markdownie…",
  "locales": {
    "pl": {
      "title": "Tytuł wyniku",
      "slug": "tytul-wyniku",
      "lead": "Zajawka…",
      "body": "## Nagłówek\n\nTreść w markdownie…",
      "seo_description": "Zajawka…",
      "body_html": "<h2>Nagłówek</h2><p>Treść w markdownie…</p>"
    }
  },
  "steps": {
    "Redaktor": { "text": "…", "model": "model-id", "tokens": 900, "cost": 0.0004 }
  },
  "ai_model": "model-id",
  "tokens_used": 1234,
  "cost": 0.01
}
  • mode: article, gdy wynik ma wygenerowaną treść, albo cluster, gdy wysyłamy sam klaster wraz z artykułami źródłowymi. W trybie klastrowym llm_response jest pusty (null), nie ma locales, a steps to pusty obiekt.
  • result_id i revision: result_id nie zmienia się między kolejnymi wersjami tego samego wyniku, a revision rośnie przy każdej zmianie treści. Po tej parze odbiorca rozpoznaje, który wpis zaktualizować.
  • executed_at i published_at: czas powstania wyniku. Ponowienie wysyła te same wartości.
  • llm_response: treść w formacie markdown (nagłówki ##, tabele GFM, cytaty). Kanoniczna struktura podróżuje w sections.
  • locales: gotowa wersja wyniku w języku źródłowym, z adresem slug i treścią już zamienioną na HTML. Drugi język pojawia się tylko wtedy, gdy włączysz Wersja w drugim języku.
  • steps: wyjście każdego kroku workflow pod jego nazwą. Odbiorca bierze stąd to, czego potrzebuje, na przykład sam brief albo wersję przed redakcją.
  • title i lead: zawsze obecne, choć mogą być puste (null). lead to zajawka z edytora. Wpisz ją w zapowiedź lub opis meta zamiast wycinać pierwszy akapit treści. Opis w clusters[0].description dotyczy klastra i nie jest tym samym tekstem.

Część pól pojawia się tylko wtedy, gdy jest co wysłać. Brak klucza nie jest błędem i nie znaczy „wartość pusta”:

{
  "sections": [{ "key": "body", "blocks": [] }],
  "image": { "url": "https://…", "attribution": "…" },
  "tags": ["słowo kluczowe"],
  "publish_mode": "draft",
  "fields": { "category": "zdrowie" }
}
  • sections: pomijane, gdy wynik nie ma sekcji (nie wysyłamy "sections": []);
  • image: tylko gdy ktoś faktycznie wybrał obrazek wyróżniający; klucz niesie też atrybucję;
  • tags: tylko gdy klaster ma słowa kluczowe;
  • publish_mode: tylko gdy cel ustawił politykę publikacji (moderation, draft lub publish). To prośba, nie rozkaz: odbiorca i tak sprawdza własne uprawnienia.
  • fields: tylko gdy odbiorca zgłosił pola, których wymaga (opis w sekcji Pola odbiorcy).

Aplikacja odbierająca dane powinna pomijać pola, których nie zna, zamiast odrzucać całe żądanie.

Kolejna wersja wyniku

Gdy do opublikowanego już klastra dojdą nowe artykuły albo gdy zmienisz treść wysłanego wyniku i wyślesz go ponownie, kolejna wersja idzie jako event: "cluster.updated" z wyższym revision. Taka dostawa dokłada dwa klucze, których nie ma w pierwszej wysyłce:

{
  "event": "cluster.updated",
  "revision": 2,
  "parent_execution": "id-poprzedniego-wyniku",
  "new_articles": ["id-artykulu"]
}

Po zmianie samej treści wyniku, który nie ma poprzedniej wersji, parent_execution jest null, a new_articles puste. Kolejna wersja klastra z nowymi artykułami niesie parent_execution. Jeśli Twój system zna już wyższą wersję, odpowiedz 409 z polem revision w body. Wyślemy wtedy wynik ponownie z kolejnym numerem.

Pola odbiorcy

Twój system może wymagać przy każdym wpisie wartości, których treść wyniku nie niesie, na przykład kategorii albo działu. Zgłasza je w tym samym meldunku możliwości co bloki i sekcje: PUT /api/goals/{id}/target-manifest z tokenem API. Poniżej fragment takiego meldunku:

{
  "source": "receiver",
  "fields": [
    {
      "key": "category",
      "label": "Kategoria",
      "type": "enum",
      "required": true,
      "multiple": false,
      "options": [{ "value": "zdrowie", "label": "Zdrowie" }]
    }
  ]
}
  • source musi mieć wartość "receiver". Meldunek bez niej traktujemy jak meldunek wtyczki WordPress, a jego pola nie obowiązują celu z kanałem Adres HTTP.
  • key to małe litery, cyfry i _, najwyżej 40 znaków. type to enum, text albo bool. Pole enum musi mieć listę options, a multiple jest dozwolone tylko dla enum.
  • Wartości ustawiasz w SemanticHubie: domyślne w kroku Akcja, w sekcji Pola odbiorcy, a dla pojedynczego wyniku w zakładce Dostawa. Wartość spoza zgłoszonej listy zostanie odrzucona przy zapisie.
  • Payload niesie klucz fields ze słownikiem klucz: wartość. Bez zgłoszonych pól tego klucza nie ma.
  • Wynik bez wartości pola wymaganego nie zostanie wysłany. Wraca w SemanticHubie do statusu Do akceptacji i czeka na uzupełnienie.

Inny kształt danych

Kształtu JSON nie zmienisz po stronie SemanticHuba. Jeśli Twój system oczekuje innych pól, wklej jako adres URL adres z Zapiera, Make albo n8n i tam przepisz dane na potrzebny kształt.

Wymagania dotyczące adresu

Adres webhooka musi być dostępny z publicznego internetu. Ze względów bezpieczeństwa SemanticHub blokuje adresy prowadzące do urządzenia lokalnego, sieci prywatnej i wewnętrznych usług chmurowych.

Wszystkie strony