Příjem webhooků
Webhook informuje váš systém o změně v Surse v okamžiku, kdy nastane: karta je publikována, ověřena nebo
archivována, otázka zůstala bez odpovědi. Sursa odešle POST přes HTTPS s událostí na adresu, kterou zaregistrujete.
Tento návod sestaví příjemce, který funguje správně i při opakováních, změněném pořadí a duplicitách.
Co jednotlivé události znamenají a obsahují, najdete v přehledu webhooků a v katalogu událostí v referenci API.
1. Zaregistrujte webhook
Administrátor registruje webhooky v Surse v Administration → Integrations → Webhooks and API (administrace →
integrace → webhooky) nebo pomocí POST /v1/webhooks s klíčem s rolí admin
a rozsahem 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"]
}'Odpověď obsahuje tajný klíč podpisu v secret, který začíná na whsec_. Zobrazí se jen jednou. Uložte ho
u svého příjemce, například do proměnné prostředí SURSA_WEBHOOK_SECRET.
Adresa musí používat https a překládat se na veřejné IP adresy: adresy v privátních, loopback a jiných
vyhrazených sítích se odmítnou při registraci i při každém doručení. Při vývoji na vlastním počítači zpřístupněte
místního příjemce přes tunelovací službu, která mu přidělí veřejnou adresu HTTPS.
Testovací událost můžete kdykoli odeslat pomocí POST /v1/webhooks/{webhook_id}/test.
Odpověď ukazuje, co váš příjemce odpověděl.
2. Jak vypadá doručení
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}
| Hlavička | Význam |
|---|---|
webhook-id | ID události. Stejné při každém opakování. Používejte ho k zahazování duplicit. |
webhook-timestamp | Kdy byl tento pokus podepsán, v unixových sekundách. |
webhook-signature | Podpis ve formátu Standard Webhooks. |
X-KB-Signature | Totéž doručení podepsané v dřívějším formátu Sursy. |
X-KB-Event | Typ události. |
X-KB-Attempt | Číslo pokusu, od 1. |
X-KB-Sequence | Pozice tohoto doručení v pořadí webhooku. |
Názvy hlaviček nerozlišují velká a malá písmena; váš framework je může zobrazovat jako X-Kb-Signature nebo
x-kb-signature.
Tělo tvoří událost. Její data závisí na typu. Webhook nastavený na režim obsahu thin dostává událost bez data;
objekt si místo toho přečtěte přes API vlastním klíčem.
3. Ověřte podpis
Podpis každého doručení zkontrolujte dřív, než mu začnete důvěřovat. Oba formáty podepisují přesné bajty těla, proto si nezpracované tělo přečtěte ještě před jakýmkoli parsováním JSON: opětovná serializace JSON bajty změní a kontrola selže.
| Formát | Podepsaný řetězec | Klíč | Podpis |
|---|---|---|---|
Standard Webhooks (webhook-signature) | <webhook-id>.<webhook-timestamp>.<body> | Část tajného klíče za whsec_, dekódovaná z base64 | v1, + base64 z HMAC-SHA256 |
Dřívější formát (X-KB-Signature) | <t>.<body> | Celý řetězec tajného klíče jako text | v1= + hex z HMAC-SHA256 |
Stačí ověřit kterýkoli z nich. Dávejte přednost hlavičce Standard Webhooks: stejnou kontrolu nabízí mnoho hotových
knihoven. Během obměny tajného klíče nese každá hlavička jeden podpis pro každý platný tajný klíč (oddělené mezerou
ve webhook-signature, opakované v1= v X-KB-Signature). Doručení je platné, pokud odpovídá kterýkoli z nich.
Odmítněte také časové razítko, které se od vašich hodin liší o více než 5 minut. Tím zabráníte tomu, aby někdo zaznamenané doručení později přehrál znovu.
# 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' ' -f1Otestujte ověření podpisu
Než kód připojíte ke skutečným doručením, spusťte ho s těmito hodnotami. S hodinami nastavenými na časové razítko musí obě kontroly projít a při změně jediného bajtu těla musí selhat.
| Vstup | Hodnota |
|---|---|
| Tajný klíč | whsec_test_secret_0123456789abcdef |
webhook-id | evt_0123456789abcdef0123456789abcdef |
webhook-timestamp a t | 1791194400 |
| Tělo | {"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. Odpovězte rychle, pracujte potom
Sursa čeká na vaši odpověď až 10 sekund. Jakýkoli stavový kód 2xx znamená, že doručení uspělo; cokoli jiného,
včetně přesměrování, vypršení časového limitu nebo uzavřeného spojení, znamená, že selhalo a bude se opakovat.
Příjemce by proto měl jen ověřit podpis, zaznamenat událost a odpovědět. Skutečnou práci udělejte až potom, z vlastní fronty.
// 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 odpovězte až poté, co je událost bezpečně uložená. Pokud odpovíte dřív a pak dojde k pádu, událost je pro vás
ztracená, protože ji Sursa už započítala jako doručenou.
5. Opakování
Neúspěšné doručení se opakuje s rostoucími pauzami: přibližně po 1 minutě, 5 minutách, 30 minutách, 2 hodinách, 5 hodinách, 10 hodinách, 24 hodinách a 24 hodinách – devět pokusů během téměř tří dnů. Každá pauza se liší až o 20 %, aby opakování z mnoha webhooků nedorazila ve stejném okamžiku.
- Odpověď
429nebo503sRetry-Afterposune další pokus nejméně na tuto dobu, nejvýše o 24 hodin. Používejte ji, když je váš systém přetížený nebo probíhá údržba. - Odpověď
410 Gonewebhook vypne: posílejte ji, jen když už na tuto adresu nechcete dostávat žádná doručení. - Po 10 neúspěšných pokusech za sebou se webhook pozastaví a administrátoři organizace dostanou upozornění.
Události se dál hromadí, a když se webhook znovu obnoví, v administračním rozhraní nebo pomocí
POST /v1/webhooks/{webhook_id}/resume, doručí se v pořadí. - Po posledním pokusu se doručení označí jako
exhausted. Můžete ho odeslat znovu ručně.
Doručení webhooku s každým pokusem, vaším kódem odpovědi a jejím časováním vypisuje
GET /v1/webhooks/{webhook_id}/deliveries i administrační rozhraní.
POST /v1/webhooks/{webhook_id}/deliveries/{delivery_id}/redeliver odešle
jedno doručení znovu. Opakované doručení má stejné webhook-id.
6. Pořadí a duplicity
- Pořadí. Doručení na jeden webhook jdou přísně jedno za druhým v pořadí podle
sequence. Dokud se jedno doručení opakuje, pozdější čekají. Různé webhooky na sebe nečekají. - Duplicity. Stejná událost může dorazit víckrát, například když se vaše odpověď cestou zpět ztratila. Ukládejte
webhook-id(v těle je to takéid) každé zpracované události a událost, která už jednou přišla, přeskočte. - Myslete ve stavech. U změn téhož objektu porovnejte
created_atnebosequences tím, co máte uloženo, a událost starší než stav, který už máte, ignorujte. V případě pochybností si aktuální objekt přečtěte přes API: událost vám řekne, že se něco změnilo, API vám řekne, jak to vypadá teď.
7. Sesouhlaste data s kanálem událostí
Každá událost se 30 dní uchovává také v kanálu událostí bez ohledu na to, zda ji nějaký webhook odebíral. Pokud
byl váš příjemce mimo provoz déle, než pokryjí opakování, nebo chcete ověřit, že nic nechybí, přečtěte kanál
klíčem s rolí admin a rozsahem 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"Událost v kanálu má stejné id a tvar jako v doručení, jen bez polí doručení sequence, payload a test. Protože
váš příjemce přeskakuje události, které už viděl, můžete sesouhlasení spouštět, jak často chcete, například jednou
za hodinu.
GET /v1/webhook-events/{event_id} přečte jednu událost podle jejího ID.
8. Obměňte tajný klíč
POST /v1/webhooks/{webhook_id}/rotate-secret vydá nový tajný klíč
a předchozí ponechá v platnosti po dobu překryvu, ve výchozím nastavení 24 hodin, nejvýše 7 dní. Během překryvu se
každé doručení podepisuje oběma tajnými klíči.
- Proveďte obměnu a nový tajný klíč uložte u svého příjemce vedle starého.
- Přijměte doručení, pokud se ověří kterýmkoli z obou tajných klíčů.
- Po skončení překryvu starý tajný klíč odstraňte.
Pokud starý tajný klíč unikl, ukončete překryv okamžitě pomocí
POST /v1/webhooks/{webhook_id}/revoke-previous-secret, nebo
tajný klíč obměňte s "overlap_hours": 0.
Kontrolní seznam
- Ověřte jeden z podpisů na nezpracovaném těle a zkontrolujte časové razítko.
- Uložte událost s klíčem
webhook-ida pak do 10 sekund odpovězte2xx. - Přeskakujte duplicity a zastaralé události; kde na tom záleží, přečtěte si aktuální objekt přes API.
- Když potřebujete, aby Sursa zpomalila, odpovězte
429nebo503sRetry-After; nikdy neodpovídejte410omylem. - Přeskakujte typy událostí a pole, které neznáte.
- Pravidelně sesouhlasujte data s kanálem událostí.