Получаване на уебхукове
Уебхукът съобщава на системата ви за промяна в Sursa в момента, в който се случи: карта е публикувана, проверена или
архивирана, въпрос е останал без отговор. Sursa изпраща HTTPS POST със събитието до адрес, който регистрирате. Това
ръководство изгражда получател, който работи правилно при повторни опити, разместен ред и дубликати.
Какво означава и какво съдържа всяко събитие, вижте в общия преглед на уебхуковете и в каталога на събитията в справочника на API.
1. Регистрирайте уебхук
Администраторът регистрира уебхукове в Sursa в Administration → Integrations → Webhooks and API (администриране,
интеграции, уебхукове и API) или с POST /v1/webhooks и ключ с ролята
admin и обхвата 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"]
}'Отговорът съдържа тайната за подписване в secret, която започва с whsec_. Тя се показва веднъж. Съхранете я
при получателя си, например в променлива на средата SURSA_WEBHOOK_SECRET.
Адресът трябва да използва https и да сочи към публични IP адреси: адресите в частни, локални (loopback) и други
резервирани мрежи се отхвърлят при регистрацията и при всяка доставка. За да разработвате на собствения си компютър,
направете локалния си получател достъпен чрез услуга за тунелиране, която му дава публичен HTTPS адрес.
Изпращайте тестово събитие по всяко време с POST /v1/webhooks/{webhook_id}/test.
Отговорът показва какво е отговорил получателят ви.
2. Как изглежда една доставка
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}
| Заглавка | Значение |
|---|---|
webhook-id | ID на събитието. Едно и също при всеки повторен опит. Използвайте го, за да отхвърляте дубликатите. |
webhook-timestamp | Кога е подписан този опит, в секунди по Unix. |
webhook-signature | Подписът във формата на Standard Webhooks. |
X-KB-Signature | Същата доставка, подписана в по-ранния формат на Sursa. |
X-KB-Event | Типът на събитието. |
X-KB-Attempt | Номерът на опита, от 1. |
X-KB-Sequence | Позицията на тази доставка в реда на доставките на уебхука. |
Имената на заглавките не зависят от регистъра на буквите; вашата рамка може да ги показва като X-Kb-Signature или
x-kb-signature.
Тялото е събитието. Неговото data зависи от типа. Уебхук с режим на тялото thin получава събитието без data;
вместо това прочетете обекта през API със собствения си ключ.
3. Проверете подписа
Проверявайте подписа на всяка доставка, преди да ѝ се доверите. И двата формата подписват точните байтове на тялото, затова прочетете необработеното тяло преди всякакъв разбор на JSON: повторното сериализиране на JSON променя байтовете и проверката се проваля.
| Формат | Подписван низ | Ключ | Подпис |
|---|---|---|---|
Standard Webhooks (webhook-signature) | <webhook-id>.<webhook-timestamp>.<body> | Частта от тайната след whsec_, декодирана от Base64 | v1, + Base64 на HMAC-SHA256 |
По-ранен формат (X-KB-Signature) | <t>.<body> | Целият низ на тайната, като текст | v1= + шестнайсетичен запис на HMAC-SHA256 |
Достатъчно е да проверите единия от двата. Предпочитайте заглавката на Standard Webhooks: същата проверка е налична в
много готови библиотеки. По време на смяна на тайната всяка заглавка носи по един подпис за всяка валидна тайна
(разделени с интервал в webhook-signature, повторено v1= в X-KB-Signature). Доставката е валидна, ако съвпада
който и да е от тях.
Отхвърляйте и времеви печат, който се отклонява с повече от 5 минути от вашия часовник. Така никой не може по-късно да изпрати повторно записана доставка.
# 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Тествайте проверката на подписа
Пуснете кода си с тези стойности, преди да го свържете с реални доставки. Когато часовникът е настроен на времевия печат, и двете проверки трябва да минат успешно и трябва да се провалят, когато се промени един байт от тялото.
| Входни данни | Стойност |
|---|---|
| Тайна | whsec_test_secret_0123456789abcdef |
webhook-id | evt_0123456789abcdef0123456789abcdef |
webhook-timestamp и t | 1791194400 |
| Тяло | {"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. Отговаряйте бързо, работете после
Sursa чака отговора ви до 10 секунди. Всеки статус 2xx означава, че доставката е успешна; всичко друго,
включително пренасочване, изтичане на времето за изчакване или затворена връзка, означава, че тя е неуспешна и ще
бъде повторена.
Затова получателят трябва само да провери, да запише събитието и да отговори. Истинската работа вършете след това, от собствената си опашка.
// 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);Отговаряйте с 2xx едва след като събитието е надеждно съхранено. Ако първо отговорите и после процесът ви се срине,
събитието е изгубено за вас, защото Sursa вече го е отчела като доставено.
5. Повторни опити
Неуспешната доставка се повтаря с нарастващи паузи: след около 1 минута, 5 минути, 30 минути, 2 часа, 5 часа, 10 часа, 24 часа и 24 часа – девет опита за почти три дни. Всяка пауза варира с до 20 %, за да не пристигат повторните опити от много уебхукове в един и същ момент.
- Отговор
429или503сRetry-Afterотлага следващия опит поне до това време, но не повече от 24 часа. Използвайте го, когато системата ви е претоварена или в профилактика. - Отговор
410 Goneизключва уебхука: изпращайте го само когато не искате повече доставки на този адрес. - След 10 поредни неуспешни опита уебхукът се спира временно и администраторите на организацията се уведомяват.
Събитията продължават да се натрупват и когато уебхукът бъде възобновен – в интерфейса за администриране или с
POST /v1/webhooks/{webhook_id}/resume, те се доставят по ред. - След последния опит доставката се отбелязва като
exhausted. Може да я изпратите отново ръчно.
Доставките на уебхука, с всеки опит, кода на отговора ви и времето му, са изброени от
GET /v1/webhooks/{webhook_id}/deliveries и в интерфейса за
администриране. POST /v1/webhooks/{webhook_id}/deliveries/{delivery_id}/redeliver
изпраща доставка отново. Повторната доставка има същия webhook-id.
6. Ред и дубликати
- Ред. Доставките към един уебхук минават строго една след друга в реда на
sequence. Докато една доставка се повтаря, следващите чакат. Различните уебхукове не се чакат един друг. - Дубликати. Едно и също събитие може да пристигне повече от веднъж, например когато отговорът ви се е изгубил
на връщане. Съхранявайте
webhook-id(то е иidв тялото) на всяко обработено събитие и пропускайте вече виждано събитие. - Мислете в състояния. При промени на един и същ обект сравнявайте
created_atилиsequenceсъс съхраненото и пренебрегвайте събитие, което е по-старо от състоянието, което вече имате. При съмнение прочетете текущия обект през API: събитието ви казва, че нещо се е променило, а API ви казва какво е то сега.
7. Сверявайте с лентата със събития
Всяко събитие се пази 30 дни и в лентата със събития, независимо дали някой уебхук е бил абониран за него. Ако
получателят ви не е работил по-дълго, отколкото покриват повторните опити, или искате да проверите, че нищо не е
пропуснато, прочетете лентата с ключ, който има ролята admin и обхвата 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"Събитието в лентата има същия id и същата структура като в доставката, без полетата на доставката sequence,
payload и test. Тъй като получателят ви пропуска вече видените събития, може да пускате сверката колкото често
искате, например веднъж на час.
GET /v1/webhook-events/{event_id} чете едно събитие по неговото ID.
8. Сменете тайната
POST /v1/webhooks/{webhook_id}/rotate-secret издава нова тайна и
запазва предишната валидна за период на припокриване – по подразбиране 24 часа и до 7 дни. По време на
припокриването всяка доставка се подписва и с двете тайни.
- Сменете тайната и съхранете новата до старата в получателя си.
- Приемайте доставка, ако тя преминава проверката с която и да е от двете тайни.
- След края на припокриването премахнете старата тайна.
Ако старата тайна е изтекла навън, прекратете припокриването веднага с
POST /v1/webhooks/{webhook_id}/revoke-previous-secret или
сменете тайната с "overlap_hours": 0.
Контролен списък
- Проверявайте един от подписите върху необработеното тяло и проверявайте времевия печат.
- Съхранявайте събитието с ключ
webhook-idи след това отговаряйте с2xxдо 10 секунди. - Пропускайте дубликатите и остарелите събития; когато е важно, четете текущия обект през API.
- Отговаряйте с
429или503иRetry-After, когато трябва Sursa да намали темпото; никога не отговаряйте с410по невнимание. - Пропускайте типовете събития и полетата, които не познавате.
- Правете сверка с лентата със събития по график.