Přejít na obsah
Sursa Developers

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čkaVýznam
webhook-idID události. Stejné při každém opakování. Používejte ho k zahazování duplicit.
webhook-timestampKdy byl tento pokus podepsán, v unixových sekundách.
webhook-signaturePodpis ve formátu Standard Webhooks.
X-KB-SignatureTotéž doručení podepsané v dřívějším formátu Sursy.
X-KB-EventTyp události.
X-KB-AttemptČíslo pokusu, od 1.
X-KB-SequencePozice 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átPodepsaný řetězecKlíčPodpis
Standard Webhooks (webhook-signature)<webhook-id>.<webhook-timestamp>.<body>Část tajného klíče za whsec_, dekódovaná z base64v1, + base64 z HMAC-SHA256
Dřívější formát (X-KB-Signature)<t>.<body>Celý řetězec tajného klíče jako textv1= + 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' ' -f1

Otestujte 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.

VstupHodnota
Tajný klíčwhsec_test_secret_0123456789abcdef
webhook-idevt_0123456789abcdef0123456789abcdef
webhook-timestamp a t1791194400
Tělo{"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. 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ěď 429 nebo 503 s Retry-After posune 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 Gone webhook 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_at nebo sequence s 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.

  1. Proveďte obměnu a nový tajný klíč uložte u svého příjemce vedle starého.
  2. Přijměte doručení, pokud se ověří kterýmkoli z obou tajných klíčů.
  3. 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-id a pak do 10 sekund odpovězte 2xx.
  • 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 429 nebo 503 s Retry-After; nikdy neodpovídejte 410 omylem.
  • Přeskakujte typy událostí a pole, které neznáte.
  • Pravidelně sesouhlasujte data s kanálem událostí.