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}
| Header | Betydning |
|---|---|
webhook-id | Hændelsens id. Det samme ved hvert nyt forsøg. Brug det til at kassere dubletter. |
webhook-timestamp | Hvornår dette forsøg blev signeret, i Unix-sekunder. |
webhook-signature | Signaturen i formatet Standard Webhooks. |
X-KB-Signature | Den samme levering signeret i Sursas tidligere format. |
X-KB-Event | Hændelsestypen. |
X-KB-Attempt | Forsøgets nummer, fra 1. |
X-KB-Sequence | Denne 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.
| Format | Signeret streng | Nøgle | Signatur |
|---|---|---|---|
Standard Webhooks (webhook-signature) | <webhook-id>.<webhook-timestamp>.<body> | Den del af hemmeligheden, der står efter whsec_, base64-dekodet | v1, + base64 af HMAC-SHA256 |
Tidligere format (X-KB-Signature) | <t>.<body> | Hele hemmelighedens streng som tekst | v1= + 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' ' -f1Test 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.
| Inddata | Værdi |
|---|---|
| Hemmelighed | whsec_test_secret_0123456789abcdef |
webhook-id | evt_0123456789abcdef0123456789abcdef |
webhook-timestamp og t | 1791194400 |
| Brødtekst | {"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. 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- eller503-svar medRetry-Afterflytter 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åidi 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_atellersequencemed 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.
- Rotér, og gem den nye hemmelighed ved siden af den gamle i din modtager.
- Acceptér en levering, hvis den kan valideres med en af hemmelighederne.
- 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-idsom nøgle, og svar derefter2xxinden for 10 sekunder. - Spring dubletter og uaktuelle hændelser over; læs det aktuelle objekt via API’et, når det har betydning.
- Svar
429eller503medRetry-After, når Sursa skal holde igen; svar aldrig410ved et uheld. - Spring hændelsestyper og felter over, som du ikke kender.
- Afstem med hændelsesfeedet efter en fast plan.