---
title: Webhooks
description: Eventi firmati HMAC per il ciclo di vita dei passaporti. Retry ladder, verifica firma, protezione anti-replay.
canonical: "https://www.tracepass.eu/it/docs/webhooks"
locale: it
source: "https://www.tracepass.eu/it/docs/webhooks"
---

# Webhooks

> Eventi firmati HMAC per il ciclo di vita dei passaporti. Retry ladder, verifica firma, protezione anti-replay.

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 un`X-TracePass-Event-Id`stabile 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.                                                              |
| epcis.event.captured | Uno o più eventi EPCIS 2.0 vengono acquisiti per un passaporto tramite l'endpoint di capture.                                                                                            |

**Tip —** Il limite giornaliero per sottoscrizione è un valore fisso di 10.000 consegne per difendersi da un endpoint mal configurato che consuma la coda. Raggiungere quel limite mette in pausa la consegna per quell'endpoint fino al giorno UTC successivo; nulla va perso — gli eventi non consegnati rimangono in coda e riprendono al tick successivo.

## 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

```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, sette 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            | 12 ore                               |
| 7 (finale)   | 24 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 `id` del 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.

**Perché il timestamp conta:** la firma da sola non protegge da una consegna catturata-e-riprodotta. La finestra del timestamp di 5 minuti più la deduplicazione dell'eventId sono la coppia che vi serve — eliminatene uno e la superficie d'attacco si allarga. Stripe, Twilio, GitHub applicano entrambi; noi seguiamo la convenzione.
