Sviluppatori

Webhook Beta

Ricevi notifiche firmate e affidabili quando cambiano contatti, conversazioni, messaggi, etichette o membri del team.

Apri il riferimento API

Crea un endpoint

  1. 01

    Apri Impostazioni, seleziona Sviluppatori, trova Webhook, poi seleziona Aggiungi endpoint.

  2. 02

    Inserisci un Nome e un URL dell'endpoint HTTPS pubblico, poi scegli almeno un elemento in Eventi.

  3. 03

    Seleziona Aggiungi endpoint. Puoi crearne uno anche con POST /api/v1/webhooks.

Sonny rifiuta credenziali negli URL, localhost, intervalli IP privati/link-local e nomi DNS che si risolvono in un indirizzo non pubblico.

Salva subito il segreto di firma

Inizia con whsec_ e viene mostrato una sola volta. Copialo nel tuo gestore dei segreti prima di selezionare L'ho salvato.

Limita un endpoint ai canali selezionati

In Impostazioni → Sviluppatori, ogni endpoint può ascoltare tutti i canali o solo quelli che scegli, sia quando lo aggiungi sia quando lo modifichi in seguito. Gli endpoint creati prima dell'introduzione del filtro per canale restano su tutti i canali finché non li modifichi.

I client API e MCP impostano lo stesso filtro con sourceIds quando creano o aggiornano un endpoint. Un array vuoto significa tutte le sorgenti dello spazio di lavoro. Quando sono presenti degli ID, gli eventi di conversazioni e messaggi vengono consegnati solo se la conversazione appartiene a una delle sorgenti selezionate. Gli eventi a livello di spazio di lavoro, come le modifiche ai contatti o ai membri, non vengono inviati a un endpoint filtrato per sorgente.

{
  "name": "Product A agent",
  "url": "https://agent.example.com/sonny",
  "events": ["conversation.created", "message.created"],
  "sourceIds": ["cm_source_id"]
}

Smetti di consegnare ciò che la tua integrazione scarta

Un agente che risponde tramite API viene riattivato dalla propria risposta, a meno che tu non indichi diversamente. Tre filtri facoltativi restringono ciò che un endpoint riceve; sono tutti disattivati di default, quindi un endpoint esistente non cambia.

agentGroupIds
Solo le conversazioni di proprietà dei gruppi di agenti indicati. Questo filtro segue la proprietà, non il canale da cui è arrivata la conversazione, quindi una conversazione da una posta in arrivo condivisa raggiunge il gruppo che ne è responsabile. Le conversazioni senza gruppo non vengono consegnate e, poiché i gruppi di solito vengono assegnati dopo l'inizio della conversazione, conversation.created spesso scatta prima che ci sia un gruppo con cui fare corrispondenza.
customerMessagesOnly
Solo i messaggi scritti dai clienti. Salta le risposte del tuo team, le note interne e tutto ciò che viene inviato tramite API, MCP o il risponditore AI, comprese le risposte di questo stesso endpoint.
excludedSenderIds
Salta i messaggi inviati dai colleghi indicati. Dai a un'integrazione un proprio account da collega ed escludilo: così non si riattiva con le proprie risposte, ma si accorge comunque quando una persona prende in mano la conversazione, il segnale di cui un agente AI ha bisogno per farsi da parte.

I filtri sui messaggi si applicano solo agli eventi message.*; per tutto il resto il controllo restano i tipi di evento. Un evento filtrato viene scartato prima di diventare una consegna, quindi non ti costa nulla e non compare mai come errore. Il payload e apiVersion restano invariati in ogni caso.

Raggruppa una raffica di messaggi in un'unica consegna

Un consumer che legge l'intero thread quando si attiva non guadagna nulla da quattro consegne separate in dieci secondi. Imposta coalesceSeconds e i messaggi di una conversazione vengono raccolti per quel tempo, poi inviati in un'unica consegna. È disattivato di default; gli endpoint che non lo usano continuano a ricevere ogni messaggio singolarmente.

Una consegna raggruppata arriva come conversation.activity con la conversazione e tutti gli ID dei messaggi raccolti, quindi leggi il thread una volta sola invece che per ogni messaggio. È l'unica impostazione che cambia la forma di ciò che ricevi, per questo va attivata esplicitamente. Gli altri filtri continuano ad applicarsi: un messaggio escluso da essi non entra mai in un gruppo. Ogni conversazione ha la propria finestra, e i nuovi tentativi trattano il gruppo come un'unica consegna.

{
  "type": "conversation.activity",
  "data": {
    "conversationId": "cm_conversation_id",
    "messageIds": ["cm_first_message", "cm_second_message"]
  }
}

Endpoint protetti da autenticazione bearer o con chiave API

Se il tuo ricevitore si trova dietro un gateway che richiede un header, aggiungilo in Header personalizzati quando crei o modifichi l'endpoint, oppure invia headers tramite l'API. I valori sono crittografati a riposo e non vengono mai restituiti: un header salvato torna indietro solo con il nome, e reinviare quel nome da solo mantiene il valore salvato. Invia un array vuoto per rimuovere tutti gli header.

Spostare un endpoint su un host diverso annulla il riutilizzo: i valori salvati devono essere inseriti di nuovo, così una credenziale non viene mai inoltrata a una destinazione per cui non è stata emessa. Cambiare solo il percorso li mantiene.

Gli header di Sonny prevalgono sui tuoi, quindi un header personalizzato non può mai sostituire sonny-signature, content-type o gli header di identità della consegna. Quando puoi, preferisci verificare la firma: autentica ogni payload, mentre un token statico identifica solo il chiamante.

{
  "name": "Gateway",
  "url": "https://api.example.com/hooks/sonny",
  "events": ["conversation.created"],
  "headers": [{ "name": "Authorization", "value": "Bearer …" }]
}

Sicurezza e affidabilità

Corpo grezzo firmato
L'HMAC-SHA256 copre il timestamp Unix, un punto e il corpo della richiesta UTF-8 non modificato.
Protezione dai replay
Rifiuta i timestamp di oltre cinque minuti nel passato o nel futuro, anche quando l'HMAC è valido.
Nuovi tentativi affidabili
Le risposte diverse da 2xx vengono ritentate dopo 1m, 5m, 30m, 2h, 6h. Il tentativo 6 è l'ultimo.

Contratto della richiesta

sonny-signature
t=<unix-seconds>,v1=<sha256-hex>
sonny-event
Il tipo di evento, per un instradamento rapido.
sonny-delivery-id
Un ID stabile per l'idempotenza e l'assistenza.
user-agent
Sonny-Webhooks/1.0
{
  "id": "cm_event_id",
  "type": "message.created",
  "apiVersion": "2026-07-15",
  "createdAt": "2026-07-15T12:00:00.000Z",
  "data": {
    "conversationId": "cm_conversation_id",
    "messageId": "cm_message_id"
  }
}

Verifica la firma

Leggi prima il corpo grezzo. Analizzare il JSON e serializzarlo di nuovo cambia gli spazi e fa fallire una firma valida.

import { createHmac, timingSafeEqual } from "node:crypto";

const rawBody = await request.text(); // do not parse/re-serialize first
const signature = request.headers.get("sonny-signature");
const secret = process.env.SONNY_WEBHOOK_SECRET;
if (!signature || !secret) throw new Error("Missing webhook signature or secret");
const { t, v1 } = Object.fromEntries(signature.split(",").map(p => p.split("=")));

if (!/^\d+$/.test(t) || !/^[a-f0-9]{64}$/i.test(v1)) {
  throw new Error("Malformed signature");
}
if (Math.abs(Math.floor(Date.now() / 1000) - Number(t)) > 300) {
  throw new Error("Stale webhook");
}
const expected = createHmac("sha256", secret)
  .update(`${t}.${rawBody}`).digest("hex");
if (!timingSafeEqual(Buffer.from(v1, "hex"), Buffer.from(expected, "hex"))) {
  throw new Error("Invalid signature");
}

Catalogo degli eventi

webhook.heartbeat

Inviato ogni cinque minuti agli iscritti attivi (incluso *), anche senza nuovi messaggi. Usa il normale percorso di consegna firmata e tentativi ripetuti e ignora i filtri sulle conversazioni. Imposta un avviso se i heartbeat mancano o sono vecchi; non cerca le conversazioni in attesa di risposta.

{"intervalSeconds":300,"nextExpectedAt":"2026-09-29T16:05:00.000Z"}
contact.created

È stato creato un contatto.

{"contactId":"cm_contact_id"}
contact.updated

Un contatto è stato aggiornato o unito.

{"contactId":"cm_contact_id"}
contact.deleted

Un contatto è stato archiviato o unito a un altro contatto. Può ancora essere letto con archived=true.

{"contactId":"cm_contact_id"}
contact.erased

Un contatto è stato cancellato definitivamente (ad esempio per una richiesta di cancellazione GDPR) con tutte le sue conversazioni, i messaggi e gli allegati. Non può più essere letto; elimina tutte le copie che conservi.

{"contactId":"cm_contact_id"}
conversation.created

È stata creata una conversazione.

{"conversationId":"cm_conversation_id"}
conversation.updated

Una conversazione è cambiata.

{"conversationId":"cm_conversation_id"}
conversation.closed

Una conversazione è stata chiusa.

{"conversationId":"cm_conversation_id"}
conversation.deleted

Una conversazione è stata spostata nel cestino ed è uscita dall'API pubblica.

{"conversationId":"cm_conversation_id"}
message.created

È stato creato un messaggio o una nota interna. Quando disponibili, sono inclusi il contesto e una breve anteprima del messaggio; il testo delle note interne non è mai incluso.

{"conversationId":"cm_conversation_id","messageId":"cm_message_id","context":{"sourceId":"cm_source_id","sourceName":"Shopstar Go","storeName":"Shiney Store","inboxId":"cm_inbox_id","inboxName":"Go support","agentGroupId":"cm_group_id","agentGroupName":"Support","identityVerified":true,"status":"open","lastRepliedAt":"2026-09-25T09:00:00.000Z"},"message":{"id":"cm_message_id","type":"incoming","senderType":"human","senderId":null,"senderName":"Shiney","textPreview":"Could you check my bill?","textTruncated":false}}
message.updated

Una risposta inviata è stata modificata nella trascrizione. Le email già consegnate restano invariate.

{"conversationId":"cm_conversation_id","messageId":"cm_message_id"}
tag.created

È stata creata un'etichetta.

{"tagId":"cm_tag_id"}
tag.updated

Un'etichetta è stata aggiornata.

{"tagId":"cm_tag_id"}
tag.deleted

Un'etichetta è stata eliminata.

{"tagId":"cm_tag_id"}
member.invited

È stato invitato un membro dello spazio di lavoro.

{"invitationId":"cm_invitation_id"}
member.updated

Il ruolo o lo stato di un membro è cambiato.

{"memberId":"cm_membership_id"}
member.removed

Un membro è stato rimosso.

{"memberId":"cm_membership_id"}
invitation.accepted

Un invito allo spazio di lavoro è stato accettato.

{"invitationId":"cm_invitation_id","memberId":"cm_membership_id"}
invitation.cancelled

Un invito in sospeso allo spazio di lavoro è stato annullato.

{"invitationId":"cm_invitation_id"}
conversation.activity

Messaggi di una conversazione raggruppati in un'unica consegna. Include un'istantanea di ogni messaggio, quando disponibile. Viene inviato al posto di message.created agli endpoint con una finestra di raggruppamento; non è possibile iscriversi direttamente.

{"conversationId":"cm_conversation_id","messageIds":["cm_message_id","cm_other_message_id"]}
webhook.test

Un evento di test richiesto da un amministratore.

{"message":"This is a test webhook from Sonny."}

Comportamento delle consegne

  • Restituisci un qualsiasi stato 2xx entro 10 secondi per segnare la consegna come riuscita.
  • Mantieni gli handler idempotenti. Usa il campo id dell'evento o sonny-delivery-id per ignorare i duplicati.
  • I reindirizzamenti non vengono seguiti. Aggiorna invece l'URL dell'endpoint in Sonny.
  • I corpi delle risposte sono limitati e per la diagnosi vengono conservati solo i primi 4 KiB.
  • Seleziona Test per inviare un evento webhook.test. Seleziona Consegne per esaminare gli ultimi 50 eventi. Quando una consegna raggiunge un errore definitivo, seleziona Riprova per inviarla di nuovo.

Rileva un feed silenzioso

Aggiungi webhook.heartbeat agli eventi del tuo endpoint nelle impostazioni Sviluppatori o tramite update_webhook. Gli iscritti abilitati (compreso *) ricevono un heartbeat firmato ogni cinque minuti, anche quando non arrivano nuovi messaggi. Gli heartbeat ignorano i filtri per canale, team e messaggi e non contengono dati delle conversazioni. Usano lo stesso percorso di consegna e gli stessi nuovi tentativi dei messaggi. Controlla createdAt e nextExpectedAt, così un vecchio tentativo non può sembrare un heartbeat recente; tieni conto dei ritardi di polling e di rete prima di generare un avviso. Un arretrato di consegne può ritardare o sopprimere gli heartbeat.

Usa list_webhook_deliveries con webhooks:read per esaminare tentativi ed errori. get_status verifica la connettività del database, non la consegna dei webhook. Pianifica in modo indipendente list_conversations con status=open, awaitingReply=true, sort=waitingSince e direction=asc per trovare i thread senza risposta più vecchi anche quando il feed è silenzioso.

Attiva Payload compatti nelle impostazioni Sviluppatori o imposta compact=true sull'endpoint per omettere le etichette di contesto mantenendo ID di instradamento, mittente, orario e anteprime dei messaggi di 200 caratteri. Vale anche per le consegne raggruppate; il testo delle note interne resta escluso. I payload predefiniti non cambiano, a parte l'aggiunta del timestamp del messaggio. Modifica gli ambiti della tua chiave API esistente per concedere webhooks:read e contacts:read per i log delle consegne e la ricerca diretta dei contatti. Entrambi gli ambiti richiedono una chiave con accesso a tutti i canali e un membro con accesso a tutti i canali; una chiave limitata non può essere ampliata solo aggiungendo permessi.

Guide correlate