Direct naar de inhoud
Sursa Developers

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}
HeaderBetekenis
webhook-idDe ID van het event. Hetzelfde bij elke nieuwe poging. Gebruik hem om duplicaten te filteren.
webhook-timestampWanneer deze poging is ondertekend, in Unix-seconden.
webhook-signatureDe handtekening in het formaat van Standard Webhooks.
X-KB-SignatureDezelfde levering, ondertekend in het eerdere formaat van Sursa.
X-KB-EventHet eventtype.
X-KB-AttemptHet nummer van de poging, vanaf 1.
X-KB-SequenceDe 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.

FormaatOndertekende stringSleutelHandtekening
Standard Webhooks (webhook-signature)<webhook-id>.<webhook-timestamp>.<body>Het deel van het geheim na whsec_, gedecodeerd uit base64v1, + base64 van HMAC-SHA256
Eerder formaat (X-KB-Signature)<t>.<body>De hele string van het geheim, als tekstv1= + 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' ' -f1

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

InvoerWaarde
Geheimwhsec_test_secret_0123456789abcdef
webhook-idevt_0123456789abcdef0123456789abcdef
webhook-timestamp en t1791194400
Body{"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. 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 429 of 503 met Retry-After schuift 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 Gone schakelt 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 ook id is 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_at of sequence met 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.

  1. Roteer en bewaar het nieuwe geheim naast het oude in je ontvanger.
  2. Accepteer een levering als ze met een van beide geheimen klopt.
  3. 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-id als sleutel en antwoord daarna binnen 10 seconden met 2xx.
  • Sla duplicaten en verouderde events over; lees het huidige object via de API als het ertoe doet.
  • Antwoord met 429 of 503 en Retry-After als Sursa moet afremmen; antwoord nooit per ongeluk met 410.
  • Sla eventtypes en velden over die je niet kent.
  • Stem volgens een schema af met de eventfeed.