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:

EventoSignificato
sentEmail accettata dal server di posta destinatario
deliveredIl server di posta ha confermato la consegna
openedIl destinatario ha aperto l'email (tracciamento pixel)
clickedIl destinatario ha cliccato un link
bouncedIl server di posta ha rifiutato la consegna
complainedIl destinatario ha segnalato il messaggio come spam
unsubscribedIl destinatario ha cliccato per disiscriversi
failedL'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 che GET /v1/emails/{id} restituisce come failureReason. È 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.
reasonPerché l'email non è partitaDefinitivo
interruptedLa 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 interrottaNo
tenant_not_activeIl workspace è sospeso o eliminato: l'invio è bloccatoSì
sender_domain_unverifiedIl dominio mittente non è verificato al momento dell'invioSì
template_not_foundIl templateId dell'email non esisteSì
template_not_publishedIl template è una bozza. Vedi TemplateSì
template_emptyIl template non ha contenutoSì
provider_rejectedIl provider di posta ha rifiutato il messaggio. data.error riporta la sua spiegazioneSì
attachment_unavailableUn allegato è stato eliminato o è scaduto prima dell'invio. La quota dell'email viene restituita. Vedi AllegatiSì

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_abc123

La 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 richiesta10 secondi
Successoqualsiasi risposta 2xx
Tentativifino a 5
Backoff5s, 30s, 5min, 30min, 2h
Disattivazione automaticadopo 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 id dell'evento. Due eventi bounced con lo stesso emailId ma id diversi 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


Modifica questa pagina su GitHub

Ultimo aggiornamento