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
| Event | Wird ausgelöst, wenn |
|---|---|
passport.published | Ein Pass wechselt zu status=published. |
passport.suspended | Ein Pass wird per API oder UI suspendiert. Der öffentliche Viewer schaltet auf einen Suspended-Zustand. |
passport.expired | Der expiresAt-Zeitstempel eines veröffentlichten Passes läuft ab (Daily-Cron). Wird vom Revenue-Protection-Moat nach 30 Tagen Cancellation-Grace genutzt. |
passport.archived | Ein Pass wird archiviert. Der öffentliche Viewer liefert 404 (404, nicht 410 — die URL verhält sich, als hätte sie nie existiert). |
extraction.completed | Die Multi-Agent-KI-Extraction-Pipeline schließt für einen Pass ab. Body trägt die extraction-id für Folge-Lesevorgänge. |
extraction.failed | Eine Extraction-Session liegt jenseits des 1h-Resume-Fensters ohne Wiederherstellung — der Orchestrator markiert sie als failed. |
supplier.submitted | Ein Lieferant schließt den token-auth Lieferantenportal-Submit für einen Ihrer Supplier-Requests ab. |
Signaturen verifizieren
Jede Anfrage trägt das Signing-Trio:
| Header | Wert |
|---|---|
X-TracePass-Signature | v1=<hex HMAC-SHA256 von `${X-TracePass-Timestamp}.${rawBody}` mit Ihrem Endpoint-Secret>. |
X-TracePass-Timestamp | Unix-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-Event | Event-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-Id | Per-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
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
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", 200Go
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:
| Versuch | Verzögerung nach vorigem Versuch |
|---|---|
| 1 (initial) | Sofort, wenn das Event auslöst |
| 2 | 1 Minute |
| 3 | 5 Minuten |
| 4 | 30 Minuten |
| 5 | 2 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 Zustellungshistorie 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.