Podłączenie własnego endpointu (REST / webhook) | justrank.it
Poradnik połączenia

Podłącz własny endpoint

Specyfikacja integracji dla osoby, która utrzymuje stronę: jedno żądanie POST, podpisany JSON i dokładnie to, czego oczekujemy w odpowiedzi.

To połączenie jest inne niż pozostałe. Przy WordPressie czy Joomli to my uczymy się cudzego API. Tutaj API jesteś Ty: podajesz nam adres, a my dostarczamy pod niego każdy artykuł jako jedno podpisane żądanie HTTP. Odbiornikiem może być kontroler w Twojej aplikacji, funkcja bezserwerowa, scenariusz w Make albo webhook w n8n.

Ta strona nie jest instrukcją klikania, tylko specyfikacją. Poniżej znajdziesz dokładny kształt żądania, każde pole treści z typem, schemat podpisu na tyle dokładny, żeby dało się go zweryfikować, oraz to, co robimy z każdą możliwą odpowiedzią. Jeśli utrzymujesz stronę klienta, to wszystko, czego potrzebujesz.

Nazwy pól i kluczy JSON podajemy po angielsku, bo tak wyglądają na łączu i tak trzeba je wpisać w kodzie. Tłumaczenie ich w tekście wysłałoby Cię na poszukiwanie klucza, którego w treści żądania nie ma.

Czym jest to połączenie

Jedno żądanie na artykuł. Nic nie utrzymuje otwartego połączenia. Kiedy artykuł jest gotowy, wysyłamy go pod Twój adres i czekamy na odpowiedź. Jedyny ruch w drugą stronę jest opcjonalny: jeśli podasz nam adres listy artykułów (sekcja 7), pobieramy z niego spis Twoich wpisów przy zapisie połączenia i potem raz dziennie. Bez tego adresu nie odpytujemy Twojej strony wcale.

Kiedy warto sięgnąć po to połączenie

  • Twoja strona to własna aplikacja, a Ty wolisz przyjąć artykuł u siebie i sam zdecydować, jak go zapisać i wyrenderować.
  • Twoja platforma nie ma żadnego użytecznego API bloga, więc nie ma czego podłączyć w normalny sposób.
  • Wystarczy Ci mała funkcja pośrednicząca, która przyjmie artykuł i przełoży go na cokolwiek, czego naprawdę używasz.
  • Pracujesz w narzędziu bez kodu, które potrafi przyjąć webhook, i chcesz poprowadzić artykuł dalej własnym przepływem.

Dwie rzeczy, których to połączenie nie robi i nie będzie robić w tej wersji. Nie czyta katalogu produktów, bo za webhookiem nie ma katalogu do przeczytania. Nie wysyła też szkiców: szkic to obietnica, że nikt nie zobaczy tekstu przed akceptacją, a takiej obietnicy nie możemy złożyć za cudzy serwer, więc pole statusu zawsze mówi publish.

Twój odbiornik jest jedynym miejscem, w którym artykuł ląduje. Nie mamy do Twojej strony żadnego innego dostępu, nie logujemy się nigdzie i nie mamy Twoich haseł. Cały kontakt to opisane niżej żądania: dostawa artykułu i, jeśli go włączysz, odczyt listy Twoich wpisów.

1. Adres, który nam podajesz

Podajesz nam dwie wartości obowiązkowe i jedną opcjonalną. Wszystkie trzymamy zaszyfrowane, a sekret nigdy nie pojawia się w treści żądania, w nagłówku ani w logu.

Trzy wartości, z czego jedna opcjonalna

  • Adres endpointu. Pełny adres, pod który mamy wysyłać. Musi zaczynać się od https://. Wysyłamy dokładnie pod ten adres, znak w znak: nie doklejamy do niego żadnej ścieżki ani parametru.
  • Sekret do podpisu. Wspólny sekret, osobny dla tego połączenia. Wygeneruj co najmniej 32 losowe bajty. Nigdy go nie wysyłamy, używamy go wyłącznie do policzenia podpisu, a Ty tym samym sekretem podpis sprawdzasz.
  • Adres listy artykułów (opcjonalnie). Drugi adres https://, pod którym oddajesz spis wpisów już opublikowanych na Twojej stronie. Podlega tym samym regułom co adres endpointu, musi leżeć na tym samym hoście co endpoint albo w domenie Twojej strony (także w jej subdomenie) i jest podpisywany tym samym sekretem. Możesz zostawić to pole puste; co wtedy tracisz, opisuje sekcja 7.

Obie wartości wpisujesz sam, w naszym panelu, w formularzu połączenia dla własnego endpointu: adres w jednym polu, sekret w drugim, a opcjonalny adres listy artykułów w polu Adres listy artykułów (opcjonalnie). To ten sam formularz w obu miejscach, w których go spotkasz: we wdrożeniu, w kroku Publikacja, pod kafelkiem Custom endpoint (REST / webhook), oraz później w Witrynach, gdzie otwierasz witrynę i wybierasz Podłącz: Publikowanie.

Naciśnięcie przycisku Sprawdź i połącz wysyła podpisane żądanie connection.test opisane w sekcji 3, a połączenie zapisuje się dopiero wtedy, gdy Twój endpoint odpowie na nie kodem 2xx i wymieni w polu supports obsługiwane zdarzenia, w tym co najmniej article.publish i article.update (sekcja 3). Jeśli podałeś adres listy artykułów, w tym samym momencie odpytujemy także jego, a połączenie zapisuje się tylko wtedy, gdy odpowie kodem 200 i poprawnym kształtem z sekcji 7. W panelu nie zapisujemy niczego: wszystkie wartości trafiają prosto do naszego silnika, który trzyma je zaszyfrowane. Jeśli sekret kiedykolwiek wycieknie, zmień go u siebie i w formularzu, a stary przestanie działać przy najbliższej dostawie.

Kiedy połączenie już stoi, na stronie witryny pojawia się przycisk Wyślij artykuł testowy. Jeśli Twój odbiornik zadeklarował article.delete, to jest prawdziwa dostawa, a nie ping: odbiornik dostaje pełną kopertę article.publish z krótkim, wyraźnie oznaczonym artykułem, więc w kilka minut widzisz u siebie, że cała droga działa, a kilka minut później dostaje article.delete dla tego samego wpisu, więc test nie zostaje na Twojej stronie. Jeśli odbiornik nie zadeklarował usuwania, przycisk tylko ponawia podpisany connection.test i niczego nie publikuje, bo artykułu, którego nie umiemy zdjąć, nie wolno nam zostawić na Twojej stronie. Jeśli utkniesz, napisz na hello@justrank.it.

Co ma być widoczne: Ekran naszego panelu z formularzem połączenia dla własnego endpointu, z widocznym polem adresu i polem sekretu, oraz przyciskiem testu połączenia.

Co sprawdzamy w adresie przed każdym żądaniem

Adres jest sprawdzany na nowo przed każdą dostawą, łącznie z testowym pingiem. Adres, który nie przejdzie któregokolwiek z tych warunków, zostaje odrzucony, zanim otworzymy jakiekolwiek połączenie, i taka odmowa nigdy nie jest ponawiana, bo ponowienie niczego by nie naprawiło.

  1. Schemat musi być dokładnie https. Nie http, nie ftp, nie file. Po zwykłym HTTP podpisany artykuł szedłby otwartym tekstem, więc nie robimy tego nawet na prośbę.
  2. W adresie nie może być nazwy użytkownika ani hasła, czyli formy https://user:pass@host/.... Uwierzytelnieniem jest podpis, nie adres.
  3. Nazwa hosta musi się rozwiązywać. Pobieramy wszystkie rekordy A i AAAA, a adres podany wprost liczbowo sprawdzamy tak samo.
  4. Każdy adres, na który nazwa się rozwiązuje, musi być publiczny. Sprawdzamy rozwiązane adresy, a nie tekst URL, więc wycelowanie publicznej domeny w adres wewnętrzny nic nie daje. Jeśli nazwa zwraca kilka adresów i choć jeden z nich jest prywatny, odrzucamy cały cel.

Odrzucane zakresy adresów

Pełne listy, tak jak stoją w kodzie. Adresy IPv4 zapisane w formie IPv6, na przykład ::ffff:127.0.0.1, są rozpakowywane i sprawdzane jako IPv4.

IPv4 IPv6
0.0.0.0/8 ::/128
10.0.0.0/8 ::1/128
100.64.0.0/10 fc00::/7
127.0.0.0/8 fe80::/10
169.254.0.0/16 ff00::/8
172.16.0.0/12 2001:db8::/32
192.0.0.0/24 64:ff9b::/96
192.0.2.0/24
192.168.0.0/16
198.18.0.0/15
198.51.100.0/24
203.0.113.0/24
224.0.0.0/4
240.0.0.0/4

Powód jest prosty i nie jest teoretyczny. Adres endpointu wpisuje człowiek, a my strzelamy pod niego uwierzytelnionym żądaniem z wnętrza naszej sieci. Bez tej kontroli wystarczyłoby wskazać adres metadanych chmury albo lokalną bazę, żeby zrobić z silnika narzędzie do zaglądania nam do środka.

Odrzucony adres jest zawsze błędem ostatecznym, nigdy powtarzalnym. To pomyłka w konfiguracji, a kolejka dobijająca się do adresu wewnętrznego jest dokładnie tym, czego ta kontrola ma nie dopuścić.

2. Żądanie, które wysyłamy

Zawsze jedno żądanie POST pod adres, który zapisałeś, z treścią w JSON i czterema nagłówkami podpisu. Pod ten adres nigdy nie używamy innej metody, nie wysyłamy formularza ani danych wieloczęściowych i nigdy nie robimy więcej niż jedno żądanie na dostawę. Jedyne żądanie GET w tym połączeniu idzie pod osobny, opcjonalny adres listy artykułów i opisuje je sekcja 7.

Kształt żądania

POST /articles HTTP/1.1
Host:                hooks.example.com
Content-Type:        application/json
Accept:              application/json
X-Signature-Version: v1
X-Timestamp:         1755859200
X-Nonce:             9f1c3b7e2a4d5061c8f30b9e7a1d2c4b
X-Signature:         2b7d4f9a1c60e8352da4bf07c19e6d8b5a03f2ce74198bd6e0a5c3f28b7d61a4

Nagłówki

Nagłówek Znaczenie
Content-Type: application/json Treść jest zawsze pojedynczym obiektem JSON zakodowanym w UTF-8. Nie uciekamy znaków spoza ASCII i nie uciekamy ukośników, więc polskie znaki i adresy zobaczysz w treści dosłownie.
Accept: application/json Prosimy o odpowiedź w JSON. Nie jest to warunek powodzenia: odpowiedź w innym formacie przyjmiemy, tylko nie znajdziemy w niej identyfikatora. Nie przyjmiemy tylko strony HTML, bo taka odpowiedź oznacza, że pod adresem nie ma odbiornika.
X-Signature-Version Wersja schematu podpisu. Dzisiaj zawsze v1. Inny schemat dostanie inną wartość, więc odrzucaj to, czego nie znasz.
X-Timestamp Czas uniksowy w sekundach z chwili, w której policzyliśmy podpis.
X-Nonce Trzydzieści dwa znaki szesnastkowe, losowane osobno dla każdego żądania. Nigdy nie powtarzamy tej wartości.
X-Signature HMAC-SHA256 zapisany jako 64 małe znaki szesnastkowe. Sposób liczenia opisuje sekcja o podpisie.

Na odpowiedź czekamy 15 sekund. Brak odpowiedzi w tym czasie traktujemy jak awarię przejściową i ponawiamy. Jeśli Twój odbiornik ma coś wolnego do zrobienia, przyjmij artykuł, odpowiedz od razu i zrób resztę w kolejce.

Podaj adres docelowy, a nie przekierowanie. Podpisujemy ścieżkę tego adresu, który zapisałeś, więc odbiornik stojący pod innym adresem po przekierowaniu policzy podpis z innej ścieżki i go nie potwierdzi.

Poza adresem, sekretem i opcjonalnym adresem listy artykułów to połączenie nie ma żadnych ustawień. Limit czasu, liczba prób i odstępy między nimi są stałe po naszej stronie i nie da się ich zmienić dla pojedynczej strony.

3. Treść żądania

Treść żądania to jeden obiekt JSON. Te nazwy kluczy są kontraktem publicznym, a nie szczegółem naszej implementacji. W wersji 1 klucze mogą przybywać, ale żaden nie zostanie przemianowany ani usunięty; gdyby kiedyś trzeba było to złamać, zmieni się envelope_version. Napisz odbiornik tak, żeby nieznane klucze po prostu ignorował.

Przykładowe żądanie publikacji

{
  "envelope_version": "1",
  "event": "article.publish",
  "id": "0f6f9c22-6f1f-4e2a-9c33-0b1e6d8a9f4e",
  "occurred_at": "2026-08-22T09:15:04+00:00",
  "site": {
    "id": "412",
    "domain": "sklep.example"
  },
  "remote_id": null,
  "article": {
    "id": "918273",
    "title": "Jak wybrać dobry ekspres do kawy",
    "slug": "jak-wybrac-dobry-ekspres-do-kawy",
    "html": "<h2>Na co patrzeć</h2><p>Ciśnienie ...</p>",
    "excerpt": "Krótkie streszczenie, nadaje się na opis meta.",
    "featured_image_url": "https://images.example/cover.jpg",
    "categories": ["porady"],
    "tags": ["ekspres", "kawa", "sprzęt"],
    "canonical_url": "https://sklep.example/jak-wybrac-dobry-ekspres-do-kawy/",
    "published_at": "2026-08-22T09:15:04+00:00",
    "status": "publish"
  }
}

Pola koperty

Pole Typ Uwagi
envelope_version tekst Dzisiaj zawsze "1".
event tekst Jedno z czterech zdarzeń opisanych niżej.
id tekst, UUID Klucz idempotencji tej konkretnej dostawy. Ponowiona próba powtarza tę samą wartość, więc odbiornik, który ją zapamięta, rozpozna duplikat i może go pominąć.
occurred_at tekst, ISO 8601 Moment, w którym zbudowaliśmy wiadomość.
site.id tekst Nasz identyfikator Twojej strony. Stały.
site.domain tekst Sama nazwa hosta, bez schematu, bez ukośnika na końcu i bez www., zapisana małymi literami.
remote_id tekst albo null Przy publikacji zawsze null. Przy korekcie i przy usuwaniu jest to identyfikator, który wcześniej od Ciebie dostaliśmy.
article obiekt albo null Przy teście połączenia i przy usuwaniu null, w pozostałych przypadkach obiekt opisany w następnej tabeli.
target obiekt, tylko przy usuwaniu Przychodzi tylko z article.delete: id to nasz identyfikator artykułu (ta sama wartość co article.id przy publikacji), a slug to jego człon adresu. Przyda się, jeśli nie zapisałeś remote_id.

Pola artykułu

Pole Typ Uwagi
article.id tekst Nasz identyfikator artykułu, stały przez całe jego życie. To jest ta jedna wartość, którą warto zapisać przy swoim wpisie.
article.title tekst Zwykły tekst, bez znaczników.
article.slug tekst Człon adresu, małe litery i myślniki. Nie gwarantujemy, że jest niepowtarzalny po Twojej stronie, więc jeśli tego potrzebujesz, rozstrzygnij konflikt sam.
article.html tekst Treść artykułu jako HTML. Nagłówki są na poziomie h2 i h3, a tytuł nie jest w środku powtórzony. Przefiltruj to przed wyrenderowaniem, tak jak każdy HTML z zewnątrz.
article.excerpt tekst Jedno albo dwa zdania zwykłego tekstu. Nadaje się na opis meta.
article.featured_image_url tekst albo null Pełny adres https do obrazka wiodącego, albo null, jeśli artykuł go nie ma.
article.categories lista tekstów Nazwy kategorii, nie identyfikatory. Zwykle jedna pozycja, czasem lista pusta.
article.tags lista tekstów Słowa kluczowe, bez powtórzeń, po obcięciu białych znaków. Może być pusta.
article.canonical_url tekst albo null Adres, pod którym artykuł ma mieszkać w Twojej domenie. Użyj go jako rel="canonical", jeśli renderujesz wpis także gdzie indziej.
article.published_at tekst, ISO 8601 Zamierzony moment publikacji.
article.status tekst Zawsze "publish". To połączenie nigdy nie wysyła szkicu, bo nie umiemy sprawdzić, czy Twój odbiornik faktycznie by go ukrył.

Cztery zdarzenia

Wartość pola event mówi, co masz zrobić. To jedyne cztery wartości, jakie wysyłamy.

Zdarzenie Pole article Pole remote_id Co masz zrobić
article.publish obiekt null Utwórz wpis.
article.update obiekt Twój identyfikator Zaktualizuj wpis o tym identyfikatorze. Jeśli już go nie ma, utwórz go na nowo.
connection.test null null Nie twórz niczego. Sprawdź podpis i odpowiedz kodem 2xx z listą obsługiwanych zdarzeń w polu supports.
article.delete null Twój identyfikator Usuń wpis o tym identyfikatorze i odpowiedz kodem 2xx z {"deleted": true}. Jeśli już go nie ma, odpowiedz 404 albo 410.

Zdarzenie connection.test wysyła przycisk Sprawdź i połącz w naszym panelu. Odbiornik, który utworzy z niego wpis, będzie produkował pusty artykuł za każdym razem, gdy ktoś sprawdzi swoje ustawienia. To najczęstszy błąd przy pisaniu odbiornika, więc obsłuż to zdarzenie zanim zajmiesz się resztą.

Co wysyła przycisk Sprawdź i połącz

{
  "envelope_version": "1",
  "event": "connection.test",
  "id": "3c8a1f04-9d2b-4c17-8f5a-71e0c6b4a29d",
  "occurred_at": "2026-08-22T09:14:58+00:00",
  "site": {
    "id": "412",
    "domain": "sklep.example"
  },
  "remote_id": null,
  "article": null
}

To całość: te same cztery nagłówki, podpisane tym samym sekretem po tym samym ciągu kanonicznym, oraz treść, w której article i remote_id są null. Nie niesie artykułu, więc rozpoznasz je po samym polu event, zanim sięgniesz po cokolwiek innego z treści. Odpowiedź opisuje punkt niżej.

Co odpowiadasz na connection.test

HTTP/1.1 200 OK
Content-Type: application/json

{
  "ok": true,
  "supports": ["article.publish", "article.update", "article.delete"]
}

Kod 2xx i obiekt JSON, w którym pole supports wymienia zdarzenia, które Twój odbiornik naprawdę obsługuje. article.publish i article.update są obowiązkowe: bez nich nie przyjmiemy połączenia, bo odbiornik, który umie tylko tworzyć, na korektę odpowiedziałby drugim wpisem, czyli duplikatem na Twojej stronie. article.delete jest opcjonalne: z nim przycisk Wyślij artykuł testowy publikuje prawdziwy artykuł testowy i sam go usuwa, bez niego wysyła tylko ten ping. Odpowiedź bez listy supports, z pustą listą albo niebędąca JSON-em znaczy dla nas „niczego nie deklaruję” i połączenie nie przechodzi testu. Nieznane nazwy pomijamy. Czytamy tę listę przy każdym teście połączenia.

Jak wygląda usuwanie

{
  "envelope_version": "1",
  "event": "article.delete",
  "id": "7b2e4d10-3c5a-4f8e-a1d2-5e9f0c3b6a71",
  "occurred_at": "2026-08-22T09:21:40+00:00",
  "site": {
    "id": "412",
    "domain": "sklep.example"
  },
  "remote_id": "5531",
  "article": null,
  "target": {
    "id": "918274",
    "slug": "artykul-testowy-justrank-it"
  }
}

Na razie article.delete wysyłamy tylko w jednej sytuacji: po artykule testowym z przycisku Wyślij artykuł testowy, kilka minut po publikacji, żeby test nie został na Twojej stronie. Dostaje je wyłącznie odbiornik, który zadeklarował article.delete w odpowiedzi na connection.test. Znajdź wpis po remote_id, czyli identyfikatorze, który nam oddałeś przy tworzeniu, a jeśli go nie trzymasz, po target.id. Usuń go i odpowiedz kodem 2xx z treścią JSON {"deleted": true}. Jeśli wpisu już nie ma, odpowiedz 404 albo 410, a uznamy sprawę za załatwioną. Każde inne 2xx, także puste, czytamy jako „nie usunięto”: wtedy sprawą zajmuje się nasz zespół, a Ty nie dostajesz żadnego zadania. Ponowiona dostawa ma to samo id, więc druga próba powinna skończyć się 404 albo tym samym {"deleted": true}, a nie błędem.

4. Jak sprawdzić podpis

Podpisujemy metodę, ścieżkę i skrót treści, a nie sam czas. Dzięki temu przechwycone żądanie nie da się odtworzyć z podmienionym artykułem. Podpis, którego nie da się sprawdzić, jest podpisem, który się ignoruje, więc poniżej jest wszystko, czego potrzebujesz, żeby policzyć go u siebie.

Ciąg kanoniczny

Sześć linii połączonych pojedynczym znakiem nowej linii. Bez znaku nowej linii na końcu.

v1
1755859200
9f1c3b7e2a4d5061c8f30b9e7a1d2c4b
POST
/articles
c1ba0d4b0e8f3a72d95e64b810cf27a3e0d5946bf1c83ea27d604b9f5138ace6

Skąd bierze się każda linia

  • Linia pierwsza to dosłowna wartość nagłówka X-Signature-Version, dzisiaj zawsze v1.
  • Linia druga to dosłowna wartość nagłówka X-Timestamp.
  • Linia trzecia to dosłowna wartość nagłówka X-Nonce.
  • Linia czwarta to metoda wielkimi literami: POST przy każdej dostawie artykułu i GET przy odczycie listy artykułów z sekcji 7.
  • Linia piąta to sama ścieżka Twojego adresu: bez schematu, bez hosta i bez zapytania, z ukośnikiem na początku. Dla https://hooks.przyklad.pl/artykuly jest to /artykuly. Jeśli adres nie ma ścieżki w ogóle, jest to /.
  • Zapytanie po znaku zapytania nie wchodzi do podpisu. Jeśli w adresie masz parametry, wyślemy je normalnie, ale nie są objęte podpisem, więc nie opieraj na nich niczego, co ma znaczenie dla bezpieczeństwa.
  • Linia szósta to SHA-256 surowych bajtów treści, zapisany szesnastkowo małymi literami. Nie parsuj JSON i nie koduj go ponownie przed policzeniem skrótu: kolejność kluczy i sposób uciekania znaków by się zmieniły i skrót przestałby się zgadzać.

Wzór

canonical = "v1" + "\n"
          + X-Timestamp + "\n"
          + X-Nonce + "\n"
          + "POST" + "\n"          // "GET" for the article list, section 7
          + path + "\n"
          + sha256_hex(raw_body)

X-Signature = hmac_sha256_hex(secret, canonical)

Co Twój odbiornik musi sprawdzić

Pięć kontroli. Cztery pierwsze są tanie, piąta jest tą, którą najczęściej robi się źle.

  1. Wszystkie cztery nagłówki podpisu są obecne. Brak któregokolwiek to odmowa.
  2. X-Signature-Version ma wartość v1. Nieznaną wersję odrzucaj, zamiast zgadywać.
  3. Różnica między X-Timestamp a Twoim czasem jest nie większa niż 300 sekund w którąkolwiek stronę.
  4. X-Nonce nie był jeszcze widziany. Trzymaj zużyte wartości przez 600 sekund; powtórzenie w tym oknie to próba odtworzenia żądania.
  5. Policzony podpis porównaj z otrzymanym w stałym czasie, funkcją typu hash_equals, crypto.timingSafeEqual albo hmac.compare_digest. Nigdy zwykłym porównaniem.

Kiedy którakolwiek kontrola nie przejdzie, odpowiedz kodem 401 i nie tłumacz, która to była. My pokażemy wtedy klientowi zdanie o niezgodnym sekrecie i nie ponowimy dostawy.

Weryfikacja w PHP

$raw = file_get_contents('php://input');

$canonical = implode("\n", [
    $request->header('X-Signature-Version'),
    $request->header('X-Timestamp'),
    $request->header('X-Nonce'),
    'POST',
    '/articles',
    hash('sha256', $raw),
]);

$expected = hash_hmac('sha256', $canonical, $secret);

abort_unless(
    hash_equals($expected, (string) $request->header('X-Signature')),
    401
);

Weryfikacja w Node

const crypto = require('crypto');

const canonical = [
  req.get('X-Signature-Version'),
  req.get('X-Timestamp'),
  req.get('X-Nonce'),
  'POST',
  '/articles',
  crypto.createHash('sha256').update(req.rawBody).digest('hex'),
].join('\n');

const expected = crypto.createHmac('sha256', secret).update(canonical).digest('hex');
const given = Buffer.from(req.get('X-Signature') || '', 'hex');

if (given.length !== 32 ||
    !crypto.timingSafeEqual(Buffer.from(expected, 'hex'), given)) {
  return res.status(401).end();
}

Zmienna z treścią musi zawierać surowe, nieprzetworzone bajty. W Express uzyskasz je przez express.json({ verify: (req, res, buf) => { req.rawBody = buf } }).

Weryfikacja w Pythonie

import hashlib
import hmac

canonical = "\n".join([
    headers["X-Signature-Version"],
    headers["X-Timestamp"],
    headers["X-Nonce"],
    "POST",
    "/articles",
    hashlib.sha256(raw_body).hexdigest(),
])

expected = hmac.new(secret.encode(), canonical.encode(), hashlib.sha256).hexdigest()

if not hmac.compare_digest(expected, headers.get("X-Signature", "")):
    return 401

Make, Zapier i n8n

Te narzędzia niechętnie oddają surową treść żądania, a ich własne kroki liczące HMAC są niewygodne. Masz dwie sensowne drogi.

  • Lepsza. Postaw przed scenariuszem małą funkcję, choćby na Cloudflare Workers albo jako funkcję bezserwerową, która sprawdzi podpis i dopiero potem przekaże dalej już zaufany JSON.
  • Do przyjęcia. Wskaż połączenie na adres webhooka na tyle długi i losowy, żeby nie dało się go zgadnąć, i potraktuj go jako jedyne zabezpieczenie. Tracisz wtedy ochronę przed odtworzeniem żądania i pewność, że treść nie została podmieniona. Powiedz sobie to na głos, zanim tak zrobisz.

5. Odpowiedź, której oczekujemy

Odpowiedź czytamy w dwóch krokach: najpierw rozstrzygamy, czy dostawa się udała, potem szukamy w niej identyfikatora wpisu.

Za powodzenie uznajemy każdy kod z zakresu 2xx i nic poza tym. Pusta odpowiedź z kodem 200, krótki tekst w rodzaju OK i dowolny JSON są pełnym sukcesem. Jedyny wyjątek to strona HTML: jeśli odpowiedź ma nagłówek Content-Type: text/html albo jej treść zaczyna się od <!doctype html lub <html, pod adresem jest zwykła strona (najczęściej strona główna, która na każde żądanie POST odpowiada kodem 200), a nie odbiornik. Test połączenia wtedy nie przechodzi, dostawa kończy się błędem bez ponawiania, a połączenie oznaczamy jako zerwane i gotowe artykuły czekają, aż test znów przejdzie. Przy connection.test sam kod 2xx nie wystarcza: odpowiedź musi też wymienić obsługiwane zdarzenia w polu supports (sekcja 3).

Jak oddać nam identyfikator wpisu

Przy publikacji odpowiedz obiektem JSON z własnym identyfikatorem utworzonego wpisu. To ten identyfikator wróci do Ciebie w polu remote_id, kiedy artykuł będzie trzeba poprawić.

HTTP/1.1 200 OK
Content-Type: application/json

{
  "id": "8821",
  "url": "https://sklep.example/jak-wybrac-dobry-ekspres-do-kawy/"
}

Szukamy po kolei kluczy id, remote_id, post_id, data.id i data.remote_id, i bierzemy pierwszą wartość, która jest niepustym tekstem albo liczbą całkowitą.

Jeśli nie znajdziemy żadnego z nich, zapamiętujemy jako identyfikator zdalny nasze własne article.id, czyli tę samą wartość, którą już dostałeś w treści żądania. Jest to celowe, żeby goły kod 200 z narzędzia bez kodu też działał.

Ma to jedną konsekwencję i warto ją zrozumieć od razu. Jeśli utworzysz wpis pod własnym identyfikatorem i nam go nie oddasz, korekty przyjdą opisane naszym identyfikatorem artykułu, a nie Twoim. Albo więc oddawaj swój identyfikator przy każdej publikacji, albo zapisuj u siebie nasz.

Adres wpisu: pole url

Obok identyfikatora oddaj w odpowiedzi klucz url z adresem wpisu, który właśnie utworzyłeś albo poprawiłeś. To pole ma dziś duże znaczenie: zapamiętujemy ten adres i to pod niego linkujemy z kolejnych artykułów (linkowanie wewnętrzne między wpisami), a także używamy go jako adresu kanonicznego. Z adresów, które nam oddajesz, poznajemy też wzór adresów na Twojej stronie.

  • Musi to być końcowy, publiczny adres wpisu, ten sam, który widzi czytelnik i który trafia do Google. Nie adres API, nie podgląd, nie adres panelu i nie adres, który dopiero przekierowuje.
  • Pełny adres bezwzględny, ze schematem i domeną, na przykład https://sklep.example/blog/jak-wybrac-ekspres/.
  • Ten sam przy każdej korekcie. Jeśli adres wpisu się zmieni, oddaj nowy przy najbliższej aktualizacji, bo stary zostałby w linkach z innych artykułów.

Sam kod 2xx nadal rozstrzyga o powodzeniu, więc odpowiedź bez url nie jest błędem. Bez niego nie znamy jednak prawdziwego adresu wpisu i nie możemy do niego pewnie linkować z Twoich kolejnych artykułów.

Kształt, który się nie psuje

Najbezpieczniejszy odbiornik jest idempotentny: szuka wpisu po kluczu, aktualizuje go, gdy istnieje, i tworzy, gdy go nie ma. Przy takim kształcie ponowiona dostawa nie robi duplikatu, a korekta nie gubi się dlatego, że ktoś skasował wpis ręcznie.

Zalecany przebieg

key = remote_id or article.id

find post where external_id = key
    found     -> update it
    not found -> create it, external_id = key

Cały odbiornik, w czystym PHP

Wszystkie zasady z tej strony w jednym pliku, bez frameworka i bez zależności. Funkcje oznaczone komentarzem <code>yours</code> są Twoje: magazyn nonce'ów, który zapomina po 600 sekundach, rejestr już obsłużonych identyfikatorów dostaw, to, czym zapisujesz wpis, oraz to, co zwraca jego publiczny adres. Reszta jest kompletna tak, jak stoi.

<?php

// One endpoint, no framework, no dependencies.

$secret = 'the same value you saved in our panel';
$path   = '/articles';   // the path of the URL you gave us, nothing more
$raw    = file_get_contents('php://input');

$version   = $_SERVER['HTTP_X_SIGNATURE_VERSION'] ?? '';
$timestamp = $_SERVER['HTTP_X_TIMESTAMP'] ?? '';
$nonce     = $_SERVER['HTTP_X_NONCE'] ?? '';
$signature = $_SERVER['HTTP_X_SIGNATURE'] ?? '';

function deny() {
    http_response_code(401);   // never say which check failed
    exit;
}

if ($version !== 'v1' || $timestamp === '' || $nonce === '' || $signature === '') {
    deny();
}

if (abs(time() - (int) $timestamp) > 300) {
    deny();
}

if (nonce_seen($nonce)) {      // yours: any store that forgets after 600s
    deny();
}

$canonical = implode("\n", [
    $version,
    $timestamp,
    $nonce,
    'POST',
    $path,
    hash('sha256', $raw),
]);

if (! hash_equals(hash_hmac('sha256', $canonical, $secret), $signature)) {
    deny();
}

remember_nonce($nonce, 600);   // yours

$envelope = json_decode($raw, true);

// The connection test. Create nothing, and say which events you handle.
// Drop article.delete from the list if your receiver cannot delete.
if (($envelope['event'] ?? '') === 'connection.test') {
    header('Content-Type: application/json');
    http_response_code(200);
    echo json_encode([
        'ok'       => true,
        'supports' => ['article.publish', 'article.update', 'article.delete'],
    ]);
    exit;
}

// A delete. Remove the post and say so; 404 when it is already gone.
if (($envelope['event'] ?? '') === 'article.delete') {
    $key = $envelope['remote_id'] ?? $envelope['target']['id'];

    if (! delete_post($key)) {         // yours: true when a post was removed
        http_response_code(404);
        exit;
    }

    header('Content-Type: application/json');
    http_response_code(200);
    echo json_encode(['deleted' => true]);
    exit;
}

// A retry of something we already handled. Answer 2xx, do nothing twice.
if (delivery_seen($envelope['id'])) {   // yours
    http_response_code(200);
    exit;
}

$article = $envelope['article'];
$key     = $envelope['remote_id'] ?? $article['id'];

$postId = upsert_post($key, $article);  // yours: find by $key, update or create

remember_delivery($envelope['id']);     // yours

header('Content-Type: application/json');
http_response_code(200);

echo json_encode([
    'id'  => (string) $postId,
    'url' => post_public_url($postId),  // yours: the final address readers see
]);

6. Co robimy przy każdym błędzie

Twoja odpowiedź decyduje, czy artykuł wróci do kolejki, czy zostanie oznaczony jako niedostarczony. Rozróżnienie jest tu naprawdę istotne i nie warto go zgadywać.

Odpowiedź Co robimy
2xx Sukces. Zapisujemy identyfikator wpisu i kończymy.
401, 403 Błąd ostateczny. Nie ponawiamy. Klient widzi zdanie o odrzuconym podpisie i prośbę o sprawdzenie, czy obie strony mają ten sam sekret.
404 Błąd ostateczny. Nie ponawiamy. Klient widzi prośbę o sprawdzenie adresu.
429 Błąd ostateczny. Nie ponawiamy, mimo że kod sugeruje inaczej. Ta dostawa się nie powiedzie. Jeśli musisz ograniczać nasze tempo, lepiej przyjmij żądanie kodem 2xx i wyhamuj u siebie w kolejce.
400, 402, 405 ... 499 Błąd ostateczny. Nie ponawiamy, bo powtórzenie tego samego żądania da ten sam wynik.
500 ... 599 Awaria przejściowa. Ponawiamy z narastającym odstępem.
timeout, reset Awaria przejściowa. Ponawiamy tak samo jak przy błędzie serwera.
url refused Błąd ostateczny, i to bez wysłania żądania. Adres nie przeszedł kontroli opisanej w sekcji pierwszej.

Harmonogram ponowień

Awarię przejściową próbujemy dostarczyć najwyżej pięć razy. Po pierwszej nieudanej próbie odstępy wynoszą kolejno:

1 minuta, 5 minut, 15 minut, 30 minut

Ostatnia próba wypada więc mniej więcej pięćdziesiąt minut po pierwszej. Jeśli i ona się nie powiedzie, artykuł zostaje oznaczony jako niedostarczony i czeka na człowieka.

Błąd ostateczny nie dostaje żadnej dodatkowej próby. Zatrzymuje artykuł natychmiast, bo ponawianie odrzuconego podpisu albo błędnego adresu tylko zużyłoby próby i niczego nie naprawiło.

Kiedy odrzucisz dostawę, zapisujemy u siebie kod odpowiedzi i pierwsze 500 znaków jej treści. Jeśli chcesz, żeby wsparcie mogło Ci szybko pomóc, zmieść w tym miejscu krótki, konkretny powód odmowy. Nie potrzebujemy śladu stosu z Twojego frameworka.

7. Lista Twoich artykułów (opcjonalnie)

To jest jedyna opcjonalna część tego połączenia. Jeśli wystawisz drugi adres, pod którym oddajesz spis wpisów już opublikowanych na Twojej stronie, będziemy go czytać. Nic tam nie zapisujemy i niczego nie zmieniamy: to jest wyłącznie odczyt.

Po co to Twojemu klientowi

  • Linkowanie wewnętrzne. Nowe artykuły mogą linkować do wpisów, które już masz, a nie tylko do tych, które dostarczyliśmy my. Linki między tekstami na jeden temat pomagają Google zrozumieć stronę i przenoszą ruch na starsze wpisy.
  • Brak kanibalizacji. Widzimy, o czym już piszesz, więc nie planujemy tematów i fraz, które Twoje wpisy już pokrywają. Nowy artykuł nie konkuruje wtedy ze starym o to samo zapytanie w Google, a dwa podobne teksty nie zabierają sobie nawzajem pozycji.

Bez tego adresu wszystko działa tak samo: publikujemy, poprawiamy i linkujemy. Obie rzeczy powyżej ograniczają się wtedy tylko do wpisów, które dostarczyliśmy sami, bo o reszcie Twojej strony nic nie wiemy.

Żądanie, które wysyłamy

Zwykłe żądanie GET pod adres, który podałeś, z dwoma parametrami zapytania: page (numer strony, od 1) i per_page (dzisiaj zawsze 100). Nie ma treści. Nagłówki podpisu są te same cztery co przy publikacji.

GET /posts?page=1&per_page=100 HTTP/1.1
Host:                hooks.example.com
Accept:              application/json
X-Signature-Version: v1
X-Timestamp:         1755859200
X-Nonce:             4d0e8b2a7c1f9e3605b8d2a4c7f1e09b
X-Signature:         8e3a1c5f07b92d64e1a8c3f5b0d7924e6a1c8f3b5d09e27a4c6f1b83d5e0a279

Podpis żądania GET

Ten sam schemat, ten sam sekret, to samo okno 300 sekund i ta sama zasada, że X-Nonce nie może się powtórzyć w ciągu 600 sekund. W ciągu kanonicznym różnią się trzy linie:

  • Linia czwarta to GET.
  • Linia piąta to sama ścieżka adresu listy, bez zapytania. Parametry page i per_page nie wchodzą do podpisu.
  • Linia szósta to SHA-256 pustej treści, czyli pustego ciągu znaków. Ta wartość jest zawsze taka sama i widać ją w przykładzie niżej.
v1
1755859200
4d0e8b2a7c1f9e3605b8d2a4c7f1e09b
GET
/posts
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

Odpowiedź, której oczekujemy

Kod 200 i jeden obiekt JSON z dwoma kluczami: articles, czyli lista wpisów na tej stronie, oraz next_page, czyli numer następnej strony albo null, jeśli to już ostatnia.

HTTP/1.1 200 OK
Content-Type: application/json

{
  "articles": [
    {
      "id": "8821",
      "url": "https://sklep.example/jak-wybrac-dobry-ekspres-do-kawy/",
      "title": "Jak wybrać dobry ekspres do kawy",
      "published_at": "2026-08-22T09:15:04+00:00",
      "excerpt": "Krótkie streszczenie wpisu.",
      "keywords": ["ekspres do kawy", "jaki ekspres wybrać"]
    },
    {
      "id": "8790",
      "url": "https://sklep.example/czyszczenie-ekspresu/",
      "title": "Jak wyczyścić ekspres ciśnieniowy",
      "published_at": null,
      "excerpt": null,
      "keywords": null
    }
  ],
  "next_page": 2
}

Pola wpisu na liście

Pole Typ Uwagi
articles[].id tekst Twój identyfikator wpisu. Wymagany.
articles[].url tekst, adres https Końcowy, publiczny adres wpisu, pełny i bezwzględny, tak samo jak w polu url z sekcji 5. Pod ten adres będziemy linkować. Wymagany.
articles[].title tekst Tytuł wpisu, zwykły tekst. Wymagany.
articles[].published_at tekst ISO 8601 albo null Moment publikacji wpisu.
articles[].excerpt tekst albo null Krótki opis wpisu, zwykły tekst. Pomaga nam zrozumieć, o czym jest wpis.
articles[].keywords lista tekstów albo null Frazy, na które wpis jest pisany, jeśli je znasz. Im dokładniejsze, tym lepiej unikamy tematów, które już pokrywasz.
next_page liczba albo null Pole całej odpowiedzi, nie wpisu. Numer strony, o którą mamy poprosić dalej, albo null na ostatniej stronie.

Stronicowanie

Zaczynamy od page=1 i idziemy za wartością next_page, dopóki nie dostaniemy null. Czytamy najwyżej 20 stron, a do spisu Twojej strony trafia najwyżej 1000 wpisów; kolejnych stron nie pobieramy. Dlatego oddawaj wpisy od najnowszych. Oddawaj tylko wpisy opublikowane i widoczne publicznie: bez szkiców, bez wpisów zaplanowanych na później i bez stron, których nie chcesz linkować.

Wiersz, któremu brakuje wymaganego pola albo w którym pole ma zły typ, pomijamy, a resztę strony czytamy dalej. Jeden wadliwy wpis nie psuje więc całej listy, ale też nie trafi do naszego spisu.

Kiedy to wołamy

Pierwszy raz przy zapisie połączenia, razem z testem z sekcji 3. Potem raz dziennie.

Co robimy przy błędzie

Przy zapisie połączenia każda odpowiedź inna niż 200, odpowiedź, która nie jest JSON-em, albo JSON bez listy articles i klucza next_page zatrzymuje zapis. Formularz pokazuje wtedy komunikat, a Ty poprawiasz adres albo odbiornik i próbujesz jeszcze raz, albo zostawiasz to pole puste.

Przy codziennym odczycie błąd niczego nie zatrzymuje. Publikacja idzie dalej normalnie, a my spróbujemy znowu następnego dnia.

Co robimy ze zdjęciami

Nie wysyłamy żadnych plików. Cała dostawa to jedno żądanie JSON, nigdy nie ma w niej danych wieloczęściowych ani obrazka zakodowanego w treści, i nigdy nie wgrywamy niczego do Twojej biblioteki mediów.

Obrazek wiodący dostajesz jako featured_image_url, czyli pełny adres https, albo jako null, jeśli artykuł go nie ma.

Obrazki wewnątrz treści zostają tam, gdzie były: jako zwykłe znaczniki w polu html, z adresami wskazującymi poza Twoją stronę.

Zalecamy przy odbiorze skopiować obrazki do własnego magazynu i podmienić adresy na swoje. Nie obiecujemy, że te adresy będą działać wiecznie, a artykuł z pustymi prostokątami zamiast zdjęć wygląda gorzej niż artykuł bez zdjęć.

Co robimy, a czego nie zrobimy nigdy

Co robimy

  • Każdy artykuł wysyłamy jako jedno żądanie POST pod adres, który zapisałeś.
  • Jeśli podasz adres listy artykułów, czytamy go podpisanym żądaniem GET przy zapisie połączenia i raz dziennie, i nic tam nie zapisujemy.
  • Podpisujemy każde żądanie, łącznie z testowym, tym samym schematem i tym samym sekretem.
  • Sprawdzamy adres przed każdym żądaniem i odmawiamy wysyłki pod adres wewnętrzny.
  • Zapisujemy identyfikator wpisu, który nam oddasz, i odsyłamy go przy każdej korekcie.
  • Ponawiamy dostawę, kiedy Twój serwer chwilowo nie odpowiada.
  • Wysyłamy podpisany ping, który niczego nie tworzy, kiedy klient sprawdza połączenie.

Czego nie zrobimy nigdy

  • Nie czytamy katalogu produktów. Za webhookiem nie ma katalogu, więc to połączenie publikuje artykuły i najwyżej czyta spis Twoich wpisów.
  • Nie wysyłamy szkiców. Pole statusu zawsze mówi publish, bo nie umiemy zagwarantować, że Twój odbiornik ukryłby szkic.
  • Nie wgrywamy plików i nie wysyłamy danych wieloczęściowych.
  • Nie łączymy się po zwykłym HTTP, nawet na wyraźną prośbę.
  • Nie wysyłamy niczego pod adres w sieci wewnętrznej ani pod domenę, która na taki adres wskazuje.
  • Nie kasujemy u Ciebie niczego, czego sami nie opublikowaliśmy. Jedyne usunięcie, o które prosimy, to nasz artykuł testowy, i tylko wtedy, gdy Twój odbiornik zadeklarował article.delete.
  • Nie wysyłamy sekretu w treści ani w nagłówku i nie zapisujemy go w logach.

Kiedy coś nie działa

Zdania z lewej kolumny widzi klient w panelu. Z prawej jest to, co naprawdę się stało, i co z tym zrobić po stronie odbiornika.

Objaw Przyczyna i co zrobić
To połączenie nie ma adresu docelowego. Pole adresu jest puste, więc nie ma dokąd wysłać żądania i nie wysyłamy żadnego. Uzupełnij adres endpointu i zapisz połączenie ponownie.
To połączenie nie ma sekretu do podpisu. Bez sekretu nie policzymy podpisu, a Twój odbiornik nie miałby czym nas zweryfikować, więc nie wysyłamy nic. Wygeneruj sekret i zapisz tę samą wartość po obu stronach.
Adres musi być adresem https. Podany adres nie zaczyna się od https://. Po zwykłym HTTP podpisany artykuł szedłby otwartym tekstem. Nie jest to ograniczenie, które zdejmujemy na prośbę.
Usuń nazwę użytkownika i hasło z adresu. W adresie jest forma user:pass@host. Uwierzytelnieniem jest podpis, a nie adres. Jeśli Twój odbiornik stoi za uwierzytelnianiem podstawowym, zdejmij je z tej ścieżki i oprzyj się na podpisie.
Ta nazwa hosta się nie rozwiązuje. DNS nie zwrócił dla niej żadnego adresu. Sprawdź, czy nazwa nie ma literówki i czy strefa jest opublikowana. Świeżo dodana nazwa potrafi jeszcze nie być widoczna wszędzie.
Ten adres prowadzi do sieci wewnętrznej. Nazwa rozwiązała się na adres z odrzucanego zakresu. Zdarza się to także wtedy, gdy publiczna domena ma jeden rekord wskazujący do środka: wystarczy jeden taki adres wśród kilku, żeby odrzucić cały cel. Wystaw odbiornik pod adresem osiągalnym publicznie.
Twój endpoint odrzucił nasz podpis. Odpowiedziałeś kodem 401 albo 403. Najczęściej powodem jest inny sekret po jednej ze stron, policzenie skrótu z treści już sparsowanej i ponownie zakodowanej, albo użycie w podpisie pełnego adresu zamiast samej ścieżki. Sprawdź to w tej kolejności.
Twój endpoint odpowiedział kodem 404. Pod tą ścieżką nic nie nasłuchuje. Sprawdź adres, a zwłaszcza to, czy trasa przyjmuje metodę POST, bo trasa zdefiniowana tylko dla GET daje w wielu frameworkach dokładnie ten kod.
Pod tym adresem jest zwykła strona, a nie odbiornik artykułów. Odpowiedź miała kod 2xx, ale była stroną HTML. Tak odpowiada strona główna albo dowolna podstrona serwisu: przyjmuje żądanie POST, ignoruje je i oddaje swój kod HTML, więc artykuł nigdzie nie trafia. Wpisz adres endpointu z dokumentacji odbiornika, a jeśli Twoja strona stoi na WordPressie albo innym obsługiwanym CMS-ie, podłącz go bezpośrednio zamiast własnego endpointu.
Twój endpoint odpowiedział na wiadomość testową bez listy "supports". Odbiornik odpowiada na connection.test samym kodem 2xx, tak jak każdy odbiornik napisany, zanim wprowadziliśmy deklarację. Dodaj do odpowiedzi {"ok": true, "supports": ["article.publish", "article.update"]} i dopisz article.delete, jeśli je obsługujesz. Wymieniaj tylko to, co odbiornik naprawdę robi.
Twój endpoint nie deklaruje article.update. Bez obsługi korekty każda poprawka opublikowanego artykułu dodałaby u Ciebie drugi wpis zamiast zmienić pierwszy, a duplikaty szkodzą pozycjom w Google. Obsłuż article.update tak, jak opisuje sekcja 3 (znajdź wpis po remote_id i zaktualizuj go), i dopisz je do listy supports.
Twój endpoint poprosił nas o zwolnienie. Odpowiedziałeś kodem 429. Ta dostawa jest stracona, bo tego kodu nie ponawiamy. Jeśli musisz nas ograniczać, przyjmij żądanie kodem 2xx i wyhamuj we własnej kolejce.
Twój endpoint odpowiedział błędem serwera. Kod z zakresu 5xx. Uznajemy to za awarię przejściową i ponawiamy z narastającym odstępem, więc jednorazowa czkawka niczego nie kosztuje. Jeśli powtarza się przy każdej próbie, zajrzyj do własnego logu.
Nie udało się dotrzeć do tego adresu w czasie. Nie odpowiedziałeś w ciągu 15 sekund albo połączenie zerwało się w trakcie. Ponawiamy. Jeśli zdarza się stale, znaczy że odbiornik robi za dużo w samym żądaniu: przyjmij artykuł, odpowiedz od razu, resztę przenieś do kolejki.
Twój endpoint odrzucił artykuł innym kodem. Każdy inny kod spoza 2xx traktujemy jako odmowę ostateczną. Podajemy numer w komunikacie, a w naszym logu mamy pierwsze 500 znaków Twojej odpowiedzi, więc krótki powód odmowy w treści bardzo przyspiesza rozmowę ze wsparciem.
Po każdym teście połączenia pojawia się pusty wpis. Odbiornik traktuje zdarzenie connection.test jak publikację. W tym zdarzeniu pole article jest puste. Sprawdź podpis, nie twórz niczego i odpowiedz kodem 2xx.
Ten sam artykuł pojawia się u mnie kilka razy. Odbiornik tworzy nowy wpis przy każdej dostawie. Zapisuj klucz id z koperty i pomijaj powtórzenia, a wpisy wiąż z article.id albo oddawaj nam swój identyfikator przy każdej publikacji.
Korekty przychodzą z identyfikatorem, którego nie znam. Przy publikacji nie oddałeś swojego identyfikatora, więc zapamiętaliśmy nasz i to jego odsyłamy w polu remote_id. Albo zacznij oddawać swój identyfikator, albo zapisuj nasz przy wpisie.
Artykuł pojawił się bez zdjęcia. Nie wgrywamy plików. Obrazek wiodący jest adresem w polu featured_image_url, a zdjęcia w treści to zwykłe znaczniki w polu html. To odbiornik decyduje, czy je pobrać i zapisać u siebie.
Nie ma synchronizacji produktów. To nie jest usterka. Za webhookiem nie ma katalogu do odczytania, więc nie oferujemy przy tym połączeniu synchronizacji produktów. Opcjonalny adres listy artykułów z sekcji 7 dotyczy wpisów, nie produktów.

Lista kontrolna odbiornika

Przejdź to przed pierwszą publikacją. Każdy punkt odpowiada jednej rzeczy, która potrafi się zepsuć cicho.

  • Czytasz surową treść żądania przed jakimkolwiek parsowaniem i liczysz skrót z dokładnie tych bajtów.
  • Odrzucasz brakującą albo nieznaną wartość X-Signature-Version.
  • Odrzucasz znacznik czasu odległy o więcej niż 300 sekund.
  • Trzymasz zużyte wartości X-Nonce przez 600 sekund i odrzucasz powtórzenie.
  • Porównujesz podpisy funkcją działającą w stałym czasie.
  • Na zdarzenie connection.test odpowiadasz kodem 2xx, nie tworzysz niczego i wymieniasz w polu supports obsługiwane zdarzenia, co najmniej article.publish i article.update.
  • Wiążesz wpisy z article.id albo oddajesz nam swój identyfikator przy każdej publikacji.
  • Pomijasz dostawę, której klucz id już przetworzyłeś.
  • Ignorujesz nieznane klucze, żeby przyszłe dodanie pola niczego nie zepsuło.
  • Filtrujesz pole html przed wyrenderowaniem.
  • Odpowiadasz w czasie krótszym niż 15 sekund, a wolną pracę przenosisz do kolejki.
  • Twój certyfikat jest ważny i łańcuch jest kompletny, bo łączymy się wyłącznie po https.
  • W odpowiedzi na publikację i korektę oddajesz url, czyli końcowy, publiczny adres wpisu.

Jeśli wystawiasz listę artykułów (opcjonalnie)

  • Adres listy przyjmuje metodę GET i odpowiada kodem 200.
  • Sprawdzasz podpis po metodzie GET, ścieżce bez zapytania i skrócie pustej treści, z tym samym oknem czasu i tą samą kontrolą nonce.
  • Oddajesz obiekt z listą articles i kluczem next_page, a każdy wpis ma co najmniej id, url i title.
  • Szanujesz page i per_page, na ostatniej stronie oddajesz next_page: null, a wpisy układasz od najnowszych.
  • Na liście są tylko wpisy opublikowane i widoczne publicznie, z ich końcowymi adresami.