Hopp til innholdet
Sursa Developers

Motta webhooks

En webhook forteller systemet ditt om en endring i Sursa idet den skjer: Et kort publiseres, verifiseres eller arkiveres, et spørsmål ble stående ubesvart. Sursa sender en HTTPS-POST med hendelsen til en adresse du registrerer. Denne veiledningen bygger en mottaker som fungerer riktig ved nye forsøk, endret rekkefølge og duplikater.

Hva hver hendelse betyr og inneholder, finner du i oversikten over webhooks og i hendelseskatalogen i API-referansen.

1. Registrer et endepunkt

En administrator registrerer endepunkter i Sursa under Administration → Integrations → Webhooks and API (administrasjon, integrasjoner, webhooks og API) eller med POST /v1/webhooks og en nøkkel med rollen admin og scopet 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"]
  }'

Svaret inneholder signeringshemmeligheten i secret, som begynner med whsec_. Den vises én gang. Lagre den sammen med mottakeren din, for eksempel i miljøvariabelen SURSA_WEBHOOK_SECRET.

Adressen må bruke https og peke til offentlige IP-adresser: Adresser i private nettverk, loopback-nettverk og andre reserverte nettverk avvises ved registreringen og ved hver leveranse. For å utvikle på din egen datamaskin gjør du den lokale mottakeren tilgjengelig gjennom en tunneltjeneste som gir den en offentlig HTTPS-adresse.

Send en testhendelse når som helst med POST /v1/webhooks/{webhook_id}/test. Svaret viser hva mottakeren din svarte.

2. Slik ser en leveranse ut

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}
HeaderBetydning
webhook-idID-en til hendelsen. Den samme ved hvert nytt forsøk. Bruk den til å forkaste duplikater.
webhook-timestampNår dette forsøket ble signert, i Unix-sekunder.
webhook-signatureSignaturen i formatet Standard Webhooks.
X-KB-SignatureDen samme leveransen signert i Sursas tidligere format.
X-KB-EventHendelsestypen.
X-KB-AttemptNummeret på forsøket, fra 1.
X-KB-SequencePlasseringen til denne leveransen i endepunktets rekkefølge.

Navn på headere skiller ikke mellom store og små bokstaver; rammeverket ditt kan vise dem som X-Kb-Signature eller x-kb-signature.

Brødteksten er hendelsen. Innholdet i data avhenger av typen. Et endepunkt som er satt til nyttelasten thin, mottar hendelsen uten data; les i stedet objektet gjennom API-et med din egen nøkkel.

3. Valider signaturen

Kontroller signaturen på hver leveranse før du stoler på den. Begge formatene signerer de nøyaktige bytene i brødteksten, så les den rå brødteksten før noen JSON-tolking: Hvis du serialiserer JSON-en på nytt, endres bytene, og kontrollen feiler.

FormatSignert strengNøkkelSignatur
Standard Webhooks (webhook-signature)<webhook-id>.<webhook-timestamp>.<body>Delen av hemmeligheten etter whsec_, base64-dekodetv1, + base64 av HMAC-SHA256
Tidligere format (X-KB-Signature)<t>.<body>Hele hemmeligheten som tekstv1= + hex av HMAC-SHA256

Det er nok å validere én av dem. Foretrekk headeren for Standard Webhooks: Den samme kontrollen finnes i mange ferdige biblioteker. Under en rotasjon av hemmeligheten har hver header én signatur per gyldig hemmelighet (atskilt med mellomrom i webhook-signature, gjentatt v1= i X-KB-Signature). En leveranse er gyldig hvis én av dem stemmer.

Avvis også et tidsstempel som avviker mer enn 5 minutter fra klokken din. Det hindrer at noen spiller av en registrert leveranse på nytt senere.

# 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

Test signaturvalideringen din

Kjør koden din mot disse verdiene før du kobler den til ekte leveranser. Med klokken satt til tidsstempelet må begge kontrollene lykkes, og de må feile når én byte i brødteksten endres.

InndataVerdi
Hemmelighetwhsec_test_secret_0123456789abcdef
webhook-idevt_0123456789abcdef0123456789abcdef
webhook-timestamp og t1791194400
Brødtekst{"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. Svar raskt, jobb etterpå

Sursa venter opptil 10 sekunder på svaret ditt. Enhver 2xx-status betyr at leveransen lyktes; alt annet, også en omdirigering, et tidsavbrudd eller en lukket tilkobling, betyr at den mislyktes og vil bli prøvd på nytt.

Mottakeren skal derfor bare validere, registrere hendelsen og svare. Gjør det egentlige arbeidet etterpå, fra din egen kø.

// 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);

Svar 2xx først når hendelsen er trygt lagret. Hvis du svarer først og så krasjer, er hendelsen tapt for deg, fordi Sursa allerede har regnet den som levert.

5. Nye forsøk

En mislykket leveranse prøves på nytt med stadig lengre pauser: etter omtrent 1 minutt, 5 minutter, 30 minutter, 2 timer, 5 timer, 10 timer, 24 timer og 24 timer, ni forsøk over nesten tre døgn. Hver pause varierer med opptil 20 %, slik at nye forsøk fra mange endepunkter ikke kommer i samme øyeblikk.

  • Et 429- eller 503-svar med Retry-After flytter neste forsøk til minst det tidspunktet, høyst 24 timer senere. Bruk det når systemet ditt er overbelastet eller under vedlikehold.
  • Et 410 Gone-svar deaktiverer endepunktet: Send det bare når du ikke vil ha flere leveranser til den adressen.
  • Etter 10 mislykkede forsøk på rad settes endepunktet på pause, og organisasjonens administratorer får beskjed. Hendelsene fortsetter å samle seg opp, og når endepunktet gjenopptas, i administrasjonsgrensesnittet eller med POST /v1/webhooks/{webhook_id}/resume, leveres de i rekkefølge.
  • Etter det siste forsøket merkes en leveranse som exhausted. Du kan sende den på nytt manuelt.

Leveransene til et endepunkt, med hvert forsøk, svarkoden din og tidsbruken, vises av GET /v1/webhooks/{webhook_id}/deliveries og i administrasjonsgrensesnittet. POST /v1/webhooks/{webhook_id}/deliveries/{delivery_id}/redeliver sender én av dem på nytt. En ny leveranse har den samme webhook-id.

6. Rekkefølge og duplikater

  • Rekkefølge. Leveranser til samme endepunkt går strengt etter hverandre i rekkefølgen til sequence. Mens én leveranse prøves på nytt, venter de senere. Ulike endepunkter venter ikke på hverandre.
  • Duplikater. Den samme hendelsen kan komme mer enn én gang, for eksempel når svaret ditt gikk tapt på veien tilbake. Lagre webhook-id (det er også id i brødteksten) for hver hendelse du behandler, og hopp over en du allerede har sett.
  • Tenk i tilstander. For endringer i samme objekt sammenligner du created_at eller sequence med det du har lagret, og ignorerer en hendelse som er eldre enn tilstanden du allerede har. Er du i tvil, leser du det nåværende objektet gjennom API-et: Hendelsen forteller deg at noe er endret, API-et forteller deg hvordan det er nå.

7. Avstem med hendelsesfeeden

Hver hendelse lagres også i 30 dager i hendelsesfeeden, uansett om et endepunkt abonnerte på den eller ikke. Hvis mottakeren din var nede lenger enn de nye forsøkene dekker, eller du vil kontrollere at ingenting gikk tapt, leser du feeden med en nøkkel som har rollen admin og scopet 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"

En hendelse i feeden har samme id og form som i en leveranse, uten leveringsfeltene sequence, payload og test. Fordi mottakeren din hopper over hendelser den allerede har sett, kan du kjøre avstemmingen så ofte du vil, for eksempel én gang i timen.

GET /v1/webhook-events/{event_id} leser én hendelse etter ID-en.

8. Roter hemmeligheten

POST /v1/webhooks/{webhook_id}/rotate-secret utsteder en ny hemmelighet og lar den forrige være gyldig i en overlappsperiode, 24 timer som standard og opptil 7 dager. Under overlappet signeres hver leveranse med begge hemmelighetene.

  1. Roter, og lagre den nye hemmeligheten ved siden av den gamle i mottakeren din.
  2. Godta en leveranse hvis den kan valideres med en av hemmelighetene.
  3. Fjern den gamle hemmeligheten etter overlappet.

Hvis den gamle hemmeligheten har lekket, avslutter du overlappet med en gang med POST /v1/webhooks/{webhook_id}/revoke-previous-secret, eller du roterer med "overlap_hours": 0.

Sjekkliste

  • Valider én av signaturene på den rå brødteksten, og kontroller tidsstempelet.
  • Lagre hendelsen med webhook-id som nøkkel, og svar deretter 2xx innen 10 sekunder.
  • Hopp over duplikater og utdaterte hendelser; les det nåværende objektet gjennom API-et når det har betydning.
  • Svar 429 eller 503 med Retry-After når Sursa skal holde igjen; svar aldri 410 ved et uhell.
  • Hopp over hendelsestyper og felt du ikke kjenner.
  • Avstem med hendelsesfeeden etter en fast plan.