Przejdź do treści
Sursa Developers

Odbieraj webhooki

Webhook informuje Twój system o zmianie w Sursie w chwili, gdy ona następuje: karta zostaje opublikowana, zweryfikowana lub zarchiwizowana, pytanie pozostaje bez odpowiedzi. Sursa wysyła żądanie HTTPS POST ze zdarzeniem na zarejestrowany przez Ciebie adres. Ten przewodnik buduje odbiorcę, który działa poprawnie mimo ponowień, zmian kolejności i duplikatów.

Co oznacza i zawiera każde zdarzenie, opisują przegląd webhooków i katalog zdarzeń w dokumentacji API.

1. Zarejestruj webhook

Administrator rejestruje webhooki w Sursie w sekcji Administration → Integrations → Webhooks and API (administracja → integracje → webhooki) albo za pomocą POST /v1/webhooks i klucza z rolą admin i zakresem webhooks:manage:

curl -X POST https://api.sursa.com/v1/webhooks \
  -H "Authorization: Bearer $SURSA_ADMIN_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://hooks.example.com/sursa",
    "description": "Sync verified cards to the help center",
    "events": ["card.published", "card.updated", "card.verified", "card.archived"]
  }'

Odpowiedź zawiera sekret podpisu w secret, zaczynający się od whsec_. Jest on pokazywany tylko raz. Przechowuj go razem z odbiorcą, np. w zmiennej środowiskowej SURSA_WEBHOOK_SECRET.

Adres musi używać https i wskazywać na publiczne adresy IP: adresy w sieciach prywatnych, w pętli zwrotnej i w innych sieciach zastrzeżonych są odrzucane przy rejestracji i przy każdym dostarczeniu. Aby pracować na własnym komputerze, udostępnij lokalnego odbiorcę przez usługę tunelowania, która nada mu publiczny adres HTTPS.

Zdarzenie testowe możesz wysłać w każdej chwili za pomocą POST /v1/webhooks/{webhook_id}/test. Odpowiedź pokazuje, co odpowiedział Twój odbiorca.

2. Jak wygląda dostarczenie

POST /sursa HTTP/1.1
Host: hooks.example.com
Content-Type: application/json
webhook-id: evt_0123456789abcdef0123456789abcdef
webhook-timestamp: 1791194400
webhook-signature: v1,l1LLl/aEl4dQq7Geb383KbyNeZgs7VZpiw6iuoJgunI=
X-KB-Signature: t=1791194400,v1=82aa6b27378d89d7d880d17c3fe96268598443ae0e64d059c0c41e94e5d3ffbe
X-KB-Event-Id: evt_0123456789abcdef0123456789abcdef
X-KB-Event: card.verified
X-KB-Delivery: 99812
X-KB-Attempt: 1
X-KB-Sequence: 1042

{"id":"evt_0123456789abcdef0123456789abcdef","type":"card.verified","created_at":"2026-10-05T10:00:00Z","org_id":1,"api_version":"1.60.0","sequence":1042,"object":{"type":"card","id":"c1"},"payload":"full","data":{"card":{"id":"c1"}},"test":false}
NagłówekZnaczenie
webhook-idID zdarzenia. Takie samo przy każdym ponowieniu. Używaj go do odrzucania duplikatów.
webhook-timestampKiedy ta próba została podpisana, w sekundach uniksowych.
webhook-signaturePodpis w formacie Standard Webhooks.
X-KB-SignatureTo samo dostarczenie podpisane we wcześniejszym formacie Sursy.
X-KB-EventTyp zdarzenia.
X-KB-AttemptNumer próby, od 1.
X-KB-SequencePozycja tego dostarczenia w kolejności dostarczeń webhooka.

Wielkość liter w nazwach nagłówków nie ma znaczenia; Twój framework może pokazywać je jako X-Kb-Signature lub x-kb-signature.

Treścią żądania jest zdarzenie. Jego data zależy od typu. Webhook ustawiony na tryb treści thin otrzymuje zdarzenie bez data; zamiast tego odczytaj obiekt przez API własnym kluczem.

3. Zweryfikuj podpis

Sprawdzaj podpis każdego dostarczenia, zanim mu zaufasz. Oba formaty podpisują dokładne bajty treści, więc odczytaj surową treść przed jakimkolwiek parsowaniem JSON: ponowna serializacja JSON zmienia bajty i psuje weryfikację.

FormatPodpisywany ciągKluczPodpis
Standard Webhooks (webhook-signature)<webhook-id>.<webhook-timestamp>.<body>Część sekretu po whsec_, zdekodowana z base64v1, + base64 z HMAC-SHA256
Wcześniejszy format (X-KB-Signature)<t>.<body>Cały ciąg sekretu jako tekstv1= + zapis szesnastkowy HMAC-SHA256

Wystarczy zweryfikować jeden z nich. Preferuj nagłówek Standard Webhooks: ta sama weryfikacja jest dostępna w wielu gotowych bibliotekach. Podczas wymiany sekretu każdy nagłówek zawiera po jednym podpisie dla każdego ważnego sekretu (oddzielone spacjami w webhook-signature, powtórzone v1= w X-KB-Signature). Dostarczenie jest prawidłowe, jeśli pasuje którykolwiek z nich.

Odrzucaj też znacznik czasu, który różni się od wskazań Twojego zegara o ponad 5 minut. Dzięki temu nikt nie może później ponownie odtworzyć nagranego dostarczenia.

# Check a saved delivery by hand: body.json holds the raw body, the values come from its headers.
SECRET="whsec_…"; WEBHOOK_ID="evt_…"; TIMESTAMP="1791194400"

# Standard Webhooks: compare with the value after "v1," in webhook-signature
KEY=$(printf '%s' "${SECRET#whsec_}" | tr '_-' '/+')
while [ $(( ${#KEY} % 4 )) -ne 0 ]; do KEY="$KEY="; done
KEY_HEX=$(printf '%s' "$KEY" | openssl base64 -d -A | xxd -p | tr -d '\n')
{ printf '%s.%s.' "$WEBHOOK_ID" "$TIMESTAMP"; cat body.json; } \
  | openssl dgst -sha256 -mac HMAC -macopt "hexkey:$KEY_HEX" -binary | openssl base64 -A

# Earlier format: compare with the value after "v1=" in X-KB-Signature
{ printf '%s.' "$TIMESTAMP"; cat body.json; } | openssl dgst -sha256 -hmac "$SECRET" -r | cut -d' ' -f1

Przetestuj swoją weryfikację

Uruchom swój kod na tych wartościach, zanim podłączysz go do prawdziwych dostarczeń. Przy zegarze ustawionym na podany znacznik czasu obie weryfikacje muszą się powieść, a po zmianie jednego bajtu treści – zakończyć się niepowodzeniem.

Dane wejścioweWartość
Sekretwhsec_test_secret_0123456789abcdef
webhook-idevt_0123456789abcdef0123456789abcdef
webhook-timestamp i t1791194400
Treść{"id":"evt_0123456789abcdef0123456789abcdef","type":"card.verified","created_at":"2026-10-05T10:00:00Z","org_id":1,"data":{"card":{"id":"c1"}}}
webhook-signaturev1,+xxWSXH6weQJ/MVg4+N78a1Dj4y/AQ4p94+PRNzpECg=
X-KB-Signaturet=1791194400,v1=ce26a19cb63cd3ae620217bf7688e1df1044fa7d59d9ae148d215f0292b768dc

4. Odpowiadaj szybko, pracuj później

Sursa czeka na Twoją odpowiedź do 10 sekund. Każdy status 2xx oznacza, że dostarczenie się powiodło; cokolwiek innego, w tym przekierowanie, przekroczenie limitu czasu lub zamknięte połączenie, oznacza niepowodzenie i ponowienie.

Dlatego odbiorca powinien tylko zweryfikować podpis, zapisać zdarzenie i odpowiedzieć. Właściwą pracę wykonaj później, z własnej kolejki.

// Node.js 20+, no framework. verifyStandard and verifyLegacy come from step 3.
import http from "node:http";

const SECRET = process.env.SURSA_WEBHOOK_SECRET;

http
  .createServer(async (req, res) => {
    const parts = [];
    for await (const part of req) parts.push(part);
    const body = Buffer.concat(parts); // the raw bytes, before any JSON parsing

    const valid = req.headers["webhook-signature"]
      ? verifyStandard(SECRET, req.headers, body)
      : verifyLegacy(SECRET, req.headers["x-kb-signature"], body);
    if (!valid) {
      res.writeHead(400).end();
      return;
    }

    const event = JSON.parse(body);
    await saveEventOnce(event); // insert into your table or queue, keyed by event.id; ignore a duplicate
    res.writeHead(204).end();
  })
  .listen(3000);

Odpowiadaj 2xx dopiero wtedy, gdy zdarzenie jest bezpiecznie zapisane. Jeśli najpierw odpowiesz, a potem nastąpi awaria, zdarzenie przepadnie dla Ciebie, ponieważ Sursa uznała je już za dostarczone.

5. Ponowienia

Nieudane dostarczenie jest ponawiane z coraz dłuższymi przerwami: po około 1 minucie, 5 minutach, 30 minutach, 2 godzinach, 5 godzinach, 10 godzinach, 24 godzinach i ponownie 24 godzinach – łącznie dziewięć prób w ciągu prawie trzech dni. Każda przerwa różni się o maksymalnie 20%, aby ponowienia z wielu webhooków nie docierały w tym samym momencie.

  • Odpowiedź 429 lub 503 z Retry-After przesuwa następną próbę co najmniej na ten czas, maksymalnie o 24 godziny. Używaj jej, gdy Twój system jest przeciążony lub trwają prace konserwacyjne.
  • Odpowiedź 410 Gone wyłącza webhook: wysyłaj ją tylko wtedy, gdy nie chcesz już dostarczeń pod tym adresem.
  • Po 10 kolejnych nieudanych próbach webhook zostaje wstrzymany, a administratorzy organizacji otrzymują powiadomienie. Zdarzenia nadal się gromadzą, a po wznowieniu webhooka, w panelu administracyjnym lub za pomocą POST /v1/webhooks/{webhook_id}/resume, są dostarczane po kolei.
  • Po ostatniej próbie dostarczenie jest oznaczane jako exhausted. Możesz wysłać je ponownie ręcznie.

Dostarczenia webhooka, z każdą próbą, kodem Twojej odpowiedzi i jej czasem, są wymienione w GET /v1/webhooks/{webhook_id}/deliveries i w panelu administracyjnym. POST /v1/webhooks/{webhook_id}/deliveries/{delivery_id}/redeliver wysyła jedno z nich ponownie. Ponowne dostarczenie ma ten sam webhook-id.

6. Kolejność i duplikaty

  • Kolejność. Dostarczenia do jednego webhooka są wysyłane ściśle jedno po drugim w kolejności sequence. Gdy jedno dostarczenie jest ponawiane, późniejsze czekają. Różne webhooki nie czekają na siebie nawzajem.
  • Duplikaty. To samo zdarzenie może dotrzeć więcej niż raz, np. gdy Twoja odpowiedź zaginęła w drodze powrotnej. Zapisuj webhook-id (jest to też id w treści) każdego przetworzonego zdarzenia i pomijaj zdarzenie, które już zostało przetworzone.
  • Myślenie w kategoriach stanów. Przy zmianach tego samego obiektu porównuj created_at lub sequence z tym, co zapisano, i ignoruj zdarzenie starsze niż stan, który już masz. W razie wątpliwości odczytaj bieżący obiekt przez API: zdarzenie mówi, że coś się zmieniło, a API mówi, jaki jest stan teraz.

7. Uzgadniaj dane z kanałem zdarzeń

Każde zdarzenie jest też przechowywane przez 30 dni w kanale zdarzeń, niezależnie od tego, czy jakiś webhook je subskrybował. Jeśli Twój odbiorca nie działał dłużej, niż obejmują ponowienia, lub chcesz sprawdzić, czy niczego nie pominięto, odczytaj kanał kluczem, który ma rolę admin i zakres webhooks:manage:

curl -G https://api.sursa.com/v1/webhook-events \
  -H "Authorization: Bearer $SURSA_API_KEY" \
  --data-urlencode "type=card.*" \
  --data-urlencode "created_since=2026-10-20T00:00:00Z" \
  --data-urlencode "limit=200"

Zdarzenie w kanale ma ten sam id i tę samą strukturę co w dostarczeniu, bez pól dostarczenia sequence, payload i test. Ponieważ Twój odbiorca pomija zdarzenia, które już widział, uzgadnianie możesz uruchamiać tak często, jak chcesz, np. raz na godzinę.

GET /v1/webhook-events/{event_id} odczytuje jedno zdarzenie według jego ID.

8. Wymień sekret

POST /v1/webhooks/{webhook_id}/rotate-secret wydaje nowy sekret i zachowuje ważność poprzedniego przez okres nakładania: domyślnie 24 godziny, maksymalnie 7 dni. W okresie nakładania każde dostarczenie jest podpisywane obydwoma sekretami.

  1. Wymień sekret i zapisz nowy obok starego w swoim odbiorcy.
  2. Akceptuj dostarczenie, jeśli weryfikuje się którymkolwiek z sekretów.
  3. Po zakończeniu okresu nakładania usuń stary sekret.

Jeśli stary sekret wyciekł, zakończ okres nakładania od razu za pomocą POST /v1/webhooks/{webhook_id}/revoke-previous-secret albo wymień sekret z "overlap_hours": 0.

Lista kontrolna

  • Weryfikuj jeden z podpisów na surowej treści i sprawdzaj znacznik czasu.
  • Zapisz zdarzenie z kluczem webhook-id, a następnie odpowiedz 2xx w ciągu 10 sekund.
  • Pomijaj duplikaty i nieaktualne zdarzenia; gdy ma to znaczenie, odczytaj bieżący obiekt przez API.
  • Odpowiadaj 429 lub 503 z Retry-After, gdy Sursa ma zwolnić; nigdy nie odpowiadaj 410 przez przypadek.
  • Pomijaj typy zdarzeń i pola, których nie znasz.
  • Uzgadniaj dane z kanałem zdarzeń według harmonogramu.