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łówek | Znaczenie |
|---|---|
webhook-id | ID zdarzenia. Takie samo przy każdym ponowieniu. Używaj go do odrzucania duplikatów. |
webhook-timestamp | Kiedy ta próba została podpisana, w sekundach uniksowych. |
webhook-signature | Podpis w formacie Standard Webhooks. |
X-KB-Signature | To samo dostarczenie podpisane we wcześniejszym formacie Sursy. |
X-KB-Event | Typ zdarzenia. |
X-KB-Attempt | Numer próby, od 1. |
X-KB-Sequence | Pozycja 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ę.
| Format | Podpisywany ciąg | Klucz | Podpis |
|---|---|---|---|
Standard Webhooks (webhook-signature) | <webhook-id>.<webhook-timestamp>.<body> | Część sekretu po whsec_, zdekodowana z base64 | v1, + base64 z HMAC-SHA256 |
Wcześniejszy format (X-KB-Signature) | <t>.<body> | Cały ciąg sekretu jako tekst | v1= + 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' ' -f1Przetestuj 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ściowe | Wartość |
|---|---|
| Sekret | whsec_test_secret_0123456789abcdef |
webhook-id | evt_0123456789abcdef0123456789abcdef |
webhook-timestamp i t | 1791194400 |
| Treść | {"id":"evt_0123456789abcdef0123456789abcdef","type":"card.verified","created_at":"2026-10-05T10:00:00Z","org_id":1,"data":{"card":{"id":"c1"}}} |
webhook-signature | v1,+xxWSXH6weQJ/MVg4+N78a1Dj4y/AQ4p94+PRNzpECg= |
X-KB-Signature | t=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ź
429lub503zRetry-Afterprzesuwa 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 Gonewyłą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żidw 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_atlubsequencez 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.
- Wymień sekret i zapisz nowy obok starego w swoim odbiorcy.
- Akceptuj dostarczenie, jeśli weryfikuje się którymkolwiek z sekretów.
- 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 odpowiedz2xxw ciągu 10 sekund. - Pomijaj duplikaty i nieaktualne zdarzenia; gdy ma to znaczenie, odczytaj bieżący obiekt przez API.
- Odpowiadaj
429lub503zRetry-After, gdy Sursa ma zwolnić; nigdy nie odpowiadaj410przez przypadek. - Pomijaj typy zdarzeń i pola, których nie znasz.
- Uzgadniaj dane z kanałem zdarzeń według harmonogramu.