Webhook
Ricevi notifiche di eventi firmati quando i documenti vengono indicizzati, le consegne falliscono o gli endpoint cambiano stato. Verifica, nuovi tentativi e test.
I webhook inviano notifiche di eventi ai tuoi sistemi come richieste HTTPS POST, così puoi reagire ai cambiamenti senza effettuare polling. Registra un endpoint dalle impostazioni del tuo workspace: l'URL deve utilizzare HTTPS, e ogni workspace può registrare fino a 10 endpoint. Al momento della creazione ricevi un segreto di firma (prefisso whsec_, il formato Standard Webhooks) esattamente una volta; non è mai recuperabile in seguito, quindi copialo immediatamente nel tuo archivio dei segreti.
Ogni consegna è un corpo JSON con la stessa busta:
{
"event": "documents.indexed",
"timestamp": "2026-07-23T09:14:07.000Z",
"tenantId": "0198f2a4-6c1e-7d30-b6a1-2f9d54c08a11",
"sequence": 42,
"data": { "count": 12 }
}I payload sono deliberatamente leggeri: solo id, conteggi e timestamp. Tratta un webhook come un segnale che qualcosa è cambiato, poi recupera lo stato attuale tramite la API autenticata. I nomi dei documenti e i contenuti non appaiono mai nel payload di un webhook.
sequence è un contatore per-endpoint, denso per il flusso di questo endpoint: eventi consecutivi consegnati a questo endpoint hanno numeri consecutivi, quindi un numero mancante significa una consegna che il tuo servizio non ha mai ricevuto. L'ORDINE di consegna non è garantito, quindi usa la sequenza (non l'ordine di arrivo) per ordinare gli eventi. Due avvertenze: i replay arrivano fuori ordine per progetto (un evento recuperato mantiene la sua sequenza originale, più vecchia), quindi deduplica su webhook-id e non scartare mai una consegna solo perché la sua sequenza è inferiore al tuo watermark alto; e una lacuna può anche significare che l'evento mancante sta ancora tentando o è in attesa in uno stato replayable, quindi tratta le lacune come "controlla il registro delle consegne", non come prova di perdita.
Catalogo degli eventi
Sottoscrivi un endpoint a qualsiasi combinazione dei tipi di evento elencati di seguito. I tipi contrassegnati come pianificati possono già essere selezionati, ma non viene ancora generato nulla per essi e la forma del loro payload non è definitiva; viene pubblicata qui quando l'evento diventa attivo.
| Evento | Stato | Payload (data) |
|---|---|---|
documents.indexed | Live | I documenti sono diventati ricercabili. Aggregato per workspace: al massimo un evento per finestra di aggregazione, con un conteggio. |
documents.failed | Live | I documenti hanno fallito definitivamente l'elaborazione. Aggregato per workspace: al massimo un evento per finestra di aggregazione, con un conteggio. |
sync.completed | Pianificato | Una sincronizzazione di un connettore è stata completata. La forma del payload non è definitiva. |
sync.failed | Pianificato | Una sincronizzazione di un connettore è fallita. La forma del payload non è definitiva. |
ingestion.completed | Pianificato | Un'esecuzione di ingestione è stata completata. La forma del payload non è definitiva. |
ingestion.failed | Pianificato | Un'esecuzione di ingestione è fallita. La forma del payload non è definitiva. |
audit.recorded | Live | Il registro di audit del workspace ha acquisito nuove voci. Aggregato per workspace su una finestra: un conteggio e la finestra da recuperare, mai le voci stesse. |
endpoint.disabled | Live | Un endpoint di webhook è stato disabilitato: automaticamente dopo ripetuti fallimenti di consegna o un 410 Gone, o da un amministratore. Consegnato agli altri endpoint del workspace. |
delivery.failing | Live | Le consegne a un endpoint di webhook stanno fallendo: generato una volta per finestra di fallimento, 30 minuti dopo il primo tentativo fallito, e consegnato agli altri endpoint del workspace. |
endpoint.enabled | Live | Un endpoint di webhook disabilitato è stato riattivato. Chiude l'incidente aperto da endpoint.disabled; consegnato agli altri endpoint del workspace. |
delivery.recovered | Live | Una consegna è riuscita su un endpoint per cui era stato generato delivery.failing. Chiude quell'incidente; consegnato agli altri endpoint del workspace. |
webhook.ping | Live | Evento di test diretto inviato dall'azione send-test della dashboard a esattamente un endpoint. Mai sottoscrivibile; data è sempre vuoto. |
Esempi di oggetti data per gli eventi live:
// documents.indexed
{ "count": 12 }
// documents.failed
{ "count": 2 }
// audit.recorded
{
"count": 34,
"windowStart": "2026-07-23T09:05:00.000Z",
"windowEnd": "2026-07-23T09:10:00.000Z"
}
// endpoint.disabled
{
"endpointId": "0198f2a4-6c1e-7d30-b6a1-2f9d54c08a11",
"reason": "sustained_failure",
"disabledAt": "2026-07-23T09:14:07.000Z"
}
// delivery.failing
{
"endpointId": "0198f2a4-6c1e-7d30-b6a1-2f9d54c08a11",
"failingSince": "2026-07-23T09:14:07.000Z"
}
// endpoint.enabled
{
"endpointId": "0198f2a4-6c1e-7d30-b6a1-2f9d54c08a11",
"enabledAt": "2026-07-24T08:02:51.000Z"
}
// delivery.recovered
{
"endpointId": "0198f2a4-6c1e-7d30-b6a1-2f9d54c08a11",
"failingSince": "2026-07-23T09:14:07.000Z",
"recoveredAt": "2026-07-23T11:40:12.000Z"
}
// webhook.ping
{}Gli eventi operativi (delivery.failing, delivery.recovered,
endpoint.disabled, endpoint.enabled) riportano lo stato di altri endpoint: l'endpoint a cui si riferisce un evento è sempre escluso dalla consegna di quell'evento, poiché consegnarvi sarebbe rumore garantito.
Ogni evento live è descritto anche nel documento OpenAPI all'indirizzo
/api/openapi.json, sotto webhooks: lo schema JSON completo del corpo e i tre header di firma, così puoi generare i tipi del ricevitore invece di copiare gli esempi sopra. Lo schema è applicato anche dal nostro lato: un payload che non lo rispetta non viene mai inviato.
Alimentare un registro di audit a un SIEM
audit.recorded è la metà push di un feed SIEM, ed è deliberatamente non le voci: una voce di audit nomina una persona, il suo indirizzo e cosa ha toccato, e un payload di webhook è memorizzato nel registro delle consegne e inviato a un URL che controlli, che può risolversi ovunque. L'evento ti dice che una finestra si è chiusa con voci al suo interno; recuperi le voci tramite l'API autenticata con una chiave che porta lo scope audit:read.
Ad ogni evento, cerca nel registro con windowStart come startDate e
windowEnd come endDate, impaginando sul cursore finché non torna nullo. Il limite inferiore della ricerca è inclusivo e quello della finestra no, quindi una pagina può ripetere la voce esattamente a quell'istante; deduplica sull'id della voce, di cui hai bisogno in ogni caso perché la consegna è almeno-una-volta.
L'endpoint del catalogo elenca ogni azione che il registro può registrare, con la categoria a cui appartiene ciascuna e una versione che cambia quando il vocabolario lo fa. Leggilo una volta, confronta la versione nelle letture successive e tratta un'azione mancante come sconosciuta piuttosto che non valida: il registro è append-only e mantiene l'ortografia con cui ogni voce è stata scritta.
Le finestre sono contigue finché rimani sottoscritto: ognuna inizia dove è terminata l'ultima che ti è stata inviata, quindi un passaggio ritardato rende la finestra successiva più ampia invece di perdere ciò che è accaduto nel frattempo. Una finestra senza voci non invia nulla.
Verifica delle consegne
Ogni consegna è firmata seguendo la specifica Standard Webhooks. Verifica sempre la firma prima di fidarti di una richiesta: il tuo URL di endpoint è raggiungibile da chiunque su internet.
Ogni richiesta porta tre header:
| Header | Valore |
|---|---|
webhook-id | Id del messaggio. Identico in ogni tentativo, rinvio e replay dello stesso evento; deduplica su di esso. |
webhook-timestamp | Timestamp Unix in secondi al momento dell'invio. |
webhook-signature | Una o più firme, separate da spazi, ognuna nel formato v1,{base64}. |
Per verificare:
- Ricostruisci il contenuto firmato come
{webhook-id}.{webhook-timestamp}.{raw body}. Usa i byte grezzi del corpo della richiesta esattamente come ricevuti, prima di qualsiasi parsing JSON. - Calcola HMAC-SHA256 su quella stringa. La chiave è la porzione decodificata in base64 del segreto dopo il prefisso
whsec_(la derivazione della chiave Standard Webhooks, che è ciò che implementano le librerie di riferimento). - Codifica in base64 l'HMAC e confrontalo con ogni firma
v1,nell'header usando un confronto a tempo costante. La consegna è autentica se almeno una corrisponde. - Rifiuta le consegne il cui
webhook-timestampè distante più di 5 minuti dal tuo orario attuale, in entrambe le direzioni. Questo limita il replay di richieste catturate.
L'header può contenere più di una firma: mentre è aperta una finestra di grazia per la rotazione del segreto, le consegne sono firmate sia con il nuovo che con il precedente segreto (v1,NEW_SIG v1,OLD_SIG), quindi la verifica continua a funzionare indipendentemente dal segreto a cui il tuo servizio è passato.
import { createHmac, timingSafeEqual } from "node:crypto";
const TOLERANCE_SECONDS = 5 * 60;
/**
* @param {string} secret - The whsec_ signing secret from endpoint creation.
* @param {Record<string, string>} headers - Lowercased request headers.
* @param {string} rawBody - The raw request body, unparsed.
*/
export function verifyWebhook(secret, headers, rawBody) {
const id = headers["webhook-id"];
const timestamp = headers["webhook-timestamp"];
const signatureHeader = headers["webhook-signature"];
if (!id || !timestamp || !signatureHeader) return false;
const skew = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp));
if (!Number.isFinite(skew) || skew > TOLERANCE_SECONDS) return false;
const key = Buffer.from(secret.slice("whsec_".length), "base64");
const expected = createHmac("sha256", key)
.update(`${id}.${timestamp}.${rawBody}`)
.digest();
return signatureHeader.split(" ").some((entry) => {
const comma = entry.indexOf(",");
if (comma === -1 || entry.slice(0, comma) !== "v1") return false;
const received = Buffer.from(entry.slice(comma + 1), "base64");
return received.length === expected.length && timingSafeEqual(received, expected);
});
}Poiché il formato segue la specifica Standard Webhooks, le librerie di verifica open-source per altri linguaggi funzionano così come sono; passa loro il segreto e i tre header.
Tentativi e fallimenti
Il tuo endpoint ha 10 secondi per rispondere, inclusa la configurazione della connessione. Una risposta con uno stato 2xx conta come consegnata; qualsiasi altra cosa, inclusi timeout o reindirizzamenti (le consegne non seguono mai i reindirizzamenti), conta come fallimento e viene ritentata. L'unica eccezione è 410 Gone: ci dice che il percorso è stato rimosso intenzionalmente, quindi quella consegna non viene ritentata e l'endpoint viene disabilitato immediatamente, con un evento endpoint.disabled (motivo gone) agli altri endpoint del workspace e un'email a ogni proprietario e amministratore. Un 410 in risposta a Send test o a un rinvio manuale fallisce solo quella consegna.
Rispondi 2xx non appena hai accettato l'evento in modo duraturo, e esegui l'elaborazione pesante in modo asincrono.
Una consegna fallita viene ritentata secondo questo programma:
| Tentativo | Ritardo dopo il fallimento precedente |
|---|---|
| 1 | immediato |
| 2 | 5 secondi |
| 3 | 5 minuti |
| 4 | 30 minuti |
| 5 | 2 ore |
| 6 | 5 ore |
| 7 | 10 ore |
| 8 | 10 ore |
Sono 8 tentativi in circa 27,5 ore, quindi un'interruzione di un giorno intero sul tuo lato non fa perdere eventi. Ogni ritardo ha fino al 20% di jitter casuale in entrambe le direzioni, il che previene burst di tentativi sincronizzati contro un endpoint in recupero. Se il tuo endpoint restituisce un header Retry-After, il ritardo richiesto viene rispettato entro limiti: è bloccato tra il ritardo programmato e il doppio del ritardo programmato. Le risposte con stato 429 e i tentativi scaduti vengono ritentati non prima di 60 secondi, indipendentemente da quanto presto cada lo slot programmato.
Disabilitazione automatica
Un endpoint che continua a fallire viene infine disabilitato piuttosto che essere martellato all'infinito:
- Quando le consegne falliscono da 30 minuti, viene generato un evento
delivery.failinguna volta (consegnato agli altri endpoint del workspace) e ogni proprietario e amministratore del workspace riceve una notifica in-app. Questo è l'avviso precoce; un breve problema che viene risolto con un successo entro quei 30 minuti non genera nulla. - Mentre la finestra di fallimento continua, l'avviso si intensifica: dopo 3 giorni e di nuovo dopo 4,5 giorni ogni proprietario e amministratore riceve una notifica in-app e un'email che indica il momento dopo il quale l'endpoint verrà disabilitato se le consegne continuano a fallire. Ogni avviso raggiunge ogni persona una volta per finestra di fallimento.
- Un evento consegnato con successo resetta la finestra di fallimento. Un evento di test no: dimostra che l'endpoint risponde, non che elabora gli eventi. Se era già stato inviato un avviso, viene generato un evento
delivery.recoverede i proprietari e gli amministratori ricevono una notifica in-app che l'endpoint si è ripristinato. - Dopo 5 giorni di fallimenti consecutivi, l'endpoint viene disabilitato: viene generato un evento
endpoint.disabled(di nuovo, agli altri endpoint), e ogni proprietario e amministratore del workspace riceve un'email.
La disabilitazione richiede una consegna fallita dopo l'avviso finale. Se non era stato inviato nulla all'endpoint al momento in cui l'avviso finale è stato emesso, il fallimento successivo genera un nuovo avviso finale e l'endpoint viene disabilitato solo se le consegne continuano a fallire 12 ore dopo.
Disattivare manualmente un endpoint genera endpoint.disabled con motivo manual agli altri endpoint del workspace. Un endpoint disabilitato non riceve ulteriori consegne e le sue azioni send-test e resend sono bloccate. Una volta che il tuo ricevitore è di nuovo funzionante, riabilita l'endpoint dalle impostazioni del workspace (clicca sul suo badge di stato): la riabilitazione cancella la cronologia dei fallimenti, quindi l'endpoint inizia una nuova finestra di 5 giorni, e viene generato un evento endpoint.enabled agli altri endpoint del workspace. Gli eventi che si verificano mentre un endpoint è disabilitato sono registrati nel suo registro delle consegne con lo stato dropped invece di essere inviati. Dopo la riabilitazione, usa Send test per confermare che l'endpoint è raggiungibile, poi Replay failed + dropped per recuperare tutto ciò che l'interruzione ha causato in un'unica azione.
Rinvio manuale e test
Il registro delle consegne nelle impostazioni del workspace mostra ogni tentativo di consegna con il suo codice di stato e la risposta. Da lì:
- Resend rinvia l'evento originale di una consegna passata (stesso
webhook-id, stesso contenuto JSON) come singolo tentativo. Non entra nel programma di tentativi e non conta mai verso la disabilitazione automatica, quindi è sicuro da usare durante il debug di un ricevitore instabile. Il rinvio porta ilwebhook-idoriginale, quindi un ricevitore che ha già elaborato l'evento prima di rispondere con un errore lo tratta come lo stesso messaggio. - Replay failed + dropped riproduce in blocco ogni consegna fallita definitivamente e dropped dell'endpoint, dalla più vecchia (le consegne ancora nel loro programma di tentativi automatici sono escluse, quindi un replay non può mai duplicare un tentativo che sarebbe comunque riuscito). Le consegne riprodotte usano il programma di tentativi completo e portano il
webhook-idoriginale: per il tuo servizio un replay è lo stesso messaggio di nuovo, quindi la gestione dell'idempotenza suwebhook-idrende il recupero sicuro indipendentemente dal fatto che l'originale sia mai arrivato. Un evento che un rinvio o un replay precedente ha già consegnato viene saltato, così come uno che è ancora in consegna. Scegli quanto indietro andare (le ultime 24 ore, gli ultimi 7 giorni o tutto ciò che è conservato), e la finestra di dialogo mostra in anteprima quanti eventi verranno inviati prima di iniziare; gli intervalli ampi vengono impaginati automaticamente, con il progresso mostrato man mano che procedono. I replay arrivano fuori ordine; ordina gli eventi recuperati in base alsequencedel payload. - Send test invia un evento
webhook.pingfirmato con un oggettodatavuoto esattamente a quell'endpoint, indipendentemente dalle sue sottoscrizioni agli eventi, e mostra l'esito (il codice di stato, o perché è fallito) non appena il tentativo termina. Usalo per confermare la raggiungibilità e per esercitare la verifica della firma end-to-end. Un evento di test non apre, avanza o resetta mai la finestra di fallimento.
Best practice
- Deduplica su
webhook-id. I tentativi automatici, i rinvii e i replay riutilizzano lo stesso id, quindi memorizzare gli id elaborati ti offre l'elaborazione esattamente-una-volta sopra la consegna almeno-una-volta. - Non trattare mai un payload come stato attuale. I payload sono leggeri e possono arrivare in ritardo; usa l'evento come trigger e leggi lo stato attuale dall'API.
- Non fare affidamento sull'ordinamento. Tentativi e consegne parallele significano che gli eventi possono arrivare fuori ordine.
- Rispondi 2xx prima del lavoro pesante. Accoda l'evento ed elaboralo in modo asincrono; un handler che fa il suo lavoro inline rischia di superare il timeout di 10 secondi e di essere ritentato, il che trasforma un evento in diversi tentativi duplicati.
- Ruota i segreti con una finestra di grazia. La rotazione dalle impostazioni del workspace mantiene valido il segreto precedente per una finestra che scegli, e le consegne sono firmate con entrambi i segreti durante essa, quindi un deploy rolling dei tuoi servizi non fa mai perdere una consegna. Un endpoint mantiene un segreto precedente, quindi una seconda rotazione con una finestra di grazia viene rifiutata finché la prima finestra non è terminata; una rotazione senza finestra di grazia è sempre accettata e revoca tutti i vecchi segreti immediatamente, che è la strada da seguire per un segreto trapelato.