TracePass
Referenz

Webhooks

HMAC-signierte Event-Zustellung für Pass-Lebenszyklus-Events. Retry-Ladder, Signaturprüfung, Replay-Schutz.

TracePass POSTet JSON-Body-Events an Ihre URL, wenn Pässe, Extractions oder Supplier-Requests den Status wechseln. Jede Zustellung ist HMAC-SHA-256-signiert mit einem von Ihnen kontrollierten Per-Endpoint-Secret, wird auf einem festen exponentiellen Ladder wiederholt und trägt eine stabileX-TracePass-Event-Idzur Replay-Protection.

Endpoint konfigurieren

Öffnen Sie Developer → Webhooks im Dashboard. Tragen Sie Ihre URL ein, kopieren Sie das Per-Endpoint-Signing-Secret (wird einmal bei der Erstellung ausgegeben — dann erfassen, rotieren via Delete + Recreate) und wählen Sie aus, welche Events zugestellt werden sollen. Sie können mehrere Endpoints haben — typischerweise einen für Staging, einen für Produktion. Der Send test-Button im Dashboard feuert einen webhook.test-Envelope, sodass Sie den Receiver verdrahten können, bevor echte Events fließen.

Events

EventWird ausgelöst, wenn
passport.publishedEin Pass wechselt zu status=published.
passport.suspendedEin Pass wird per API oder UI suspendiert. Der öffentliche Viewer schaltet auf einen Suspended-Zustand.
passport.expiredDer expiresAt-Zeitstempel eines veröffentlichten Passes läuft ab (Daily-Cron). Wird vom Revenue-Protection-Moat nach 30 Tagen Cancellation-Grace genutzt.
passport.archivedEin Pass wird archiviert. Der öffentliche Viewer liefert 404 (404, nicht 410 — die URL verhält sich, als hätte sie nie existiert).
extraction.completedDie Multi-Agent-KI-Extraction-Pipeline schließt für einen Pass ab. Body trägt die extraction-id für Folge-Lesevorgänge.
extraction.failedEine Extraction-Session liegt jenseits des 1h-Resume-Fensters ohne Wiederherstellung — der Orchestrator markiert sie als failed.
supplier.submittedEin Lieferant schließt den token-auth Lieferantenportal-Submit für einen Ihrer Supplier-Requests ab.

Signaturen verifizieren

Jede Anfrage trägt das Signing-Trio:

HeaderWert
X-TracePass-Signaturev1=<hex HMAC-SHA256 von `${X-TracePass-Timestamp}.${rawBody}` mit Ihrem Endpoint-Secret>.
X-TracePass-TimestampUnix-Sekunden zum Zeitpunkt der Signierung. Lehnen Sie Zustellungen ab, deren Zeitstempel mehr als 5 Minuten von Ihrer Uhr abweicht — das ist das Replay-Protection-Fenster.
X-TracePass-EventEvent-Typname (z. B. passport.published). Erlaubt Ihrem Receiver Dispatch ohne JSON-Parsing.
X-TracePass-Event-IdÜber Retries stabil — dedupen Sie darauf. Spiegelt das `id`-Feld des Bodies.
X-TracePass-Delivery-IdPer-Attempt-ID. Für Support-Korrelation nutzen, nicht für Deduplication.

Berechnen Sie die Signatur erneut über ${timestamp}.${rawBody} — der Zeitstempel wird mit einem literalen .-Separator vor den Rohbytes konkateniert. Nicht den JSON neu serialisieren; Whitespace-Unterschiede flippen den HMAC. Vergleichen Sie mit einem Constant-Time- Check.

Node / TypeScript

typescript
import { createHmac, timingSafeEqual } from "node:crypto";
import express from "express";

const app = express();
const SECRET = process.env.TRACEPASS_WEBHOOK_SECRET!;

// Capture the RAW body — needed for HMAC verification.
app.post("/tracepass-webhook", express.raw({ type: "application/json" }), (req, res) => {
  const sigHeader = String(req.headers["x-tracepass-signature"] ?? "");
  const ts = String(req.headers["x-tracepass-timestamp"] ?? "");

  // 5-minute replay window.
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - Number(ts)) > 300) {
    return res.status(400).send("stale");
  }

  // Parse the v1=<hex> envelope.
  const m = /^v1=([0-9a-f]+)$/i.exec(sigHeader);
  if (!m) return res.status(400).send("malformed signature header");
  const provided = Buffer.from(m[1], "hex");

  // HMAC over `${timestamp}.${body}`.
  const expected = createHmac("sha256", SECRET)
    .update(`${ts}.`)
    .update(req.body)
    .digest();

  if (
    provided.length !== expected.length ||
    !timingSafeEqual(provided, expected)
  ) {
    return res.status(400).send("bad signature");
  }

  const event = JSON.parse(req.body.toString("utf8"));
  // ... your handler ...
  res.status(200).send("ok");
});

Python

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

app = Flask(__name__)
SECRET = os.environ["TRACEPASS_WEBHOOK_SECRET"].encode()

@app.post("/tracepass-webhook")
def handler():
    sig_header = request.headers.get("X-TracePass-Signature", "")
    ts = request.headers.get("X-TracePass-Timestamp", "0")

    if abs(int(time.time()) - int(ts)) > 300:
        abort(400, "stale")

    if not sig_header.startswith("v1="):
        abort(400, "malformed signature header")
    provided = bytes.fromhex(sig_header[3:])

    raw = request.get_data()
    expected = hmac.new(SECRET, f"{ts}.".encode() + raw, hashlib.sha256).digest()

    if not hmac.compare_digest(provided, expected):
        abort(400, "bad signature")

    event = request.get_json()
    # ... your handler ...
    return "ok", 200

Go

go
func handler(w http.ResponseWriter, r *http.Request) {
    sigHeader := r.Header.Get("X-TracePass-Signature")
    ts        := r.Header.Get("X-TracePass-Timestamp")

    tsInt, _ := strconv.ParseInt(ts, 10, 64)
    if math.Abs(float64(time.Now().Unix()-tsInt)) > 300 {
        http.Error(w, "stale", http.StatusBadRequest); return
    }
    if !strings.HasPrefix(sigHeader, "v1=") {
        http.Error(w, "malformed signature header", http.StatusBadRequest); return
    }
    provided, err := hex.DecodeString(strings.TrimPrefix(sigHeader, "v1="))
    if err != nil {
        http.Error(w, "malformed signature header", http.StatusBadRequest); return
    }

    body, _ := io.ReadAll(r.Body)
    mac := hmac.New(sha256.New, []byte(os.Getenv("TRACEPASS_WEBHOOK_SECRET")))
    mac.Write([]byte(ts + "."))
    mac.Write(body)
    expected := mac.Sum(nil)

    if !hmac.Equal(provided, expected) {
        http.Error(w, "bad signature", http.StatusBadRequest); return
    }

    // parse body, dispatch event …
    w.WriteHeader(http.StatusOK)
}

Retry-Ladder

Eine Zustellung gilt als erfolgreich, wenn Ihr Endpoint binnen 10 Sekunden ein 2xx liefert. Alles andere (Netzwerk-Fehler, Timeout, 4xx, 5xx) löst einen Retry auf diesem festen Plan aus, insgesamt sechs Versuche:

VersuchVerzögerung nach vorigem Versuch
1 (initial)Sofort, wenn das Event auslöst
21 Minute
35 Minuten
430 Minuten
52 Stunden
6 (final)12 Stunden

Nach Versuch 6 wird die Zustellung im Webhook-Event-Log des Dashboards mit Status failed geparkt; Sie können sie dort manuell erneut auslösen. Die Zustellungs­historie ist plan-skaliert: Basic 30 Tage, Starter 60, Growth 90, Scale 180, Pro 365, Enterprise 730. Die TTL wird beim Insert aus dem Plan der Firma berechnet, ein Plan-Wechsel lässt alte Zustellungen nicht rückwirkend ablaufen.

Replay-Protection

Behandeln Sie X-TracePass-Event-Id als Deduplication-Key. Sie spiegelt das id-Feld des Bodies, wiederholt sich über Retries absichtlich (damit Ihr Receiver no-oppen kann, wenn das Event bereits verarbeitet ist) und wiederholt sich nie über verschiedene Events. SQL UNIQUE auf der Spalte oder Redis SET NX mit 7-Tage-TTL — beides funktioniert.