Impara a gestire webhook in modo affidabile con firma, chiavi di idempotenza, protezione contro replay e un workflow veloce per il debug di errori segnalati dai clienti.

Quando qualcuno dice “i webhook non funzionano”, di solito intende una di tre cose: eventi mai arrivati, eventi arrivati due volte, o eventi arrivati in un ordine confuso. Dal loro punto di vista il sistema ha “perso” qualcosa. Dal tuo punto di vista il provider ha inviato l'evento, ma il tuo endpoint non l'ha accettato, non l'ha processato o non lo ha registrato come ti aspettavi.
I webhook vivono su Internet pubblico. Le richieste vengono ritardate, ritentate e talvolta consegnate fuori ordine. La maggior parte dei provider ritenta aggressivamente quando vede timeout o risposte non 2xx. Questo trasforma un piccolo intoppo (un database lento, un deploy, una breve interruzione) in duplicati e condizioni di race.
Log scadenti fanno sembrare il tutto casuale. Se non puoi dimostrare se una richiesta era autentica, non puoi agirci in sicurezza. Se non riesci a collegare il reclamo di un cliente a un tentativo di consegna specifico, finisci per indovinare.
La maggior parte dei failure reali rientra in pochi casi:
L'obiettivo pratico è semplice: accettare eventi reali una volta sola, rifiutare i falsi e lasciare una traccia chiara così puoi fare debug di una segnalazione cliente in pochi minuti.
Un webhook è solo una richiesta HTTP che un provider invia a un endpoint che esponi. Non la richiami come una API. Il mittente la spinge quando succede qualcosa, e il tuo compito è riceverla, rispondere rapidamente e processarla in sicurezza.
Una consegna tipica include un body della richiesta (spesso JSON) più header che ti aiutano a validare e tracciare ciò che hai ricevuto. Molti provider includono un timestamp, un tipo di evento (come invoice.paid) e un ID evento univoco che puoi memorizzare per rilevare duplicati.
La parte che sorprende i team: la consegna è quasi mai “esattamente una volta”. La maggior parte dei provider punta a “almeno una volta”, cioè lo stesso evento può arrivare più volte, a volte con minuti o ore di distanza.
I retry accadono per motivi banali: il tuo server è lento o va in timeout, restituisci un 500, la loro rete non vede il tuo 200, o il tuo endpoint è temporaneamente non disponibile durante deploy o picchi di traffico.
Un timeout è particolarmente insidioso. Il tuo server potrebbe ricevere la richiesta e anche finire di processarla, ma la risposta non arriva al mittente in tempo. Dal punto di vista del provider è fallita, quindi ritentano. Senza protezioni, processi lo stesso evento due volte.
Un buon modello mentale è trattare la richiesta HTTP come un “tentativo di consegna”, non come “l'evento”. L'evento è identificato dal suo ID. La tua elaborazione dovrebbe basarsi su quell'ID, non su quante volte il provider ti chiama.
La firma del webhook è il modo in cui il mittente dimostra che una richiesta è davvero partita da loro e non è stata modificata in transito. Senza firma, chiunque indovini la tua URL webhook può postare falsi eventi “pagamento riuscito” o “utente aggiornato”. Peggio ancora, un evento reale potrebbe essere alterato (importo, ID cliente, tipo evento) e sembrare comunque valido alla tua app.
Il pattern più comune è HMAC con un secret condiviso. Entrambe le parti conoscono lo stesso valore segreto. Il mittente prende il payload esatto (di solito il body grezzo), calcola un HMAC usando quel segreto e invia la firma insieme al payload. Il tuo compito è ricalcolare l'HMAC sugli stessi byte e verificare che le firme combacino.
I dati della firma sono solitamente posti in un header HTTP. Alcuni provider includono anche un timestamp lì così puoi aggiungere protezione contro replay. Meno comune è che la firma sia nel corpo JSON, il che è più rischioso perché i parser o la rserializzazione possono cambiare il formato e rompere la verifica.
Quando confronti le firme, non usare un confronto di stringhe normale. I confronti basici possono rivelare differenze temporali che aiutano un attaccante a indovinare la firma corretta dopo molti tentativi. Usa una funzione di confronto a tempo costante fornita dal tuo linguaggio o libreria crittografica e rifiuta al primo mismatch.
Se un cliente segnala “il vostro sistema ha accettato un evento che non abbiamo inviato”, parti dai controlli di firma. Se la verifica fallisce, probabilmente hai un mismatch di secret o stai hashando i byte sbagliati (per esempio JSON parsato invece del body grezzo). Se passa, puoi fidarti dell'identità del mittente e passare a deduping, ordinamento e retry.
La gestione affidabile dei webhook parte da una regola noiosa: verifica ciò che hai ricevuto, non ciò che vorresti aver ricevuto.
Cattura il body grezzo della richiesta esattamente com'è arrivato. Non parsare e rserializzare il JSON prima di controllare la firma. Piccole differenze (whitespace, ordine delle chiavi, unicode) cambiano i byte e possono far sembrare invalide firme genuine.
Poi ricostruisci l'esatta stringa che il provider si aspetta che tu firmi. Molti sistemi firmano una stringa come timestamp + "." + raw_body. Il timestamp non è decorazione. Serve a rifiutare richieste vecchie.
Calcola l'HMAC usando il secret condiviso e l'algoritmo richiesto (spesso SHA-256). Conserva il secret in uno store sicuro e trattalo come una password.
Infine, confronta il valore calcolato con l'header della firma usando un confronto a tempo costante. Se non coincide, ritorna un 4xx e fermati. Non “accettare comunque”.
Checklist rapida di implementazione:
Un cliente segnala “i webhook hanno smesso di funzionare” dopo che avete aggiunto middleware di parsing JSON. Vedi mismatch di firma, soprattutto su payload più grandi. La soluzione è solitamente verificare usando il body grezzo prima di qualsiasi parsing e loggare quale passo è fallito (per esempio “header firma mancante” vs “timestamp fuori finestra”). Quel dettaglio spesso riduce il tempo di debug da ore a minuti.
I provider ritentano perché la consegna non è garantita. Il tuo server potrebbe essere giù per un minuto, un hop di rete può perdere la richiesta, o il tuo handler va in timeout. Il provider presume “forse è andata a buon fine” e reinvia lo stesso evento.
Una chiave di idempotenza è il numero di ricevuta che usi per riconoscere un evento già processato. Non è una funzionalità di sicurezza e non sostituisce la verifica della firma. Non risolve nemmeno le condizioni di race a meno che non la memorizzi e la verifichi in modo sicuro sotto concorrenza.
Scegliere la chiave dipende da cosa ti fornisce il provider. Preferisci un valore che rimane stabile tra i retry:
Quando ricevi un webhook, scrivi la chiave nello storage prima usando una regola di unicità così solo una richiesta “vince”. Poi processa l'evento. Se vedi la stessa chiave di nuovo, ritorna successo senza rifare il lavoro.
Mantieni la ricevuta memorizzata piccola ma utile: la chiave, stato di processamento (ricevuto/processato/failed), timestamp (first seen/last seen) e un sommario minimo (tipo evento e ID dell'oggetto correlato). Molti team conservano le chiavi per 7–30 giorni così i retry tardivi e la maggior parte delle segnalazioni clienti sono coperti.
La protezione contro replay ferma un problema semplice ma caro: qualcuno cattura una richiesta webhook reale (con firma valida) e la invia di nuovo più tardi. Se il tuo handler tratta ogni consegna come nuova, quel replay può causare rimborsi duplicati, inviti utente ripetuti o cambi di stato ripetuti.
Un approccio comune è firmare non solo il payload ma anche un timestamp. Il tuo webhook include header come X-Signature e X-Timestamp. A ricezione, verifica la firma e controlla che il timestamp sia fresco dentro una finestra breve.
Il clock drift è ciò che solitamente causa falsi rifiuti. I tuoi server e quelli del mittente possono avere differenze di uno o due minuti, e le reti possono ritardare la consegna. Mantieni un buffer e logga perché hai rifiutato una richiesta.
Regole pratiche che funzionano bene:
abs(now - timestamp) <= window (per esempio 5 minuti più una piccola tolleranza).Se mancano i timestamp, non puoi fare vera protezione replay basata solo sul tempo. In quel caso, punta di più sull'idempotenza (memorizza e rifiuta ID evento duplicati) e considera di richiedere timestamp nella prossima versione del webhook.
La rotazione dei secret è importante. Se ruoti i secret di firma, conserva più secret attivi per un breve periodo di sovrapposizione. Verifica contro il secret più recente prima, poi fai fallback a quelli più vecchi. Questo evita rotture durante il rollout. Se il tuo team rilascia endpoint velocemente (per esempio generando codice con Koder.ai e usando snapshot e rollback durante i deploy), quella finestra di sovrapposizione aiuta perché versioni più vecchie potrebbero restare live per un po'.
I retry sono normali. Presumi che ogni consegna possa essere duplicata, ritardata o fuori ordine. Il tuo handler dovrebbe comportarsi allo stesso modo sia che veda un evento una volta sia cinque volte.
Mantieni il percorso della richiesta corto. Fai solo ciò che serve per accettare l'evento, poi sposta il lavoro pesante in un job di background.
Un pattern semplice che regge in produzione:
Restituisci 2xx solo dopo aver verificato la firma e registrato l'evento (o messo in coda). Se rispondi 200 prima di salvare qualcosa, puoi perdere eventi in caso di crash. Se fai lavoro pesante prima di rispondere, i timeout triggerano retry e potresti ripetere side effect.
Sistemi downstream lenti sono la ragione principale per cui i retry diventano dolorosi. Se il tuo provider email, CRM o database è lento, lascia che una coda assorba il ritardo. Il worker potrà ritentare con backoff e potrai allertare sui job bloccati senza bloccare il mittente.
Gli eventi fuori ordine accadono anch'essi. Per esempio, un subscription.updated potrebbe arrivare prima di subscription.created. Costruisci tolleranza controllando lo stato corrente prima di applicare cambiamenti, permettendo upsert e trattando il “not found” come motivo per riprovare più tardi (quando ha senso) invece che come fallimento permanente.
Molti problemi “casuali” sui webhook sono autoinflitti. Sembrano reti fluttuanti, ma si ripetono in pattern, spesso dopo un deploy, una rotazione di secret o una piccola modifica al parsing.
Il bug di firma più comune è hashare i byte sbagliati. Se parsate il JSON prima, il server può riformattarlo (whitespace, ordine chiavi, formattazione numeri). Poi verifichi la firma su un body diverso da quello che il mittente ha firmato, e la verifica fallisce anche se il payload è genuino. Verifica sempre contro i byte grezzi esatti della richiesta così come ricevuti.
La fonte successiva di confusione sono i secret. I team testano in staging ma per sbaglio verificano con il secret di produzione, o tengono un secret vecchio dopo la rotazione. Quando un cliente segnala fallimenti “solo in un ambiente”, assumi prima secret sbagliato o config errata.
Alcuni errori che portano a lunghe indagini:
Esempio: un cliente dice “order.paid non è mai arrivato”. Vedi che i fallimenti di firma sono iniziati dopo un refactor che ha cambiato il middleware di parsing delle richieste. Il middleware legge e riformatta il JSON, quindi il controllo firma usa ora un body modificato. La soluzione è semplice, ma la trovi solo se sai dove cercare.
Quando un cliente dice “il vostro webhook non è scattato”, trattalo come un problema di trace, non come un problema di indovinare. Ancora su un singolo tentativo di consegna dal provider e seguilo nel sistema.
Inizia ottenendo l'identificatore di consegna del provider, request ID o event ID per il tentativo fallito. Con quell'unico valore dovresti poter trovare la voce di log corrispondente rapidamente.
Da lì, controlla tre cose in ordine:
Poi conferma cosa hai restituito al provider. Un 200 lento può essere tanto dannoso quanto un 500 se il provider va in timeout e ritenta. Guarda codice di stato, tempo di risposta e se il tuo handler ha riconosciuto prima di fare lavoro pesante.
Se devi riprodurre, fallo in modo sicuro: memorizza un campione raw della richiesta redatto (header chiave più body grezzo) e riproducilo in un ambiente di test usando lo stesso secret e lo stesso codice di verifica.
Quando un'integrazione webhook comincia a fallare “a caso”, la velocità conta più della perfezione. Questo runbook cattura le cause più comuni.
Prendi prima un esempio concreto: nome del provider, tipo evento, timestamp approssimativo (con timezone) e qualsiasi event ID che il cliente possa vedere.
Poi verifica:
Se il provider dice “abbiamo ritentato 20 volte”, controlla prima pattern comuni: secret sbagliato (firma fallisce), drift orologio (finestra replay), limiti di dimensione payload (413), timeout (nessuna risposta) e picchi di 5xx dalle dipendenze downstream.
Un cliente scrive: “Ci siamo persi un evento invoice.paid ieri. Il nostro sistema non si è aggiornato.” Ecco un modo veloce per tracciarlo.
Per prima cosa, conferma se il provider ha tentato la consegna. Estrai event ID, timestamp, URL di destinazione e l'esatto codice di risposta che il tuo endpoint ha restituito. Se ci sono stati retry, annota la prima ragione di fallimento e se un retry successivo è riuscito.
Poi valida cosa ha visto il tuo codice al bordo: conferma il secret di firma configurato per quell'endpoint, ricalcola la verifica della firma usando il body grezzo e controlla il timestamp della richiesta rispetto alla tua finestra consentita.
Fai attenzione alle finestre di replay durante i retry. Se la tua finestra è di 5 minuti e il provider ritenta 30 minuti dopo, potresti rifiutare un retry legittimo. Se quella è la tua policy, assicurati sia intenzionale e documentata. Se non lo è, amplia la finestra o cambia la logica in modo che l'idempotenza rimanga la difesa primaria contro i duplicati.
Se firma e timestamp sono a posto, segui l'event ID nel tuo sistema e rispondi: l'avete processato, deduplicato o scartato?
Esiti comuni:
Quando rispondi al cliente, sii conciso e specifico: “Abbiamo ricevuto tentativi di consegna alle 10:03 e 10:33 UTC. Il primo è andato in timeout dopo 10s; il retry è stato rifiutato perché il timestamp era fuori dalla nostra finestra di 5 minuti. Abbiamo ampliato la finestra e aggiunto un riconoscimento più veloce. Invia di nuovo l'event ID X se necessario.”
Il modo più rapido per fermare i problemi con i webhook è far sì che ogni integrazione segua lo stesso playbook. Scrivi il contratto che tu e il mittente concordate: header richiesti, metodo di firma esatto, quale timestamp usare e quali ID trattare come unici.
Standardizza poi ciò che registri per ogni tentativo di consegna. Un piccolo log di ricevuta di solito è sufficiente: received_at, event_id, delivery_id, signature_valid, idempotency_result (new/duplicate), handler_version e response status.
Un workflow che resta utile man mano che cresci:
Se costruisci app su Koder.ai (Koder.ai), Planning Mode è un buon modo per definire prima il contratto webhook (header, firma, ID, comportamento dei retry) e poi generare un endpoint consistente e un record di ricevuta attraverso i progetti. Quella coerenza è ciò che rende il debug veloce invece che eroico.
Perché la consegna dei webhook è solitamente at-least-once, non exactly-once. I provider ritentano su timeout, risposte 5xx o quando non vedono il tuo 2xx in tempo, quindi puoi avere duplicati, ritardi e consegne fuori ordine anche quando tutto sembra funzionare.
Di base segui questa regola: verifica prima la firma, poi registra/dedupe l'evento, quindi rispondi 2xx, infine esegui il lavoro pesante in modo asincrono.
Se fai lavori pesanti prima della risposta, incappi in timeout e triggeri retry; se rispondi prima di registrare tutto, rischi di perdere eventi in caso di crash.
Usa esattamente i byte grezzi del body della richiesta così come sono arrivati. Non parsare il JSON e poi rserializzarlo prima della verifica: spazi, ordine delle chiavi e formattazione dei numeri possono cambiare la firma.
Assicurati inoltre di ricreare esattamente la stringa che il provider firma (spesso timestamp + "." + raw_body).
Restituisci un 4xx (comune 400 o 401) e non processare il payload.
Logga una ragione minimale (header firma mancante, mismatch, timestamp fuori finestra), ma non registrare segreti né payload sensibili completi.
Una chiave di idempotenza è un identificatore stabile e unico che memorizzi così i retry non riapplichino side effect.
Opzioni migliori:
Applicala con un vincolo di così solo una richiesta “vince” sotto concorrenza.
Scrivi la chiave di idempotenza prima di effettuare side effect, con una regola di unicità. Poi:
Se l'inserimento fallisce perché la chiave esiste già, ritorna 2xx e salta l'azione business.
Includi il timestamp nei dati firmati e rifiuta le richieste fuori da una breve finestra (ad esempio pochi minuti).
Per evitare di bloccare retry legittimi:
Non presumere che l'ordine di consegna corrisponda all'ordine degli eventi. Rendi i handler tolleranti:
Memorizza event ID e tipo così puoi ricostruire cosa è successo anche con ordini strani.
Registra una piccola “ricevuta” per ogni tentativo di consegna così puoi tracciare un evento end-to-end:
Rendi i log ricercabili per event ID così il supporto può rispondere rapidamente alle segnalazioni dei clienti.
Chiedi prima un identificatore concreto: event ID o delivery ID, più un timestamp approssimativo.
Poi verifica in ordine:
Se usi Koder.ai, mantieni lo stesso pattern handler (verify → record/dedupe → queue → respond). La coerenza rende queste verifiche rapide durante gli incidenti.