Sign inGet Started

Webhook ed eventi

Quando qualcosa accade nel tuo spazio di lavoro (un'email viene consegnata, un destinatario genera un bounce, un messaggio WhatsApp viene letto), Bird invia tramite POST un evento JSON firmato a ogni endpoint webhook iscritto a quel tipo di evento. Bird segue la specifica Standard Webhooks per header, firma e struttura del payload, quindi se verifichi già webhook da un'altra piattaforma Standard Webhooks, lo stesso codice di verifica funziona qui senza modifiche.
Per una panoramica sugli endpoint webhook e sulla consegna, consulta Cos'è un webhook?.

Creare un endpoint

Registra un endpoint nella dashboard sotto Developers > Webhooks, oppure dal terminale con il bird CLI:
Esempio di codice
bird webhooks create https://example.com/webhooks/bird \
  --events email.delivered,email.bounced,email.complained \
  --description "Production delivery + bounce notifications"
La gestione degli endpoint richiede lo scope webhooks. Le sessioni nella dashboard e il login del CLI lo ereditano dal tuo ruolo utente, e anche le chiavi API possono includerlo: concedi webhooks:read per ispezionare endpoint e tentativi di consegna, oppure webhooks:write per gestirli. Le operazioni sottostanti partono da POST /v1/webhooks.
La pagina Webhooks nella dashboard Bird, con un endpoint attivo e i relativi eventi sottoscritti
Gli URL degli endpoint devono essere HTTPS, lunghi al massimo 2048 caratteri e raggiungibili pubblicamente. URL su indirizzi privati, di loopback, link-local o comunque interni vengono rifiutati con un 422 quando crei o aggiorni l'endpoint. Le consegne partono dall'infrastruttura di consegna di Bird, esterna alla tua rete.
L'array events elenca fino a 100 tipi dal catalogo eventi. Un endpoint riceve solo i tipi che elenca. Usa PATCH /v1/webhooks/{webhook_id} per sostituire l'intera lista per le consegne future. Per ricevere ogni evento, iscriviti a ogni tipo: un tipo fuori dal catalogo viene rifiutato con un 422, e questo include un carattere jolly come sms.*. Le iscrizioni esistenti non si espandono quando diventano disponibili nuovi tipi.
La risposta di creazione include il secret di firma dell'endpoint (con prefisso whsec_) una sola volta. Salvalo subito nel tuo secret manager; non può essere recuperato di nuovo, e se lo perdi, esegui la rotazione.
Esempio di codice
{
  "id": "whk_01ky7q639cfh99xb7ysy2x2gzj",
  "url": "https://example.com/webhooks/bird",
  "events": ["email.delivered", "email.bounced", "email.complained"],
  "description": "Production delivery + bounce notifications",
  "status": "active",
  "secret": "whsec_c+dgRBsFELJ9mR4tu2cyhK0gTMMkqntv",
  "created_at": "2026-07-23T14:48:29.740Z",
  "updated_at": "2026-07-23T14:48:29.740Z"
}
Gli endpoint supportano il CRUD completo: list, get, update e delete. Eliminare un endpoint interrompe tutte le consegne verso di esso, incluse le ripetizioni di consegne precedentemente fallite, e non può essere annullato; per interrompere le consegne temporaneamente, imposta status a paused. Uno spazio di lavoro può registrare più endpoint, ciascuno con il proprio URL, filtro eventi e secret.

Verificare le firme

Ogni consegna include tre header:
HeaderValore
webhook-idIdentifica la consegna dell'evento. Ripetizioni e replay riutilizzano lo stesso valore.
webhook-timestampTimestamp Unix (secondi) di questo tentativo di consegna
webhook-signaturev1,<base64 HMAC-SHA256>, eventualmente più firme separate da spazi
La firma è un HMAC-SHA256 sulla stringa {webhook-id}.{webhook-timestamp}.{raw request body}, con chiave il secret dell'endpoint (rimuovi il prefisso whsec_ e decodifica in base64 il resto per ottenere i byte della chiave). Il tuo handler deve verificare la firma, rifiutare le consegne il cui webhook-timestamp è più vecchio di 5 minuti e deduplicare su webhook-id: Bird consegna at-least-once, quindi la stessa consegna può arrivare più di una volta.
Con la Bird SDK, i controlli di firma e timestamp si riducono a una chiamata; la deduplicazione resta nel tuo handler:
// Pass the RAW request body; set the secret via new BirdClient({ webhooks: { secret } }).
const event = bird.webhooks.unwrap(rawBody, headers);
console.log(event.type); // discriminated union: narrow on event.type
Rifiutare una consegna con 400, come fanno gli esempi precedenti, non scarta l'evento: lo riproviamo secondo la programmazione sotto. È intenzionale, ed è ciò che vuoi. La causa più comune di una verifica fallita è un secret che il tuo handler non ha ancora, durante una rotazione o un deploy errato, quindi la finestra di ripetizione è la tua occasione per correggere il secret e ricevere comunque l'evento. Restituisci 2xx solo quando intendi scartare la consegna definitivamente.
Funziona anche qualsiasi libreria di riferimento Standard Webhooks. Se verifichi manualmente, la procedura è:
Esempio di codice
import { createHmac, timingSafeEqual } from "node:crypto";

function verify(rawBody: string, headers: Record<string, string>, secret: string): boolean {
  const id = headers["webhook-id"];
  const timestamp = headers["webhook-timestamp"];
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false; // 5-minute tolerance

  const key = Buffer.from(secret.slice("whsec_".length), "base64");
  const expected = createHmac("sha256", key)
    .update(`${id}.${timestamp}.${rawBody}`)
    .digest("base64");

  // During secret rotation, the header can contain several signatures. Accept any match.
  return headers["webhook-signature"].split(" ").some((part) => {
    const sig = Buffer.from(part.replace(/^v1,/, ""), "base64");
    return (
      sig.length === Buffer.byteLength(expected, "base64") &&
      timingSafeEqual(sig, Buffer.from(expected, "base64"))
    );
  });
}
Calcola sempre l'HMAC sui byte grezzi del corpo della richiesta. Parsare e riserializzare il JSON cambia spazi bianchi o ordine delle chiavi e rompe la firma.

Semantica di consegna

Ogni consegna è un evento per HTTP POST con Content-Type: application/json, senza batching. Il tuo endpoint ha 15 secondi per rispondere; qualsiasi status 2xx conta come successo, e qualsiasi altra cosa (inclusi redirect 3xx e timeout) conta come fallimento. Ogni fallimento segue la stessa programmazione di ripetizione. Lo status che restituisci cambia ciò che vedi nel log dei tentativi di consegna, non se riproviamo: non esiste uno status code che interrompa la consegna anticipatamente. Rispondi rapidamente e processa in modo asincrono: accoda l'evento e restituisci 200 prima di eseguire il lavoro effettivo.
Dopo il primo tentativo, le consegne fallite vengono riprovate secondo questa programmazione, con jitter di ±20 % per evitare che le ripetizioni si sincronizzino:
RipetizioneRitardo dopo il tentativo precedente
15 secondi
25 minuti
330 minuti
42 ore
55 ore
610 ore
710 ore
In tutto sono otto tentativi in circa 27,5 ore. Un 429 o un timeout alza a 60 secondi qualsiasi ritardo programmato inferiore a 60 secondi, il che in pratica incide solo sul primo nuovo tentativo: dopo il jitter, arriva da 48 a 72 secondi dopo. Un header Retry-After su una risposta fallita può allungare l'attesa successiva. L'header è accettato come delay-seconds o come data HTTP. Un ritardo richiesto più lungo di quello programmato lo sostituisce, con un tetto pari al doppio del ritardo programmato (dopo l'eventuale innalzamento a 60 secondi); un ritardo più breve viene ignorato, quindi l'header non anticipa mai un nuovo tentativo. Il jitter si aggiunge a tutto il resto. Ogni nuovo tentativo porta lo stesso webhook-id, ed è questo che fa funzionare la deduplicazione. Dopo l'ultimo tentativo la consegna è permanentemente fallita; il replay la recupera.
Le consegne non sono ordinate. Un email.delivered può arrivare prima del email.accepted per lo stesso messaggio, specialmente quando sono coinvolte ripetizioni. Ordina in base al campo timestamp nel payload dell'evento, mai in base all'ordine di arrivo.

Gestire gli endpoint

Invii di test

POST /v1/webhooks/{webhook_id}/test invia un evento sintetico firmato al tuo endpoint e restituisce il risultato in modo sincrono: se il tuo endpoint lo ha accettato, lo status HTTP che ha restituito e la latenza round-trip. Il corpo del test è uno stub JSON minimale che contiene solo il type dell'evento, firmato esattamente come una consegna reale; non rispecchia un payload di evento reale. Passa {"event_type": "email.delivered"} per scegliere qualsiasi tipo dal catalogo, sottoscritto o meno, oppure ometti il corpo per usare il primo tipo di evento sottoscritto dall'endpoint.
Il tuo endpoint ha 10 secondi per rispondere. Un endpoint non raggiungibile produce status: failed nel corpo della risposta, mentre la richiesta stessa ha successo. Usa questo risultato per fare debug della connettività. Gli invii di test vanno direttamente al tuo endpoint: funzionano su un endpoint in pausa e non vengono registrati nel log dei tentativi di consegna. Un 412 significa che l'endpoint non può ancora essere testato perché manca un secret di firma valido o un tipo di evento sottoscritto.
Per test end-to-end con flussi di eventi reali, invia agli indirizzi sandbox: gli invii sandbox emettono eventi webhook reali attraverso il normale percorso di consegna, il modo migliore per testare il tuo handler prima di andare in produzione.

Replay delle consegne fallite

POST /v1/webhooks/{webhook_id}/replay accoda la riconsegna delle consegne fallite. Gli eventi che l'endpoint ha già ricevuto con successo vengono saltati, quindi un replay non consegna mai due volte; un evento riconsegnato porta il suo webhook-id originale, quindi il tuo controllo di deduplicazione copre anche i replay. Solo i tentativi falliti vengono riprodotti: un evento che il tuo endpoint non ha mai ricevuto non ha un tentativo fallito, quindi un replay non lo recupera.
Passa i timestamp since/until per delimitare la finestra (predefinito: le ultime 24 ore fino al momento della richiesta). Entrambi i limiti sono inclusivi ed entrambi selezionano in base a quando la consegna è stata tentata, non a quando l'evento si è verificato, quindi un ritentativo arrivato un giorno dopo il suo evento rientra nella finestra in base all'ora in cui è stato tentato. Il replay legge il log dei tentativi di consegna, che conserva tre giorni, perciò quella è la cronologia più vecchia che raggiunge: un since anteriore allarga la finestra senza recuperare nulla di più vecchio. Un singolo replay copre al massimo i 10.000 eventi più vecchi nella finestra.
La richiesta restituisce 202 e gli eventi vengono riconsegnati in modo asincrono. Una riconsegna ha un solo tentativo, non la programmazione di ripetizione sopra. Il tentativo viene registrato e il lavoro è terminato indipendentemente dal fatto che il tuo endpoint lo abbia accettato, quindi un replay verso un endpoint ancora guasto costa una richiesta per evento anziché otto; correggi l'endpoint e ripeti il replay. Questi fallimenti non influiscono sullo stato di salute dell'endpoint: un replay non può portare un endpoint a degraded né metterlo in pausa automatica. Una riconsegna accettata dal tuo endpoint li ripristina entrambi.
Esegui il replay di un endpoint paused e la richiesta restituisce comunque 202, ma nulla viene riconsegnato. Riabilitalo prima, come descritto in Pausa automatica e riabilitazione.
I replay sono limitati a 20 per organizzazione per giorno UTC; oltre quel limite la richiesta restituisce un 429 (WebhookReplayQuotaExceeded). La risposta non include un contatore né un ID attività. Monitora i risultati con GET /v1/webhooks/{webhook_id}/attempts, che elenca i tentativi di consegna recenti dal più nuovo al più vecchio con status code e latenza. Ogni richiesta HTTP ha la propria voce, quindi un evento riprovato appare una volta per tentativo, e una riconsegna appare come una voce aggiuntiva.

Rotazione del secret di firma

POST /v1/webhooks/{webhook_id}/rotate-secret genera un nuovo secret e lo restituisce una sola volta. Per le successive 24 ore, Bird firma ogni consegna con entrambi i secret. L'header webhook-signature contiene le firme separate da spazi (v1,<old> v1,<new>), consentendoti di distribuire il nuovo secret durante la sovrapposizione. Le librerie Standard Webhooks provano tutte le firme automaticamente. Dopo 24 ore il vecchio secret smette di firmare. Un endpoint può avere al massimo 5 secret validi contemporaneamente, quindi effettuare rotazioni ripetute all'interno della finestra di sovrapposizione fallisce con WebhookTooManySecrets finché un secret più vecchio non scade.

Pausa automatica e riabilitazione

Il status dell'endpoint è active, degraded o paused. Fallimenti di consegna recenti contrassegnano un endpoint come degraded, a titolo di avviso sullo stato di salute; continuiamo a consegnare e riprovare. Un endpoint che fallisce continuamente per circa cinque giorni viene automaticamente paused e ogni consegna si interrompe; una singola consegna riuscita durante quel periodo resetta il conteggio. Un endpoint in pausa non riprende da solo. Riabilitalo con PATCH /v1/webhooks/{webhook_id} e {"status": "active"} (o dalla pagina Webhooks nella dashboard), quindi esegui il replay per riconsegnare i tentativi falliti prima della pausa. Riabilita prima: un replay richiesto mentre l'endpoint è ancora in pausa non riconsegna nulla. Gli eventi arrivati mentre era in pausa non sono mai stati inviati, quindi un replay non li recupera.
Ognuna di queste azioni riporta un endpoint degraded a active:
Cosa lo ripristinaPerché
Una consegna ha successoL'endpoint ha accettato di nuovo un evento.
Modifica dell'url dell'endpointI fallimenti registrati descrivono una destinazione che non usi più.
Riabilitazione di un endpoint pausedSta rientrando in servizio, quindi i vecchi fallimenti non si applicano più.
Un invio di test che restituisce 2xxHai dimostrato che l'endpoint è raggiungibile.
Modificare la descrizione di un endpoint o i suoi tipi di evento sottoscritti non dice nulla sulla raggiungibilità, quindi lascia degraded inalterato, così come un invio di test che fallisce.
Inviamo un'email ai proprietari dell'organizzazione quando un endpoint diventa degraded per la prima volta, una volta per episodio anziché per ogni consegna fallita. Un successivo degrado dopo un ripristino genera una nuova email, soggetta a un cooldown di 24 ore: inviamo al massimo un'email di degrado per endpoint ogni 24 ore, in modo che un endpoint che oscilla tra active e degraded non inondi la casella di posta. La modifica dell'url dell'endpoint azzera il cooldown, perciò il primo degrado su un nuovo URL può generare un'email anche entro 24 ore dall'ultima.

Catalogo eventi

I payload degli eventi contengono fatti compatti e relativi al destinatario per la correlazione con il tuo sistema. Non contengono la risorsa completa. Se hai bisogno di più contesto, recupera la risorsa tramite il suo ID. I tipi di evento seguono la convenzione di denominazione resource.action e sono raggruppati per prodotto; la pagina eventi di ciascun prodotto riporta i campi del payload per evento:
  • Eventi email: il ciclo di vita della consegna (da email.accepted a email.delivered o email.bounced), engagement (email.opened, email.clicked), disiscrizioni e email in entrata
  • Eventi SMS: il ciclo di vita del messaggio da sms.accepted a uno stato terminale
  • Webhook WhatsApp: da whatsapp.accepted a whatsapp.delivered, whatsapp.read, whatsapp.failed, whatsapp.rejected, whatsapp.received per un messaggio in entrata, whatsapp.reacted quando un utente reagisce a un tuo messaggio, e whatsapp.group.join_request_created e whatsapp.group.join_request_revoked quando qualcuno chiede di unirsi a un gruppo che richiede approvazione o ritira la richiesta
  • Eventi Verify: il ciclo di vita della verifica (verify.verification.created, verify.verification.verified) e la consegna di ciascun tentativo con codice di verifica (verify.attempt.sent, verify.attempt.delivered, verify.attempt.undelivered)
  • Eventi Preference: il registro di consenso cross-channel: preference.granted, preference.revoked e preference.deleted
Ogni corpo di consegna è la risposta annidata Standard Webhooks con type, timestamp e un oggetto data specifico per tipo. L'header webhook-id porta l'identità dell'evento. Il campo timestamp della risposta registra quando l'evento si è verificato. L'header webhook-timestamp registra il tentativo di consegna corrente e cambia a ogni ripetizione.
Esempio di codice
{
  "type": "email.delivered",
  "timestamp": "2026-06-10T14:30:00Z",
  "data": {
    "email_id": "em_01krdgeqcxet5s7t44vh8rt9mg",
    "recipient_id": "er_01krdgeqcxet5s7t44vh8rt9mg",
    "workspace_id": "ws_01krdgeqcxet5s7t44vh8rt9mg",
    "recipient": "user@example.com",
    "recipient_role": "to",
    "tags": [{ "name": "category", "value": "welcome" }],
    "metadata": { "order_id": "ord_123" },
    "broadcast_id": null
  }
}
Il data di ogni evento email include email_id, recipient_id, workspace_id, l'indirizzo recipient e il relativo recipient_role della risposta. Include anche tags e metadata dalla richiesta di invio, oppure null quando non forniti. Contiene inoltre broadcast_id, che indica il broadcast di cui l'invio faceva parte, oppure null quando non c'era alcun broadcast. Per email.unsubscribed e email.list_unsubscribed, null non esclude un broadcast; eventi email spiega perché. I tipi di evento aggiungono i propri campi a questa base. Ogni variante ha un set di campi stabile: i campi sono obbligatori per default e la loro presenza dipende solo dal tipo di evento.
I nomi degli eventi non vengono mai rinominati e nuovi tipi vengono aggiunti con il rilascio dei prodotti, quindi scrivi il tuo handler in modo che ignori i tipi che non riconosce.

Eventi Preference

Le preferenze dichiarate (le concessioni di consenso e le revoche descritte nella guida di ciascun canale: email, SMS, WhatsApp) coprono più canali, perciò i relativi eventi indicano il canale nel payload anziché nel tipo. preference.granted si attiva quando una concessione di consenso entra in vigore, preference.revoked quando entra in vigore una revoca, e preference.deleted quando una dichiarazione registrata viene rimossa e la sua chiave torna a non avere alcun record. Un evento significa che il record corrente della chiave è cambiato: una dichiarazione che ripete quella corrente non genera nulla, e neppure una rifiutata perché fuori ordine. Il campo timestamp dell'envelope indica quando la dichiarazione è entrata in vigore: per una dichiarazione retrodatata corrisponde al momento in cui è stata formulata, non a quando ha raggiunto Bird.
Ogni payload porta la chiave di preferenza completa: channel, handle, sender_scope e topic_id, con i campi di scoping presenti con valore null quando non restringono. Accanto alla chiave si trovano il coverage della dichiarazione, il preference_id, il transition_id della voce di cronologia aggiunta dalla scrittura e il contact_id il cui handle ha trovato corrispondenza al momento della registrazione della dichiarazione, oppure null:
Esempio di codice
{
  "type": "preference.revoked",
  "timestamp": "2026-08-25T14:52:03.192524705Z",
  "data": {
    "preference_id": "prf_01krdgeqcxet5s7t44vh8rt9mg",
    "transition_id": "prt_01krdgeqcxet5s7t44vh8rt9mg",
    "channel": "sms",
    "handle": "+15550001234",
    "sender_scope": null,
    "topic_id": null,
    "coverage": "non_transactional",
    "contact_id": null
  }
}

Prossimi passi