Webhooks ontvangen
Een webhook meldt je systeem een wijziging in Sursa op het moment dat die gebeurt: een kaart is gepubliceerd,
geverifieerd of gearchiveerd, een vraag bleef onbeantwoord. Sursa stuurt een HTTPS-POST met het event naar een
adres dat je registreert. In deze handleiding bouw je een ontvanger die correct blijft bij nieuwe pogingen, een
gewijzigde volgorde en duplicaten.
Wat elk event betekent en bevat, lees je in het overzicht van webhooks en in de eventcatalogus in de API-referentie.
1. Webhook registreren
Een beheerder registreert webhooks in Sursa onder Administration → Integrations → Webhooks and API (beheer →
integraties → webhooks en API), of met POST /v1/webhooks en een sleutel met de
rol admin en de scope 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"]
}'De response bevat het ondertekeningsgeheim in secret, beginnend met whsec_. Het wordt één keer getoond. Bewaar
het bij je ontvanger, bijvoorbeeld in een omgevingsvariabele SURSA_WEBHOOK_SECRET.
Het ontvangstadres moet https gebruiken en naar openbare IP-adressen verwijzen: adressen in privénetwerken,
loopbacknetwerken en andere gereserveerde netwerken worden geweigerd bij het registreren en bij elke levering. Om op je
eigen computer te ontwikkelen, maak je je lokale ontvanger bereikbaar via een tunneldienst die hem een openbaar
HTTPS-adres geeft.
Verstuur op elk moment een testevent met POST /v1/webhooks/{webhook_id}/test.
De response laat zien wat je ontvanger heeft geantwoord.
2. Hoe een levering eruitziet
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}
| Header | Betekenis |
|---|---|
webhook-id | De ID van het event. Hetzelfde bij elke nieuwe poging. Gebruik hem om duplicaten te filteren. |
webhook-timestamp | Wanneer deze poging is ondertekend, in Unix-seconden. |
webhook-signature | De handtekening in het formaat van Standard Webhooks. |
X-KB-Signature | Dezelfde levering, ondertekend in het eerdere formaat van Sursa. |
X-KB-Event | Het eventtype. |
X-KB-Attempt | Het nummer van de poging, vanaf 1. |
X-KB-Sequence | De positie van deze levering in de volgorde van de webhook. |
Namen van headers zijn niet hoofdlettergevoelig; je framework toont ze misschien als X-Kb-Signature of
x-kb-signature.
De body is het event. De data ervan hangt af van het type. Een webhook die is ingesteld op de payload thin,
ontvangt het event zonder data; lees het object in dat geval met je eigen sleutel via de API.
3. Handtekening controleren
Controleer de handtekening van elke levering voordat je haar vertrouwt. Beide formaten ondertekenen de exacte bytes van de body, dus lees de ruwe body vóór het parseren van de JSON: de JSON opnieuw serialiseren verandert de bytes en breekt de controle.
| Formaat | Ondertekende string | Sleutel | Handtekening |
|---|---|---|---|
Standard Webhooks (webhook-signature) | <webhook-id>.<webhook-timestamp>.<body> | Het deel van het geheim na whsec_, gedecodeerd uit base64 | v1, + base64 van HMAC-SHA256 |
Eerder formaat (X-KB-Signature) | <t>.<body> | De hele string van het geheim, als tekst | v1= + hex van HMAC-SHA256 |
Het is genoeg om een van beide te controleren. Gebruik bij voorkeur de header van Standard Webhooks: dezelfde controle
zit in veel kant-en-klare bibliotheken. Tijdens een rotatie van het geheim bevat elke header één handtekening per
geldig geheim (gescheiden door spaties in webhook-signature, herhaalde v1= in X-KB-Signature). Een levering is
geldig als een van de handtekeningen overeenkomt.
Weiger ook een tijdstempel die meer dan 5 minuten van je eigen klok afwijkt. Zo kan niemand een opgenomen levering later opnieuw afspelen.
# 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' ' -f1Je controle testen
Laat je code op deze waarden los voordat je hem op echte leveringen aansluit. Met de klok op het tijdstempel moeten beide controles slagen, en ze moeten mislukken als er één byte van de body verandert.
| Invoer | Waarde |
|---|---|
| Geheim | whsec_test_secret_0123456789abcdef |
webhook-id | evt_0123456789abcdef0123456789abcdef |
webhook-timestamp en t | 1791194400 |
| Body | {"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. Snel antwoorden, later werken
Sursa wacht maximaal 10 seconden op je antwoord. Elke 2xx-status betekent dat de levering is geslaagd; al het
andere, ook een doorverwijzing, een time-out of een gesloten verbinding, betekent dat ze is mislukt en opnieuw wordt
geprobeerd.
De ontvanger moet dus alleen controleren, het event vastleggen en antwoorden. Doe het echte werk daarna, vanuit je eigen wachtrij.
// 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);Antwoord pas met 2xx als het event veilig is opgeslagen. Antwoord je eerst en crasht je ontvanger daarna, dan ben je
het event kwijt, want Sursa heeft het al als geleverd geteld.
5. Nieuwe pogingen
Een mislukte levering wordt met steeds langere pauzes opnieuw geprobeerd: na ongeveer 1 minuut, 5 minuten, 30 minuten, 2 uur, 5 uur, 10 uur, 24 uur en 24 uur – negen pogingen in bijna drie dagen. Elke pauze varieert met maximaal 20%, zodat nieuwe pogingen van veel webhooks niet op hetzelfde moment binnenkomen.
- Een antwoord
429of503metRetry-Afterschuift de volgende poging op tot minstens dat tijdstip, maximaal 24 uur later. Gebruik dit als je systeem overbelast is of in onderhoud. - Een antwoord
410 Goneschakelt de webhook uit: stuur dat alleen als je op dat adres geen leveringen meer wilt. - Na 10 mislukte pogingen op rij wordt de webhook gepauzeerd en krijgen de beheerders van de organisatie een
melding. Events blijven zich opstapelen, en als de webhook wordt hervat, in de beheeromgeving of met
POST /v1/webhooks/{webhook_id}/resume, worden ze op volgorde geleverd. - Na de laatste poging wordt een levering gemarkeerd als
exhausted. Je kunt haar met de hand opnieuw versturen.
De leveringen van een webhook, met elke poging, je antwoordcode en de timing ervan, staan in
GET /v1/webhooks/{webhook_id}/deliveries en in de beheeromgeving.
POST /v1/webhooks/{webhook_id}/deliveries/{delivery_id}/redeliver verstuurt
er een opnieuw. Een nieuwe levering heeft dezelfde webhook-id.
6. Volgorde en duplicaten
- Volgorde. Leveringen aan één webhook gaan strikt na elkaar, in de volgorde van
sequence. Zolang een levering opnieuw wordt geprobeerd, wachten de latere. Verschillende webhooks wachten niet op elkaar. - Duplicaten. Hetzelfde event kan meer dan eens aankomen, bijvoorbeeld als je antwoord onderweg verloren ging.
Sla de
webhook-id(die ookidis in de body) op van elk event dat je verwerkt, en sla een event over dat je al hebt gezien. - Denken in toestanden. Vergelijk bij wijzigingen van hetzelfde object
created_atofsequencemet wat je hebt opgeslagen, en negeer een event dat ouder is dan de toestand die je al hebt. Lees bij twijfel het huidige object via de API: het event vertelt je dat er iets is veranderd, de API vertelt je hoe het nu is.
7. Afstemmen met de eventfeed
Elk event wordt ook 30 dagen bewaard in de eventfeed, of er nu een webhook op geabonneerd was of niet. Lag je
ontvanger langer plat dan de nieuwe pogingen dekken, of wil je controleren dat er niets is gemist, lees dan de feed
met een sleutel die de rol admin en de scope webhooks:manage heeft:
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"Een event in de feed heeft dezelfde id en vorm als in een levering, zonder de leveringsvelden sequence, payload
en test. Omdat je ontvanger events overslaat die hij al heeft gezien, kun je de afstemming zo vaak uitvoeren als je
wilt, bijvoorbeeld eens per uur.
GET /v1/webhook-events/{event_id} leest één event op zijn ID.
8. Geheim roteren
POST /v1/webhooks/{webhook_id}/rotate-secret geeft een nieuw geheim uit
en houdt het vorige geldig gedurende een overlapperiode, standaard 24 uur en maximaal 7 dagen. Tijdens de overlap wordt
elke levering met beide geheimen ondertekend.
- Roteer en bewaar het nieuwe geheim naast het oude in je ontvanger.
- Accepteer een levering als ze met een van beide geheimen klopt.
- Verwijder het oude geheim na de overlap.
Is het oude geheim uitgelekt, beëindig de overlap dan meteen met
POST /v1/webhooks/{webhook_id}/revoke-previous-secret, of
roteer met "overlap_hours": 0.
Checklist
- Controleer een van de handtekeningen op de ruwe body en controleer het tijdstempel.
- Sla het event op met
webhook-idals sleutel en antwoord daarna binnen 10 seconden met2xx. - Sla duplicaten en verouderde events over; lees het huidige object via de API als het ertoe doet.
- Antwoord met
429of503enRetry-Afterals Sursa moet afremmen; antwoord nooit per ongeluk met410. - Sla eventtypes en velden over die je niet kent.
- Stem volgens een schema af met de eventfeed.