Hoppa till innehållet
Sursa Developers

Ta emot webhookar

En webhook berättar för ditt system om en ändring i Sursa när den sker: ett kort publiceras, verifieras eller arkiveras, en fråga förblev obesvarad. Sursa skickar en HTTPS-POST med händelsen till en adress som du registrerar. Den här guiden bygger en mottagare som fungerar korrekt trots nya försök, ändrad ordning och dubbletter.

Vad varje händelse betyder och innehåller står i översikten över webhookar och i händelsekatalogen i API-referensen.

1. Registrera en webhook

En administratör registrerar webhookar i Sursa under Administration → Integrations → Webhooks and API, eller med POST /v1/webhooks och en nyckel med rollen admin och omfånget 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 innehåller signeringshemligheten i secret, som börjar med whsec_. Den visas en gång. Förvara den hos din mottagare, till exempel i en miljövariabel SURSA_WEBHOOK_SECRET.

Adressen måste använda https och peka på offentliga IP-adresser: adresser i privata nätverk, loopback-nätverk och andra reserverade nätverk avvisas både när du registrerar och vid varje leverans. För att utveckla på din egen dator exponerar du din lokala mottagare via en tunneltjänst som ger den en offentlig HTTPS-adress.

Skicka en testhändelse när som helst med POST /v1/webhooks/{webhook_id}/test. Svaret visar vad din mottagare svarade.

2. Hur en leverans ser 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}
HuvudBetydelse
webhook-idHändelsens id. Detsamma vid varje nytt försök. Använd det för att slänga dubbletter.
webhook-timestampNär det här försöket signerades, i Unix-sekunder.
webhook-signatureSignaturen i formatet Standard Webhooks.
X-KB-SignatureSamma leverans signerad i Sursas tidigare format.
X-KB-EventHändelsetypen.
X-KB-AttemptFörsökets nummer, från 1.
X-KB-SequenceDen här leveransens position i webhookens ordning.

Huvudenas namn är skiftlägesokänsliga; ditt ramverk kan visa dem som X-Kb-Signature eller x-kb-signature.

Brödtexten är händelsen. Dess data beror på typen. En webhook med nyttolastläget thin tar emot händelsen utan data; läs i stället objektet via API:et med din egen nyckel.

3. Verifiera signaturen

Kontrollera signaturen för varje leverans innan du litar på den. Båda formaten signerar brödtextens exakta byte, så läs den råa brödtexten före all JSON-tolkning: om du serialiserar om JSON-koden ändras byten och kontrollen misslyckas.

FormatSignerad strängNyckelSignatur
Standard Webhooks (webhook-signature)<webhook-id>.<webhook-timestamp>.<body>Delen av hemligheten efter whsec_, base64-avkodadv1, + base64 av HMAC-SHA256
Tidigare format (X-KB-Signature)<t>.<body>Hela hemlighetens sträng, som textv1= + hex av HMAC-SHA256

Det räcker att verifiera en av dem. Använd hellre huvudet för Standard Webhooks: samma kontroll finns i många färdiga bibliotek. Under en rotation av hemligheten bär varje huvud en signatur per giltig hemlighet (åtskilda med mellanslag i webhook-signature, upprepat v1= i X-KB-Signature). En leverans är giltig om någon av dem stämmer.

Avvisa också en tidsstämpel som avviker mer än 5 minuter från din klocka. Det hindrar någon från att spela upp en inspelad leverans igen senare.

# 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

Testa din verifierare

Kör din kod mot de här värdena innan du kopplar den till riktiga leveranser. Med klockan satt till tidsstämpeln måste båda kontrollerna godkännas, och de måste misslyckas när en byte i brödtexten ändras.

IndataVärde
Hemlighetwhsec_test_secret_0123456789abcdef
webhook-idevt_0123456789abcdef0123456789abcdef
webhook-timestamp och t1791194400
Brödtext{"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. Svara snabbt, arbeta senare

Sursa väntar upp till 10 sekunder på ditt svar. Varje 2xx-status betyder att leveransen lyckades; allt annat, även en omdirigering, en tidsgräns som löper ut eller en stängd anslutning, betyder att den misslyckades och kommer att göras om.

Mottagaren ska alltså bara verifiera, registrera händelsen och svara. Gör det egentliga arbetet efteråt, från 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);

Svara 2xx först när händelsen är säkert lagrad. Om du svarar först och sedan kraschar går händelsen förlorad för dig, eftersom Sursa redan har räknat den som levererad.

5. Nya försök

En misslyckad leverans görs om med allt längre pauser: efter ungefär 1 minut, 5 minuter, 30 minuter, 2 timmar, 5 timmar, 10 timmar, 24 timmar och 24 timmar, nio försök under nästan tre dygn. Varje paus varierar med upp till 20 % så att nya försök från många webhookar inte kommer samtidigt.

  • Ett svar 429 eller 503 med Retry-After flyttar nästa försök till minst den tiden, upp till 24 timmar senare. Använd det när ditt system är överbelastat eller under underhåll.
  • Ett svar 410 Gone inaktiverar webhooken: skicka det bara när du inte vill ha fler leveranser till den adressen.
  • Efter 10 misslyckade försök i rad pausas webhooken, och organisationens administratörer får besked. Händelserna fortsätter att samlas, och när webhooken återupptas, i administrationsgränssnittet eller med POST /v1/webhooks/{webhook_id}/resume, levereras de i ordning.
  • Efter det sista försöket markeras en leverans som exhausted. Du kan skicka den igen för hand.

En webhooks leveranser, med varje försök, din svarskod och dess tidsåtgång, listas av GET /v1/webhooks/{webhook_id}/deliveries och i administrationsgränssnittet. POST /v1/webhooks/{webhook_id}/deliveries/{delivery_id}/redeliver skickar en leverans igen. En ny leverans har samma webhook-id.

6. Ordning och dubbletter

  • Ordning. Leveranser till en webhook skickas strikt en efter en i sequence-ordning. Medan en leverans görs om väntar de senare. Olika webhookar väntar inte på varandra.
  • Dubbletter. Samma händelse kan komma mer än en gång, till exempel när ditt svar gick förlorat på vägen tillbaka. Spara webhook-id (det är också id i brödtexten) för varje händelse som du bearbetar och hoppa över en som du redan har sett.
  • Tänk i tillstånd. Vid ändringar av samma objekt jämför du created_at eller sequence med det som du har lagrat och ignorerar en händelse som är äldre än det tillstånd som du redan har. Är du osäker läser du det aktuella objektet via API:et: händelsen berättar att något har ändrats, API:et berättar hur det ser ut nu.

7. Stäm av mot händelseflödet

Varje händelse sparas också i 30 dagar i händelseflödet, oavsett om någon webhook prenumererade på den eller inte. Om din mottagare var nere längre än de nya försöken täcker, eller om du vill kontrollera att inget har missats, läser du flödet med en nyckel som har rollen admin och omfånget 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 flödet har samma id och form som i en leverans, utan leveransfälten sequence, payload och test. Eftersom din mottagare hoppar över händelser som den redan har sett kan du köra avstämningen så ofta du vill, till exempel en gång i timmen.

GET /v1/webhook-events/{event_id} läser en händelse utifrån dess id.

8. Rotera hemligheten

POST /v1/webhooks/{webhook_id}/rotate-secret utfärdar en ny hemlighet och låter den föregående gälla under en överlappningsperiod, 24 timmar som standard och upp till 7 dagar. Under överlappningen signeras varje leverans med båda hemligheterna.

  1. Rotera och spara den nya hemligheten bredvid den gamla i din mottagare.
  2. Godta en leverans om den kan verifieras med någon av hemligheterna.
  3. Ta bort den gamla hemligheten efter överlappningen.

Om den gamla hemligheten har läckt ut avslutar du överlappningen genast med POST /v1/webhooks/{webhook_id}/revoke-previous-secret, eller roterar med "overlap_hours": 0.

Checklista

  • Verifiera en av signaturerna på den råa brödtexten och kontrollera tidsstämpeln.
  • Lagra händelsen med webhook-id som nyckel och svara sedan 2xx inom 10 sekunder.
  • Hoppa över dubbletter och inaktuella händelser; läs det aktuella objektet via API:et när det spelar roll.
  • Svara 429 eller 503 med Retry-After när du behöver att Sursa backar; svara aldrig 410 av misstag.
  • Hoppa över händelsetyper och fält som du inte känner till.
  • Stäm av mot händelseflödet enligt ett schema.