Zum Inhalt springen
Sursa Developers

Webhooks empfangen

Ein Webhook informiert Ihr System über eine Änderung in Sursa, sobald sie geschieht: Eine Karte wird veröffentlicht, geprüft oder archiviert, eine Frage blieb unbeantwortet. Sursa sendet einen HTTPS-POST mit dem Ereignis an eine Adresse, die Sie registrieren. Diese Anleitung baut einen Empfänger, der auch bei Wiederholungen, veränderter Reihenfolge und Duplikaten korrekt arbeitet.

Was jedes Ereignis bedeutet und enthält, finden Sie in der Übersicht über Webhooks und im Ereigniskatalog der API-Referenz.

1. Endpunkt registrieren

Endpunkte registrieren Administratorinnen und Administratoren in Sursa unter Administration → Integrations → Webhooks and API oder mit POST /v1/webhooks und einem Schlüssel mit der Rolle admin und dem 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"]
  }'

Die Antwort enthält das Signatur-Secret in secret, beginnend mit whsec_. Es wird nur einmal angezeigt. Bewahren Sie es bei Ihrem Empfänger auf, zum Beispiel in einer Umgebungsvariablen SURSA_WEBHOOK_SECRET.

Die Adresse muss https verwenden und auf öffentliche IP-Adressen auflösen: Adressen in privaten, Loopback- und anderen reservierten Netzen werden bei der Registrierung und bei jeder Zustellung abgelehnt. Um auf Ihrem eigenen Rechner zu entwickeln, machen Sie Ihren lokalen Empfänger über einen Tunneldienst erreichbar, der ihm eine öffentliche HTTPS-Adresse gibt.

Senden Sie jederzeit ein Testereignis mit POST /v1/webhooks/{webhook_id}/test. Die Antwort zeigt, was Ihr Empfänger geantwortet hat.

2. So sieht eine Zustellung aus

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}
HeaderBedeutung
webhook-idDie ID des Ereignisses. Bei jeder Wiederholung gleich. Verwenden Sie sie, um Duplikate zu verwerfen.
webhook-timestampWann dieser Versuch signiert wurde, in Unix-Sekunden.
webhook-signatureDie Signatur im Format Standard Webhooks.
X-KB-SignatureDieselbe Zustellung, signiert im früheren Sursa-Format.
X-KB-EventDer Ereignistyp.
X-KB-AttemptDie Nummer des Versuchs, ab 1.
X-KB-SequenceDie Position dieser Zustellung in der Reihenfolge des Endpunkts.

Bei Header-Namen wird nicht zwischen Groß- und Kleinschreibung unterschieden; Ihr Framework zeigt sie möglicherweise als X-Kb-Signature oder x-kb-signature.

Der Body ist das Ereignis. Sein data hängt vom Typ ab. Ein Endpunkt mit dem Payload-Modus thin erhält das Ereignis ohne data; lesen Sie das Objekt stattdessen mit Ihrem eigenen Schlüssel über die API.

3. Signatur prüfen

Prüfen Sie die Signatur jeder Zustellung, bevor Sie ihr vertrauen. Beide Formate signieren die exakten Bytes des Bodys. Lesen Sie daher den rohen Body vor jedem JSON-Parsing: Wird das JSON neu serialisiert, ändern sich die Bytes, und die Prüfung schlägt fehl.

FormatSignierte ZeichenketteSchlüsselSignatur
Standard Webhooks (webhook-signature)<webhook-id>.<webhook-timestamp>.<body>Der Teil des Secrets nach whsec_, base64-dekodiertv1, + Base64 von HMAC-SHA256
Früheres Format (X-KB-Signature)<t>.<body>Die ganze Secret-Zeichenkette als Textv1= + Hex von HMAC-SHA256

Es genügt, eines der beiden Formate zu prüfen. Bevorzugen Sie den Header von Standard Webhooks: Dieselbe Prüfung gibt es in vielen fertigen Bibliotheken. Während einer Rotation des Secrets trägt jeder Header eine Signatur pro gültigem Secret (in webhook-signature durch Leerzeichen getrennt, in X-KB-Signature als wiederholtes v1=). Eine Zustellung ist gültig, wenn eine davon übereinstimmt.

Lehnen Sie außerdem einen Zeitstempel ab, der mehr als 5 Minuten von Ihrer Uhr abweicht. So kann niemand eine aufgezeichnete Zustellung später erneut abspielen.

# 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

Ihre Prüfung testen

Führen Sie Ihren Code mit diesen Werten aus, bevor Sie ihn an echte Zustellungen anschließen. Mit der Uhr auf dem Zeitstempel müssen beide Prüfungen bestehen und fehlschlagen, sobald sich ein Byte des Bodys ändert.

EingabeWert
Secretwhsec_test_secret_0123456789abcdef
webhook-idevt_0123456789abcdef0123456789abcdef
webhook-timestamp und 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. Schnell antworten, später arbeiten

Sursa wartet bis zu 10 Sekunden auf Ihre Antwort. Jeder Status 2xx bedeutet, dass die Zustellung erfolgreich war; alles andere, auch eine Weiterleitung, ein Timeout oder eine geschlossene Verbindung, bedeutet, dass sie fehlgeschlagen ist und wiederholt wird.

Der Empfänger sollte daher nur prüfen, das Ereignis festhalten und antworten. Erledigen Sie die eigentliche Arbeit danach aus Ihrer eigenen Warteschlange.

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

Antworten Sie erst mit 2xx, wenn das Ereignis sicher gespeichert ist. Antworten Sie zuerst und stürzen danach ab, ist das Ereignis für Sie verloren, weil Sursa es bereits als zugestellt gezählt hat.

5. Wiederholungen

Eine fehlgeschlagene Zustellung wird mit wachsenden Pausen wiederholt: nach etwa 1 Minute, 5 Minuten, 30 Minuten, 2 Stunden, 5 Stunden, 10 Stunden, 24 Stunden und 24 Stunden – neun Versuche über fast drei Tage. Jede Pause schwankt um bis zu 20 %, damit Wiederholungen an viele Endpunkte nicht im selben Moment eintreffen.

  • Eine Antwort 429 oder 503 mit Retry-After verschiebt den nächsten Versuch auf mindestens diese Zeit, höchstens 24 Stunden später. Verwenden Sie sie, wenn Ihr System überlastet ist oder gewartet wird.
  • Eine Antwort 410 Gone deaktiviert den Endpunkt: Senden Sie sie nur, wenn Sie an dieser Adresse keine Zustellungen mehr wünschen.
  • Nach 10 aufeinanderfolgenden fehlgeschlagenen Versuchen wird der Endpunkt pausiert, und die Administratorinnen und Administratoren der Organisation werden benachrichtigt. Ereignisse sammeln sich weiter an und werden, sobald der Endpunkt fortgesetzt wird – in der Administrationsoberfläche oder mit POST /v1/webhooks/{webhook_id}/resume –, in der richtigen Reihenfolge zugestellt.
  • Nach dem letzten Versuch wird eine Zustellung als exhausted markiert. Sie können sie von Hand erneut senden.

Die Zustellungen eines Endpunkts mit jedem Versuch, Ihrem Antwortcode und dessen Zeitpunkt listet GET /v1/webhooks/{webhook_id}/deliveries auf, ebenso die Administrationsoberfläche. POST /v1/webhooks/{webhook_id}/deliveries/{delivery_id}/redeliver sendet eine Zustellung erneut. Eine erneute Zustellung hat dieselbe webhook-id.

6. Reihenfolge und Duplikate

  • Reihenfolge. Zustellungen an einen Endpunkt erfolgen streng nacheinander in der Reihenfolge von sequence. Während eine Zustellung wiederholt wird, warten die späteren. Verschiedene Endpunkte warten nicht aufeinander.
  • Duplikate. Dasselbe Ereignis kann mehr als einmal ankommen, zum Beispiel wenn Ihre Antwort auf dem Rückweg verloren ging. Speichern Sie die webhook-id (sie ist auch id im Body) jedes verarbeiteten Ereignisses und überspringen Sie eines, das Sie bereits gesehen haben.
  • In Zuständen denken. Vergleichen Sie bei Änderungen am selben Objekt created_at oder sequence mit dem Gespeicherten und ignorieren Sie ein Ereignis, das älter ist als der Zustand, den Sie bereits haben. Lesen Sie im Zweifel das aktuelle Objekt über die API: Das Ereignis sagt Ihnen, dass sich etwas geändert hat, die API sagt Ihnen, wie es jetzt ist.

7. Mit dem Ereignis-Feed abgleichen

Jedes Ereignis wird außerdem 30 Tage im Ereignis-Feed aufbewahrt, unabhängig davon, ob ein Endpunkt es abonniert hatte. War Ihr Empfänger länger ausgefallen, als die Wiederholungen abdecken, oder möchten Sie prüfen, dass nichts verpasst wurde, lesen Sie den Feed mit einem Schlüssel, der die Rolle admin und den Scope webhooks:manage hat:

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"

Ein Ereignis im Feed hat dieselbe id und dieselbe Form wie in einer Zustellung, nur ohne die Zustellungsfelder sequence, payload und test. Weil Ihr Empfänger bereits gesehene Ereignisse überspringt, können Sie den Abgleich so oft ausführen, wie Sie möchten, zum Beispiel einmal pro Stunde.

GET /v1/webhook-events/{event_id} liest ein einzelnes Ereignis über seine ID.

8. Secret rotieren

POST /v1/webhooks/{webhook_id}/rotate-secret erzeugt ein neues Secret und lässt das vorherige für einen Überlappungszeitraum gültig, standardmäßig 24 Stunden und bis zu 7 Tage. Während der Überlappung wird jede Zustellung mit beiden Secrets signiert.

  1. Rotieren Sie und hinterlegen Sie das neue Secret neben dem alten in Ihrem Empfänger.
  2. Akzeptieren Sie eine Zustellung, wenn sie sich mit einem der beiden Secrets prüfen lässt.
  3. Entfernen Sie nach der Überlappung das alte Secret.

Ist das alte Secret nach außen gelangt, beenden Sie die Überlappung sofort mit POST /v1/webhooks/{webhook_id}/revoke-previous-secret oder rotieren Sie mit "overlap_hours": 0.

Checkliste

  • Prüfen Sie eine der Signaturen am rohen Body und prüfen Sie den Zeitstempel.
  • Speichern Sie das Ereignis anhand von webhook-id und antworten Sie dann innerhalb von 10 Sekunden mit 2xx.
  • Überspringen Sie Duplikate und veraltete Ereignisse; lesen Sie das aktuelle Objekt über die API, wenn es darauf ankommt.
  • Antworten Sie mit 429 oder 503 und Retry-After, wenn Sursa langsamer zustellen soll; antworten Sie nie versehentlich mit 410.
  • Überspringen Sie Ereignistypen und Felder, die Sie nicht kennen.
  • Gleichen Sie regelmäßig mit dem Ereignis-Feed ab.