Dokumente/API & Webhooks/Webhooks

Webhooks

Abonnieren Sie einen HTTPS-Endpoint auf Ihrer Seite und SecureCodingHub wird einen signierten JSON-Umschlag POSTen, wann immer die Ereignisse, die Sie interessieren, geschehen. Jede Zustellung ist mit HMAC-SHA256 signiert, sodass Sie verifizieren können, dass sie von uns kam, nicht von einem Spoofer.

Wann Webhooks verwenden

  • Öffnen Sie ein Jira-Ticket, wann immer ein Entwickler eine obligatorische Zuweisung abschließt (oder nicht besteht).
  • Aktualisieren Sie einen HR-Datensatz, wenn ein Kategorieabschluss-Zertifikat ausgestellt wird.
  • Benachrichtigen Sie Slack, wenn ein SARIF-Lauf von CI Schulungszuweisungen automatisch erstellt.
  • Spiegeln Sie SCH-Zustand in Ihr eigenes Data Warehouse, ohne die REST API zu polln.

Einen Endpoint erstellen

1

Melden Sie sich als Organisationsadministrator an und öffnen Sie Organisation → Webhooks.

2

Klicken Sie auf + Endpoint hinzufügen. Geben Sie die öffentliche HTTPS-URL ein, die Zustellungen erhalten wird. Ngrok / cloudflared Tunnel funktionieren gut für die lokale Entwicklung.

3

Markieren Sie die Ereignisse, die Sie abonnieren möchten (Sie können dies später ändern).

4

Klicken Sie auf Endpoint erstellen. Ein whsec_… Signaturgeheimnis wird einmal angezeigt. Kopieren Sie es in Ihren Secrets-Manager — dies ist der Schlüssel, mit dem Sie jede Zustellung verifizieren.

Ereigniskatalog

v1 veröffentlicht vier Ereignistypen. Der Katalog ist auch programmatisch unter GET /api/public/v1/webhook-endpoints/events verfügbar.

EreignisWird ausgelöst, wennPayload data-Form
assignment.createdEine Zuweisung erstellt wird — manuell von einem Administrator, über die öffentliche API oder als Nebenwirkung der SARIF-Ingestion.Gleiche Form wie POST /assignments-Antwort.
assignment.completedEin Benutzer 100% Abschluss bei einer Zuweisung erreicht, die ihm gegeben wurde.Zuweisungsübersicht plus userId, userName, userEmail, completedAt.
sarif.ingestedEin SARIF-Lauf verarbeitet wurde (unabhängig davon, ob Zuweisungen daraus erstellt wurden).Gleiche Form wie die synchrone POST /sarif-Antwort.
certificate.issuedEin Kategorieabschluss-Zertifikat automatisch an einen Benutzer ausgestellt wird.certificateId, certificateNumber, Benutzerdetails, categoryId, categoryTitle, issuedAt.

Anfrageform

Jede Zustellung ist ein POST mit diesen Headern und einem JSON-Körper:

POST /your-endpoint HTTP/1.1
Host: example.com
Content-Type: application/json
User-Agent: SecureCodingHub-Webhooks/1.0
X-SecureCodingHub-Event: assignment.completed
X-SecureCodingHub-Event-Id: evt_8a3d…
X-SecureCodingHub-Delivery: 9c1b…
X-SecureCodingHub-Signature: t=1717003245,v1=ad8c…

{
  "id": "evt_8a3d…",
  "type": "assignment.completed",
  "createdAt": "2026-05-29T13:45:12.412Z",
  "orgId": "e188fc87-…",
  "data": { … }
}

Der Umschlag ist über Ereignistypen identisch. Nur type und das innere data ändern sich.

Die Signatur verifizieren

Der X-SecureCodingHub-Signature-Header hat das Format t=<unix_seconds>,v1=<hex>. Zur Verifizierung:

1

Parsen Sie t und v1 aus dem Header.

2

Berechnen Sie HMAC-SHA256("<t>.<raw_body>", secret) als Kleinbuchstaben-Hex-String.

3

Vergleichen Sie mit v1 in konstanter Zeit.

4

Ablehnen, wenn der Zeitstempel mehr als 5 Minuten von Ihrer Server-Uhr entfernt ist (verteidigt gegen Replay).

Verifizieren Sie immer gegen den roh-Anfragekörper — das erneute Serialisieren des JSON ändert die Byte-Reihenfolge und macht die Signatur ungültig.

Node.js-Beispiel

import crypto from 'crypto'
import express from 'express'

const app = express()
const SECRET = process.env.SCH_WEBHOOK_SECRET

app.post('/webhook',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const header = req.header('X-SecureCodingHub-Signature') || ''
    const parts = Object.fromEntries(
      header.split(',').map(p => p.trim().split('='))
    )
    const ts = parseInt(parts.t, 10)
    const sig = parts.v1
    if (!ts || !sig) return res.status(400).send('bad signature header')
    if (Math.abs(Date.now() / 1000 - ts) > 300) {
      return res.status(400).send('stale')
    }
    const expected = crypto
      .createHmac('sha256', SECRET)
      .update(`${ts}.${req.body.toString('utf8')}`)
      .digest('hex')
    const ok = crypto.timingSafeEqual(
      Buffer.from(expected, 'hex'),
      Buffer.from(sig, 'hex')
    )
    if (!ok) return res.status(401).send('bad signature')

    const event = JSON.parse(req.body.toString('utf8'))
    console.log(`✓ ${event.type} ${event.id}`)
    res.json({ received: true })
  })

app.listen(7777)

Python (Flask)-Beispiel

import hashlib, hmac, os, time
from flask import Flask, request, abort

SECRET = os.environ['SCH_WEBHOOK_SECRET'].encode()
app = Flask(__name__)

@app.post('/webhook')
def webhook():
    header = request.headers.get('X-SecureCodingHub-Signature', '')
    parts = dict(p.split('=') for p in header.split(','))
    try:
        ts = int(parts['t']); sig = parts['v1']
    except (KeyError, ValueError):
        abort(400, 'bad signature header')
    if abs(time.time() - ts) > 300:
        abort(400, 'stale')
    raw = request.get_data()  # raw bytes
    expected = hmac.new(SECRET, f"{ts}.".encode() + raw, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, sig):
        abort(401, 'bad signature')
    event = request.get_json()
    print('OK', event['type'], event['id'])
    return {'received': True}

Go-Beispiel

package main

import (
  "crypto/hmac"; "crypto/sha256"; "encoding/hex"; "io"
  "net/http"; "os"; "strconv"; "strings"; "time"
)

var secret = []byte(os.Getenv("SCH_WEBHOOK_SECRET"))

func webhook(w http.ResponseWriter, r *http.Request) {
  header := r.Header.Get("X-SecureCodingHub-Signature")
  var ts int64; var sig string
  for _, p := range strings.Split(header, ",") {
    kv := strings.SplitN(strings.TrimSpace(p), "=", 2)
    if len(kv) != 2 { continue }
    if kv[0] == "t" { ts, _ = strconv.ParseInt(kv[1], 10, 64) }
    if kv[0] == "v1" { sig = kv[1] }
  }
  if ts == 0 || sig == "" { http.Error(w, "bad header", 400); return }
  if abs(time.Now().Unix() - ts) > 300 { http.Error(w, "stale", 400); return }
  body, _ := io.ReadAll(r.Body)
  mac := hmac.New(sha256.New, secret)
  mac.Write([]byte(strconv.FormatInt(ts, 10) + "."))
  mac.Write(body)
  if !hmac.Equal(mac.Sum(nil), mustHex(sig)) {
    http.Error(w, "bad signature", 401); return
  }
  w.Write([]byte(`{"received":true}`))
}

func mustHex(s string) []byte { b, _ := hex.DecodeString(s); return b }
func abs(x int64) int64 { if x < 0 { return -x }; return x }

func main() {
  http.HandleFunc("/webhook", webhook)
  http.ListenAndServe(":7777", nil)
}

Zustellungsgarantien & Wiederholungen

Webhook-Zustellungen werden in eine Warteschlange gestellt und asynchron von einem Hintergrund-Worker versendet. Sie sind mindestens-einmal — unter seltenen Umständen (Timeouts, Response-Races) kann dasselbe Ereignis zweimal zugestellt werden. Verwenden Sie das id-Feld im Umschlag (und den X-SecureCodingHub-Event-Id-Header), um zu deduplizieren.

Ihr Endpoint sollte mit einem 2xx-Status innerhalb von 15 Sekunden antworten. Alles andere wird als Fehler behandelt und löst eine Wiederholung mit exponentiellem Backoff aus:

VersuchZeit nach vorherigem
1 (initial)sofort nach dem Quellereignis in die Warteschlange gestellt
2+1 Minute
3+5 Minuten
4+30 Minuten
5 (final)+2 Stunden

Nach dem 5. Fehler wird die Zustellung als exhausted markiert und der Endpoint automatisch deaktiviert. Sie sehen die Fehler und den Auto-Disable-Zeitstempel unter Organisation → Webhooks → Zustellungen anzeigen.

Best Practices

  • Verifizieren Sie immer die Signatur. Jeder, der Ihre URL errät, könnte sonst gefälschte Ereignisse in Ihre Pipeline spammen.
  • Antworten Sie schnell. Übergeben Sie die Arbeit an eine Warteschlange und geben Sie 200 innerhalb von Millisekunden zurück — verarbeiten Sie nicht synchron innerhalb des Handlers.
  • Dedupliziere nach event.id. Gesehene IDs für mindestens 7 Tage speichern.
  • Vertrauen Sie nicht der IP-basierten Filterung. Cloudflare-Egress-IPs können sich ohne Vorankündigung ändern; die Signatur ist der einzige stabile Vertrauensanker.
  • Testen Sie vor der Inbetriebnahme. Verwenden Sie cloudflared (cloudflared tunnel --url http://localhost:7777) oder ngrok, um einen lokalen Empfänger freizulegen, dann lösen Sie ein Ereignis über die API aus, um eine End-to-End-Zustellung zu erfassen.

Letzte Zustellungen prüfen

Die Administratorkonsole listet die letzten 50 Zustellungen pro Endpoint mit Status, Versuchsanzahl, Antwortcode und jeder Fehlermeldung auf. Programmatischer Zugriff ist verfügbar unter:

GET /api/public/v1/webhook-endpoints/{id}/deliveries

Dies erfordert den webhook:manage-Scope.