Webhook: Setup, Eventi e Verifica Firma
Ricevi gli eventi email in tempo reale tramite webhook
I webhook sono callback HTTP che avvisano la tua applicazione quando accade qualcosa (email consegnata, aperta, respinta, e così via). Invece di interrogare l'API, è Tratto a inviarti gli eventi.
Nella dashboard solo il proprietario del workspace gestisce i webhook: un membro riceve 403 FORBIDDEN (Only the workspace owner can do this.). Vedi Team.
Piani che includono i webhook
I webhook sono inclusi nei piani Starter, Growth e Scale (richiede l'API 0.23.0 o successiva). Conta il piano del workspace su cui agisce la richiesta (copia del piano del proprietario), sia che chiami con una chiave API (live o test) sia da una sessione della dashboard.
Su un workspace con piano Free, le rotte che fanno consegnare un webhook
rispondono 403 PLAN_UPGRADE_REQUIRED:
POST /v1/webhooks(registrare un webhook)POST /v1/webhooks/{id}/test(inviare una consegna di prova)POST /v1/webhooks/{id}/rotate-secret(ruotare il segreto di firma)
{
"error": {
"code": "PLAN_UPGRADE_REQUIRED",
"message": "This feature is not included in your plan. Upgrade to use it.",
"docs": "https://docs.tratto.email/en/docs/error-codes",
"suggestion": "Webhooks are available from the Starter plan. The account owner can upgrade at https://app.tratto.email/settings/billing."
}
}Queste rotte restano disponibili sul Free, così puoi ancora vedere e ripulire quello che hai:
GET /v1/webhooks(elenco)DELETE /v1/webhooks/{id}(eliminazione)GET /v1/webhooks/{id}/deliveries(storico delle consegne)
I webhook attivi che esistono già su un workspace Free continuano a ricevere
gli eventi. Uno disattivato per fallimenti ripetuti non si riattiva sul Free,
perché serve una rotazione del segreto. Per sapere se il tuo piano include i webhook, leggi limits.webhooks
(true o false) da GET /v1/workspace: vedi
Leggi i tuoi limiti dall'API.
Registrare un Webhook
cURL
curl -X POST https://api.tratto.email/v1/webhooks \
-H "Authorization: Bearer tratto_live_..." \
-H "Content-Type: application/json" \
-d '{
"url":"https://yourapi.com/webhooks/tratto",
"events":["sent","delivered","opened","clicked","bounced","complained","unsubscribed","failed"]
}'Risposta: 201 Created:
{
"data": {
"id": "wh_abc123",
"secret": "whsec_..."
}
}Conserva il secret. Viene restituito solo qui e alla rotazione: non esiste
un endpoint che te lo ridia. L'elenco dei webhook restituisce un secretPrefix
troncato per identificarli, non il valore completo.
Eventi Webhook
Tratto invia richieste POST al tuo URL quando si verificano questi eventi:
| Evento | Significato |
|---|---|
sent | Email accettata dal server di posta destinatario |
delivered | Il server di posta ha confermato la consegna |
opened | Il destinatario ha aperto l'email (tracciamento pixel) |
clicked | Il destinatario ha cliccato un link |
bounced | Il server di posta ha rifiutato la consegna |
complained | Il destinatario ha segnalato il messaggio come spam |
unsubscribed | Il destinatario ha cliccato per disiscriversi |
failed | L'email non è stata inviata. Vedi Evento failed (richiede l'API 0.29.0 o successiva) |
Sottoscrivere un evento fuori da questa lista viene rifiutato in fase di
registrazione. Un webhook riceve solo gli eventi elencati nel suo events:
non esiste un carattere jolly.
Payload del Webhook
{
"id": "evt_abc123",
"type": "delivered",
"emailId": "email_xyz789",
"recipient": "user@example.com",
"occurredAt": "2025-06-30T12:00:10Z",
"data": {},
"livemode": true
}data contiene i dettagli specifici dell'evento ed è un oggetto vuoto quando non
ce ne sono. Usa id per deduplicare: i retry consegnano lo stesso evento più di
una volta.
livemode è false per gli eventi generati dalle chiavi di test e
dall'endpoint di consegna di prova manuale. Usalo per tenere il traffico di
test fuori dai tuoi handler di produzione (vedi Modalità test).
recipient è l'indirizzo a cui si riferisce l'evento. Per bounced e
complained è l'indirizzo segnalato dal server di posta del destinatario, e la
differenza si vede quando la stessa email parte verso più indirizzi (to, cc,
bcc): in quel caso puoi ricevere più eventi bounced o complained, uno per
ogni indirizzo coinvolto, ciascuno con il suo id. Una sola segnalazione di
spam può riguardare più destinatari dello stesso dominio: ricevi un evento
complained per ognuno e ogni contatto corrispondente viene messo in
soppressione. delivered resta sempre un solo evento per email.
Evento failed
Richiede l'API 0.29.0 o successiva.
failed dice che un'email non è stata inviata: Tratto l'ha fermata prima
o durante il passaggio al provider di posta. È l'evento dietro lo
status failed. C'è un solo
evento failed per email, e il suo recipient è il primo indirizzo in to.
{
"id": "evt_abc123",
"type": "failed",
"emailId": "email_xyz789",
"recipient": "user@example.com",
"occurredAt": "2026-11-02T09:00:01Z",
"data": {
"reason": "template_not_published",
"error": "Template 'tmpl_abc123' is not published and cannot be used to send."
},
"livemode": true
}data.reasonè un codice stabile, lo stesso cheGET /v1/emails/{id}restituisce comefailureReason. È il campo su cui decidere.data.errorè una frase per le persone. Può contenere testo del provider di posta: non analizzarla e non mostrarla così com'è ai tuoi utenti.
reason | Perché l'email non è partita | Definitivo |
|---|---|---|
interrupted | La consegna si è interrotta mentre l'email passava al provider di posta, quindi Tratto non può sapere se il provider l'ha accettata. Non viene ritentata, per non inviarla due volte. Vedi Quando la consegna viene interrotta | No |
tenant_not_active | Il workspace è sospeso o eliminato: l'invio è bloccato | Sì |
sender_domain_unverified | Il dominio mittente non è verificato al momento dell'invio | Sì |
template_not_found | Il templateId dell'email non esiste | Sì |
template_not_published | Il template è una bozza. Vedi Template | Sì |
template_empty | Il template non ha contenuto | Sì |
provider_rejected | Il provider di posta ha rifiutato il messaggio. data.error riporta la sua spiegazione | Sì |
attachment_unavailable | Un allegato è stato eliminato o è scaduto prima dell'invio. La quota dell'email viene restituita. Vedi Allegati | Sì |
L'elenco dei codici è aperto. Cause nuove aggiungono codici nuovi, e i codici esistenti non vengono mai rinominati: tratta un codice che non conosci come un fallimento senza un rimedio specifico, mai come un errore del tuo handler.
interrupted è l'unico non definitivo. L'email potrebbe essere arrivata
comunque: se in seguito il provider la segnala, segue un evento delivered,
bounced o complained e lo status cambia. Ogni altro failed è definitivo.
I webhook esistenti non lo ricevono. Un webhook riceve failed solo se
failed è nel suo events. Non esiste un endpoint che cambi gli eventi di un
webhook: per iniziare a ricevere failed, registra un webhook che lo elenca
(riceve un secret nuovo) ed elimina quello vecchio.
Verifica Firma Webhook
Ogni consegna porta un header X-Tratto-Signature. Non è un hash nudo:
contiene un timestamp e la firma.
X-Tratto-Signature: t=1784977714054,v1=0b37dd97144dcdf176572a1b...
X-Tratto-Webhook-Id: wh_abc123La stringa firmata è il timestamp e il corpo grezzo uniti da un punto:
{timestamp}.{rawBody}La verifica ha quindi quattro passi: estrarre t e v1, rifiutare i timestamp
fuori dalla finestra di tolleranza, ricalcolare l'HMAC su {t}.{rawBody} e
confrontare con v1 a tempo costante.
Verifica della Firma dei Webhook contiene le implementazioni funzionanti per Node, runtime edge, Python e PHP.
Firmare il solo corpo, o confrontare direttamente l'header con un digest, non corrisponderà mai. Sono i due errori che vediamo più spesso.
Prevenire i Replay Attack
Usa il campo t dell'header della firma, non occurredAt del payload. t è
coperto dalla firma e registra quando la consegna è stata firmata; occurredAt
registra quando è avvenuto l'evento, e i due valori divergono nei retry.
Il controllo di tolleranza è già incluso nelle implementazioni in Verifica della Firma dei Webhook.
Consegne e Retry
| Timeout della richiesta | 10 secondi |
| Successo | qualsiasi risposta 2xx |
| Tentativi | fino a 5 |
| Backoff | 5s, 30s, 5min, 30min, 2h |
| Disattivazione automatica | dopo 10 fallimenti consecutivi |
Due conseguenze da tenere presenti:
- Conferma prima di elaborare. Tutto ciò che supera i 10 secondi conta come fallimento e verrà ritentato.
- Gestisci i duplicati. Deduplica sul campo
iddell'evento. Due eventibouncedcon lo stessoemailIdmaiddiversi non sono duplicati: riguardano destinatari diversi.
Un webhook disattivato per fallimenti ripetuti ha status: "disabled". Ruotarne
il segreto azzera il contatore dei fallimenti e lo riattiva; sul piano Free la
rotazione non è disponibile (vedi Piani che includono i webhook),
quindi un webhook disattivato resta disattivato.
Ruotare il Secret
curl -X POST https://api.tratto.email/v1/webhooks/wh_abc123/rotate-secret \
-H "Authorization: Bearer tratto_live_..."Risposta:
{
"data": {
"secret": "whsec_new..."
}
}La rotazione ha effetto immediato: non esiste una finestra di sovrapposizione e il secret precedente smette di funzionare appena viene emesso quello nuovo. Per ruotare senza perdere consegne, pubblica prima un endpoint che accetti entrambi i secret, poi ruota, poi rimuovi il vecchio. Vedi Rotazione del secret.
Evento di Test
curl -X POST https://api.tratto.email/v1/webhooks/wh_abc123/test \
-H "Authorization: Bearer tratto_live_..."Risposta:
{
"data": {
"queued": true
}
}Il test viene messo in coda, non consegnato in modo sincrono: un 200 qui
significa che è stato accettato per l'invio, non che il tuo endpoint ha risposto.
Controlla le consegne per l'esito.
Il test invia un evento delivered il cui data è { "isTest": true }, così il
tuo handler può distinguerlo dal traffico reale.
Elenco dei Webhook
curl https://api.tratto.email/v1/webhooks \
-H "Authorization: Bearer tratto_live_..."Risposta:
{
"data": [
{
"id": "wh_abc123",
"url": "https://yourapi.com/webhooks/tratto",
"events": ["delivered", "bounced"],
"status": "active",
"secretPrefix": "whsec_5f3a9c…",
"failureCount": 0,
"createdAt": "2025-06-30T12:00:00Z"
}
]
}failureCount conta i fallimenti consecutivi. Tienilo d'occhio: a 10 il webhook
viene disattivato.
Consegne del Webhook
curl "https://api.tratto.email/v1/webhooks/wh_abc123/deliveries?limit=50" \
-H "Authorization: Bearer tratto_live_..."Paginazione a cursore con after e limit (default 50, massimo 100).
Risposta:
{
"data": [
{
"id": "del_001",
"webhookId": "wh_abc123",
"eventType": "delivered",
"status": "success",
"httpStatus": 200,
"responseBody": "{\"ok\":true}",
"retryCount": 0,
"attemptedAt": "2025-06-30T12:00:10Z"
},
{
"id": "del_002",
"webhookId": "wh_abc123",
"eventType": "bounced",
"status": "scheduled",
"httpStatus": 500,
"responseBody": "Internal Server Error",
"retryCount": 2,
"attemptedAt": "2025-06-30T12:05:00Z"
}
],
"pagination": { "hasMore": false, "nextCursor": null }
}status vale success, failed o scheduled: scheduled significa che un
retry è ancora in attesa. responseBody è troncato ai primi 1000 caratteri della
tua risposta, ed è il modo più rapido per capire perché una consegna è stata
rifiutata.
Eliminare un Webhook
curl -X DELETE https://api.tratto.email/v1/webhooks/wh_abc123 \
-H "Authorization: Bearer tratto_live_..."Restituisce 204 No Content. L'eliminazione è immediata e non reversibile; le
consegne in coda per quel webhook si interrompono.
Prossimi Passi
- Inviare email? Vai a Invia Email
- Tracciare lo stato? Vedi Status Email
- Ricevere webhook su Firebase, Vercel o Cloudflare? Vedi Deployment
- Usare i webhook nei flow? Vedi Flow
Modifica questa pagina su GitHub
Ultimo aggiornamento