Отримуйте вебхуки
Вебхук повідомляє вашу систему про зміну в Sursa в момент, коли вона стається: картку опубліковано, верифіковано чи
архівовано, запитання лишилося без відповіді. Sursa надсилає HTTPS-запит POST з подією на зареєстровану вами
адресу. У цьому посібнику ви створите отримувач, що працює правильно попри повтори, зміну порядку й дублікати.
Що означає і що містить кожна подія, див. в огляді вебхуків і в каталозі подій довідника API.
1. Зареєструйте вебхук
Адміністратор реєструє вебхуки в Sursa в розділі Administration → Integrations → Webhooks and 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= + hex від 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випадково. - Пропускайте невідомі типи подій і поля.
- Звіряйтеся зі стрічкою подій за розкладом.