TracePass
Riferimento

Webhooks

Consegna di eventi firmata HMAC per gli eventi del ciclo di vita dei passaporti. Scala dei nuovi tentativi, verifica della firma, protezione contro la riproduzione.

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

EventoSi attiva quando
passport.publishedUn passaporto passa allo stato status=published.
passport.suspendedUn passaporto viene sospeso tramite API o UI. Il visualizzatore pubblico passa a uno stato sospeso.
passport.expiredIl 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.archivedUn passaporto viene archiviato. Il visualizzatore pubblico restituisce 404 (404, non 410 — l'URL si comporta come se non fosse mai esistito).
extraction.completedLa pipeline di estrazione IA multi-agente termina per un passaporto. Il corpo porta l'id dell'estrazione per le letture successive.
extraction.failedUna sessione di estrazione è oltre la finestra di ripresa di 1 ora senza recupero — l'orchestratore la contrassegna come failed.
supplier.submittedUn 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:

HeaderValore
X-TracePass-Signaturev1=<hex HMAC-SHA256 di `${X-TracePass-Timestamp}.${rawBody}` con il secret del vostro endpoint>.
X-TracePass-TimestampSecondi 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-EventNome del tipo di evento (es. passport.published). Permette al vostro ricevitore di smistare senza fare il parsing del JSON.
X-TracePass-Event-IdStabile tra i nuovi tentativi — deduplicate su questo. Rispecchia il campo `id` del corpo.
X-TracePass-Delivery-IdId 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

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)
}

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:

TentativoRitardo dopo il tentativo precedente
1 (iniziale)Immediato quando l'evento si attiva
21 minuto
35 minuti
430 minuti
52 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.