TracePass invia in POST eventi con corpo JSON al vostro URL quando passaporti, estrazioni o richieste ai fornitori cambiano stato. Ogni consegna è firmata HMAC-SHA-256 con un secret per endpoint che controllate voi, ritentata su una scala esponenziale fissa e porta unX-TracePass-Event-Idstabile per la protezione contro la riproduzione.
Configurare un endpoint
Aprite Developer → Webhooks nella dashboard. Aggiungete il vostro URL, copiate il secret di firma per endpoint (restituito una sola volta alla creazione — catturatelo allora, ruotatelo con elimina + ricrea) e selezionate quali eventi consegnare. Potete avere più endpoint — tipicamente uno per lo staging, uno per la produzione. Il pulsante Send test della dashboard invia un envelope webhook.test così potete collegare il ricevitore prima che fluiscano gli eventi reali.
Eventi
| Evento | Si attiva quando |
|---|---|
passport.published | Un passaporto passa allo stato status=published. |
passport.suspended | Un passaporto viene sospeso tramite API o UI. Il visualizzatore pubblico passa a uno stato sospeso. |
passport.expired | Il timestamp expiresAt di un passaporto pubblicato viene superato (cron giornaliero). Usato dal meccanismo di protezione dei ricavi dopo i 30 giorni di tolleranza per la cancellazione. |
passport.archived | Un passaporto viene archiviato. Il visualizzatore pubblico restituisce 404 (404, non 410 — l'URL si comporta come se non fosse mai esistito). |
extraction.completed | La pipeline di estrazione IA multi-agente termina per un passaporto. Il corpo porta l'id dell'estrazione per le letture successive. |
extraction.failed | Una sessione di estrazione è oltre la finestra di ripresa di 1 ora senza recupero — l'orchestratore la contrassegna come failed. |
supplier.submitted | Un fornitore completa l'invio dal portale fornitori con autenticazione a token per una delle vostre richieste ai fornitori. |
Verificare le firme
Ogni richiesta porta il trio di firma:
| Header | Valore |
|---|---|
X-TracePass-Signature | v1=<hex HMAC-SHA256 di `${X-TracePass-Timestamp}.${rawBody}` con il secret del vostro endpoint>. |
X-TracePass-Timestamp | Secondi Unix al momento della firma. Rifiutate le consegne il cui timestamp è a più di 5 minuti dal vostro orologio — è la finestra di protezione contro la riproduzione. |
X-TracePass-Event | Nome del tipo di evento (es. passport.published). Permette al vostro ricevitore di smistare senza fare il parsing del JSON. |
X-TracePass-Event-Id | Stabile tra i nuovi tentativi — deduplicate su questo. Rispecchia il campo `id` del corpo. |
X-TracePass-Delivery-Id | Id per tentativo. Usatelo per la correlazione con il supporto, non per la deduplicazione. |
Ricalcolate la firma su ${timestamp}.${rawBody} — il timestamp viene concatenato con un separatore .letterale prima dei byte grezzi. Non riserializzate il JSON; le differenze di spazi bianchi faranno cambiare l'HMAC. Confrontate con un controllo a tempo costante.
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)
}Scala dei nuovi tentativi
Una consegna è considerata riuscita quando il vostro endpoint restituisce un 2xx entro 10 secondi. Qualsiasi altra cosa (errore di rete, timeout, 4xx, 5xx) attiva un nuovo tentativo su questa pianificazione fissa, sei tentativi in totale:
| Tentativo | Ritardo dopo il tentativo precedente |
|---|---|
| 1 (iniziale) | Immediato quando l'evento si attiva |
| 2 | 1 minuto |
| 3 | 5 minuti |
| 4 | 30 minuti |
| 5 | 2 ore |
| 6 (finale) | 12 ore |
Dopo il tentativo 6 la consegna viene parcheggiata nel registro eventi webhook della dashboard con stato failed; potete riprodurla manualmente da lì. La conservazione della cronologia delle consegne scala con il piano: Basic 30 giorni, Starter 60, Growth 90, Scale 180, Pro 365, Enterprise 730. Il TTL viene calcolato all'inserimento dal piano dell'azienda, quindi un cambio di piano non fa scadere retroattivamente le vecchie consegne.
Protezione contro la riproduzione
Trattate X-TracePass-Event-Id come una chiave di deduplicazione. Rispecchia il campo iddel corpo, si ripete tra i nuovi tentativi di proposito (così il vostro ricevitore può non fare nulla quando ha già elaborato l'evento) e non si ripete mai tra eventi distinti. Funzionano sia un UNIQUE SQL sulla colonna sia un SET NX Redis con un TTL di 7 giorni.