Gå til indholdet
Sursa Developers

Modtag webhooks

En webhook fortæller dit system om en ændring i Sursa, når den sker: Et kort publiceres, verificeres eller arkiveres, et spørgsmål forblev ubesvaret. Sursa sender en HTTPS-POST med hændelsen til en adresse, som du registrerer. I denne vejledning bygger du en modtager, der fungerer korrekt ved nye forsøg, ændret rækkefølge og dubletter.

Hvad hver hændelse betyder og indeholder, kan du læse i oversigten over webhooks og i hændelseskataloget i API-referencen.

1. Registrér et slutpunkt

En administrator registrerer slutpunkter i Sursa under Administration → Integrations → Webhooks and API (administration, integrationer, webhooks og API) eller med POST /v1/webhooks og en nøgle 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 indeholder signeringshemmeligheden i secret, der begynder med whsec_. Den vises én gang. Gem den sammen med din modtager, fx i en miljøvariabel SURSA_WEBHOOK_SECRET.

Adressen skal bruge https og pege på offentlige IP-adresser: Adresser i private netværk, loopback-netværk og andre reserverede netværk afvises ved registreringen og ved hver levering. Vil du udvikle på din egen computer, så gør din lokale modtager tilgængelig via en tunneltjeneste, der giver den en offentlig HTTPS-adresse.

Send en testhændelse når som helst med POST /v1/webhooks/{webhook_id}/test. Svaret viser, hvad din modtager svarede.

2. Sådan ser en levering ud

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-idHændelsens id. Det samme ved hvert nyt forsøg. Brug det til at kassere dubletter.
webhook-timestampHvornår dette forsøg blev signeret, i Unix-sekunder.
webhook-signatureSignaturen i formatet Standard Webhooks.
X-KB-SignatureDen samme levering signeret i Sursas tidligere format.
X-KB-EventHændelsestypen.
X-KB-AttemptForsøgets nummer, fra 1.
X-KB-SequenceDenne leverings position i slutpunktets rækkefølge.

Headernavne skelner ikke mellem store og små bogstaver; dit framework viser dem måske som X-Kb-Signature eller x-kb-signature.

Brødteksten er hændelsen. Dens data afhænger af typen. Et slutpunkt, der er sat til payload-tilstanden thin, modtager hændelsen uden data; læs i stedet objektet via API’et med din egen nøgle.

3. Validér signaturen

Kontrollér signaturen på hver levering, før du stoler på den. Begge formater signerer brødtekstens nøjagtige bytes, så læs den rå brødtekst, før du parser JSON: Serialiserer du JSON’en igen, ændres de nøjagtige bytes, og kontrollen fejler.

FormatSigneret strengNøgleSignatur
Standard Webhooks (webhook-signature)<webhook-id>.<webhook-timestamp>.<body>Den del af hemmeligheden, der står efter whsec_, base64-dekodetv1, + base64 af HMAC-SHA256
Tidligere format (X-KB-Signature)<t>.<body>Hele hemmelighedens streng som tekstv1= + hex af HMAC-SHA256

Det er nok at validere den ene af dem. Foretræk headeren fra Standard Webhooks: Den samme kontrol findes i mange færdige biblioteker. Under en rotation af hemmeligheden har hver header én signatur pr. gyldig hemmelighed (adskilt af mellemrum i webhook-signature, gentaget v1= i X-KB-Signature). En levering er gyldig, hvis en af dem passer.

Afvis også et tidsstempel, der afviger mere end 5 minutter fra dit ur. Det forhindrer, at nogen afspiller en optaget levering igen 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 din signaturvalidering

Kør din kode mod disse værdier, før du forbinder den med rigtige leveringer. Med uret sat til tidsstemplet skal begge kontroller bestå, og de skal fejle, når én byte i brødteksten ændres.

InddataVærdi
Hemmelighedwhsec_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 hurtigt, arbejd bagefter

Sursa venter op til 10 sekunder på dit svar. Enhver 2xx-status betyder, at leveringen lykkedes; alt andet, også en omdirigering, en timeout eller en lukket forbindelse, betyder, at den mislykkedes og vil blive forsøgt igen.

Modtageren skal altså kun validere, registrere hændelsen og svare. Udfør det egentlige arbejde bagefter 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 først 2xx, når hændelsen er gemt sikkert. Svarer du først og går ned bagefter, er hændelsen tabt for dig, fordi Sursa allerede har regnet den som leveret.

5. Nye forsøg

En mislykket levering forsøges igen med stadig længere pauser: efter ca. 1 minut, 5 minutter, 30 minutter, 2 timer, 5 timer, 10 timer, 24 timer og 24 timer, ni forsøg over næsten tre døgn. Hver pause varierer med op til 20 %, så nye forsøg fra mange slutpunkter ikke kommer på samme tidspunkt.

  • Et 429- eller 503-svar med Retry-After flytter det næste forsøg til mindst det tidspunkt, op til 24 timer senere. Brug det, når dit system er overbelastet eller under vedligeholdelse.
  • Et 410 Gone-svar deaktiverer slutpunktet: Send det kun, når du ikke vil have flere leveringer til den adresse.
  • Efter 10 mislykkede forsøg i træk sættes slutpunktet på pause, og organisationens administratorer får besked. Hændelserne bliver ved med at samle sig, og når slutpunktet genoptages, i administrationsgrænsefladen eller med POST /v1/webhooks/{webhook_id}/resume, leveres de i rækkefølge.
  • Efter det sidste forsøg markeres en levering som exhausted. Du kan sende den igen manuelt.

Et slutpunkts leveringer med hvert forsøg, din svarkode og svartiden vises af GET /v1/webhooks/{webhook_id}/deliveries og i administrationsgrænsefladen. POST /v1/webhooks/{webhook_id}/deliveries/{delivery_id}/redeliver sender en levering igen. En ny levering har det samme webhook-id.

6. Rækkefølge og dubletter

  • Rækkefølge. Leveringer til ét slutpunkt sendes strengt én efter én i rækkefølgen efter sequence. Mens en levering forsøges igen, venter de senere. Forskellige slutpunkter venter ikke på hinanden.
  • Dubletter. Den samme hændelse kan komme mere end én gang, fx når dit svar gik tabt på vejen tilbage. Gem webhook-id (det er også id i brødteksten) for hver hændelse, du behandler, og spring en hændelse over, som du allerede har set.
  • Tænk i tilstande. Ved ændringer af det samme objekt skal du sammenligne created_at eller sequence med det, du har gemt, og ignorere en hændelse, der er ældre end den tilstand, du allerede har. Er du i tvivl, så læs det aktuelle objekt via API’et: Hændelsen fortæller dig, at noget er ændret, API’et fortæller dig, hvordan det er nu.

7. Afstem med hændelsesfeedet

Hver hændelse gemmes også i 30 dage i hændelsesfeedet, uanset om et slutpunkt abonnerede på den. Var din modtager nede i længere tid, end de nye forsøg dækker, eller vil du kontrollere, at intet er gået tabt, så læs feedet med en nøgle, der 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 hændelse i feedet har samme id og form som i en levering, men uden leveringsfelterne sequence, payload og test. Da din modtager springer hændelser over, som den allerede har set, kan du køre afstemningen så ofte, du vil, fx én gang i timen.

GET /v1/webhook-events/{event_id} læser én hændelse ud fra dens id.

8. Rotér hemmeligheden

POST /v1/webhooks/{webhook_id}/rotate-secret udsteder en ny hemmelighed og lader den forrige være gyldig i en overlapsperiode, som standard 24 timer og op til 7 dage. Under overlappet signeres hver levering med begge hemmeligheder.

  1. Rotér, og gem den nye hemmelighed ved siden af den gamle i din modtager.
  2. Acceptér en levering, hvis den kan valideres med en af hemmelighederne.
  3. Fjern den gamle hemmelighed efter overlappet.

Er den gamle hemmelighed sluppet ud, så afslut overlappet med det samme med POST /v1/webhooks/{webhook_id}/revoke-previous-secret, eller rotér med "overlap_hours": 0.

Tjekliste

  • Validér en af signaturerne på den rå brødtekst, og kontrollér tidsstemplet.
  • Gem hændelsen med webhook-id som nøgle, og svar derefter 2xx inden for 10 sekunder.
  • Spring dubletter og uaktuelle hændelser over; læs det aktuelle objekt via API’et, når det har betydning.
  • Svar 429 eller 503 med Retry-After, når Sursa skal holde igen; svar aldrig 410 ved et uheld.
  • Spring hændelsestyper og felter over, som du ikke kender.
  • Afstem med hændelsesfeedet efter en fast plan.