Développeurs
Webhooks Beta
Recevez des notifications durables et signées lorsque des contacts, des conversations, des messages, des étiquettes ou les membres de l'équipe changent.
Ouvrir la référence de l'APICréer un point de terminaison
- 01
Ouvrez Paramètres, sélectionnez Développeur, repérez Webhooks, puis sélectionnez Ajouter un point de terminaison.
- 02
Saisissez un Nom et une URL du point de terminaison HTTPS publique, puis choisissez au moins un élément sous Événements.
- 03
Sélectionnez Ajouter le point de terminaison. Vous pouvez aussi en créer un avec
POST /api/v1/webhooks.
Sonny refuse les identifiants dans les URL, localhost, les plages d'IP privées ou link-local, et les noms DNS qui pointent vers une adresse non publique.
Enregistrez immédiatement le secret de signature
Il commence par whsec_ et n'est affiché qu'une seule fois. Copiez-le dans votre gestionnaire de secrets avant de sélectionner Je l'ai enregistrée.
Limiter un point de terminaison à certains canaux
Dans Paramètres → Développeur, chaque point de terminaison peut écouter tous les canaux ou uniquement ceux que vous choisissez, lors de sa création comme lors d'une modification ultérieure. Les points de terminaison créés avant l'existence de cette option restent sur tous les canaux jusqu'à ce que vous les modifiiez.
Les clients API et MCP définissent le même filtre avec sourceIds lors de la création ou de la mise à jour d'un point de terminaison. Un tableau vide signifie toutes les sources de l'espace de travail. Lorsque des identifiants sont présents, les événements de conversation et de message ne sont livrés que si leur conversation appartient à l'une des sources sélectionnées. Les événements au niveau de l'espace de travail, comme les changements de contacts ou de membres, ne sont pas envoyés à un point de terminaison filtré par source.
{
"name": "Product A agent",
"url": "https://agent.example.com/sonny",
"events": ["conversation.created", "message.created"],
"sourceIds": ["cm_source_id"]
}Ne plus livrer ce que votre intégration ignore
Un agent qui répond via l'API est réveillé par sa propre réponse, sauf indication contraire. Trois filtres optionnels restreignent ce qu'un point de terminaison reçoit ; tous sont désactivés par défaut, donc un point de terminaison existant reste inchangé.
agentGroupIds- Uniquement les conversations détenues par les groupes d'agents listés. Ce filtre suit la responsabilité, pas le canal par lequel la conversation est arrivée : une conversation d'une boîte partagée atteint le groupe qui en est responsable. Les conversations sans groupe ne sont pas livrées, et comme les groupes sont généralement attribués après le début d'une conversation,
conversation.createdse déclenche souvent avant qu'il y ait un groupe à faire correspondre. customerMessagesOnly- Uniquement les messages écrits par les clients. Ignore les réponses de votre équipe, les notes internes et tout ce qui est envoyé via l'API, MCP ou le répondeur IA — y compris les propres réponses de ce point de terminaison.
excludedSenderIds- Ignore les messages envoyés par les collègues listés. Donnez à une intégration son propre compte de collègue et excluez-le : elle ne se réveille plus sur ses propres réponses, tout en étant prévenue quand un humain reprend la conversation — le signal dont un agent IA a besoin pour se retirer.
Les filtres de messages ne s'appliquent qu'aux événements message.* ; pour tout le reste, ce sont les types d'événements qui servent de contrôle. Un événement filtré est écarté avant de devenir une livraison : il ne vous coûte rien et n'apparaît jamais comme un échec. La charge utile et apiVersion restent inchangées dans tous les cas.
Regrouper une rafale de messages en une seule livraison
Un consommateur qui relit tout le fil à son réveil ne gagne rien à recevoir quatre livraisons distinctes en dix secondes. Définissez coalesceSeconds : les messages d'une même conversation sont collectés pendant cette durée, puis envoyés en une seule livraison. Cette option est désactivée par défaut ; les points de terminaison qui ne l'utilisent pas continuent de recevoir chaque message séparément.
Une livraison groupée arrive sous la forme conversation.activity, avec la conversation et chaque id de message collecté : lisez donc le fil une seule fois plutôt que message par message. C'est le seul réglage qui modifie la forme de ce que vous recevez, d'où son activation volontaire. Vos autres filtres s'appliquent toujours — un message exclu par ceux-ci ne rejoint jamais un groupe. Chaque conversation a sa propre fenêtre, et les nouvelles tentatives traitent le groupe comme une seule livraison.
{
"type": "conversation.activity",
"data": {
"conversationId": "cm_conversation_id",
"messageIds": ["cm_first_message", "cm_second_message"]
}
}Points de terminaison protégés par un jeton Bearer ou une clé API
Si votre récepteur se trouve derrière une passerelle qui exige un en-tête, ajoutez-le sous En-têtes personnalisés lors de la création ou de la modification du point de terminaison, ou envoyez headers depuis l'API. Les valeurs sont chiffrées au repos et ne sont jamais renvoyées — un en-tête enregistré revient avec son nom seulement, et renvoyer ce nom seul conserve la valeur stockée. Envoyez un tableau vide pour supprimer tous les en-têtes.
Déplacer un point de terminaison vers un autre hôte annule cette réutilisation : les valeurs enregistrées doivent être saisies à nouveau, afin qu'un identifiant ne soit jamais transmis à une destination pour laquelle il n'a pas été émis. Modifier uniquement le chemin les conserve.
Les en-têtes de Sonny l'emportent sur les vôtres : un en-tête personnalisé ne peut jamais remplacer sonny-signature, content-type ni les en-têtes d'identité de livraison. Privilégiez la vérification de la signature quand c'est possible : elle authentifie chaque charge utile, alors qu'un jeton statique ne fait qu'identifier l'appelant.
{
"name": "Gateway",
"url": "https://api.example.com/hooks/sonny",
"events": ["conversation.created"],
"headers": [{ "name": "Authorization", "value": "Bearer …" }]
}Sécurité et fiabilité
- Corps brut signé
- Le HMAC-SHA256 couvre l'horodatage Unix, un point et le corps de requête UTF-8 non modifié.
- Protection contre la relecture
- Rejetez les horodatages décalés de plus de cinq minutes dans le passé ou le futur, même si le HMAC est valide.
- Nouvelles tentatives durables
- Les réponses autres que 2xx sont relancées après 1m, 5m, 30m, 2h, 6h. La tentative 6 est la dernière.
Contrat de requête
sonny-signature- t=<unix-seconds>,v1=<sha256-hex>
sonny-event- Le type d'événement, pour un routage rapide.
sonny-delivery-id- Un identifiant stable pour l'idempotence et le support.
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"
}
}Vérifier la signature
Lisez d'abord le corps brut. Analyser le JSON puis le resérialiser modifie les espaces et fait échouer une signature pourtant valide.
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");
}Catalogue des événements
webhook.heartbeatEnvoyé toutes les cinq minutes aux abonnés actifs (y compris *), même sans nouveau message. Utilise le chemin normal de livraison signée et de relance, et ignore les filtres de conversation. Déclenchez une alerte en cas de heartbeat manquant ou ancien ; il ne recherche pas les conversations en attente de réponse.
{"intervalSeconds":300,"nextExpectedAt":"2026-09-29T16:05:00.000Z"}contact.createdUn contact a été créé.
{"contactId":"cm_contact_id"}contact.updatedUn contact a été mis à jour ou fusionné.
{"contactId":"cm_contact_id"}contact.deletedUn contact a été archivé ou fusionné dans un autre contact. Il reste lisible avec archived=true.
{"contactId":"cm_contact_id"}contact.erasedUn contact a été définitivement effacé (par exemple suite à une demande d'effacement RGPD) avec toutes ses conversations, messages et pièces jointes. Il n'est plus lisible ; supprimez toutes les copies que vous conservez.
{"contactId":"cm_contact_id"}conversation.createdUne conversation a été créée.
{"conversationId":"cm_conversation_id"}conversation.updatedUne conversation a changé.
{"conversationId":"cm_conversation_id"}conversation.closedUne conversation a été fermée.
{"conversationId":"cm_conversation_id"}conversation.deletedUne conversation a été placée dans la corbeille et a quitté l'API publique.
{"conversationId":"cm_conversation_id"}message.createdUn message ou une note interne a été créé. Le contexte et un court aperçu du message sont inclus lorsqu'ils sont disponibles ; le texte des notes internes n'est jamais inclus.
{"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.updatedUne réponse envoyée a été modifiée dans la transcription. Les e-mails déjà distribués restent inchangés.
{"conversationId":"cm_conversation_id","messageId":"cm_message_id"}tag.createdUne étiquette a été créée.
{"tagId":"cm_tag_id"}tag.updatedUne étiquette a été mise à jour.
{"tagId":"cm_tag_id"}tag.deletedUne étiquette a été supprimée.
{"tagId":"cm_tag_id"}member.invitedUn membre de l'espace de travail a été invité.
{"invitationId":"cm_invitation_id"}member.updatedLe rôle ou le statut d'un membre a changé.
{"memberId":"cm_membership_id"}member.removedUn membre a été retiré.
{"memberId":"cm_membership_id"}invitation.acceptedUne invitation à l'espace de travail a été acceptée.
{"invitationId":"cm_invitation_id","memberId":"cm_membership_id"}invitation.cancelledUne invitation en attente à l'espace de travail a été annulée.
{"invitationId":"cm_invitation_id"}conversation.activityMessages d'une même conversation, regroupés en une seule livraison. Inclut un instantané de chaque message lorsqu'il est disponible. Envoyé à la place de message.created aux points de terminaison disposant d'une fenêtre de regroupement ; on ne s'y abonne jamais directement.
{"conversationId":"cm_conversation_id","messageIds":["cm_message_id","cm_other_message_id"]}webhook.testUn événement de test demandé par un administrateur.
{"message":"This is a test webhook from Sonny."}
Comportement de livraison
- Renvoyez n'importe quel statut 2xx dans les 10 secondes pour que la livraison soit considérée comme réussie.
- Gardez vos gestionnaires idempotents. Utilisez le champ
idde l'événement ousonny-delivery-idpour ignorer les doublons. - Les redirections ne sont pas suivies. Mettez plutôt à jour l'URL du point de terminaison dans Sonny.
- Les corps de réponse sont plafonnés et seuls les 4 premiers Kio sont conservés pour le diagnostic.
- Sélectionnez Tester pour envoyer un événement
webhook.test. Sélectionnez Livraisons pour inspecter les 50 derniers événements. Quand une livraison atteint un échec définitif, sélectionnez Réessayer pour l'envoyer à nouveau.
Détecter un flux silencieux
Ajoutez webhook.heartbeat aux événements de votre point de terminaison dans les paramètres Développeur, ou via update_webhook. Les abonnés actifs (y compris *) reçoivent un heartbeat signé toutes les cinq minutes, même sans nouveau message. Les heartbeats ignorent les filtres de canal, d'équipe et de message et ne contiennent aucune donnée de conversation. Ils utilisent le même circuit de livraison et les mêmes nouvelles tentatives que les messages. Vérifiez createdAt et nextExpectedAt pour qu'une ancienne relance ne passe pas pour un heartbeat récent ; tenez compte des délais d'interrogation et du réseau avant de déclencher une alerte. Un retard de livraison peut retarder ou supprimer les heartbeats.
Utilisez list_webhook_deliveries avec webhooks:read pour inspecter les tentatives et les échecs. get_status vérifie la connexion à la base de données, pas la livraison des webhooks. Planifiez indépendamment list_conversations avec status=open, awaitingReply=true, sort=waitingSince et direction=asc pour retrouver les anciens fils sans réponse, même quand le flux est calme.
Activez Charges utiles compactes dans les paramètres Développeur ou définissez compact=true sur le point de terminaison pour omettre les libellés de contexte tout en conservant les identifiants de routage, l'expéditeur, l'heure et des aperçus de message de 200 caractères. Cela s'applique aussi aux livraisons groupées ; le texte des notes internes reste exclu. Les charges utiles par défaut sont inchangées, à part l'horodatage du message ajouté. Modifiez les portées de votre clé API existante pour accorder webhooks:read et contacts:read afin d'accéder aux journaux de livraison et aux recherches directes de contacts. Ces deux portées exigent une clé couvrant tous les canaux et un accès membre à tous les canaux ; une clé à portée limitée ne peut pas être élargie simplement en ajoutant des permissions.
Documentation associée
- API publiqueBeta
Triez et synchronisez les conversations, partagez le contexte client, lisez les rapports et envoyez des fichiers avec des clés API à portée limitée.
- Boîte de réception
Comprenez les statuts, les priorités, l'attribution, le report, les actions groupées et les raccourcis clavier.
- Contacts
Découvrez comment les contacts sont créés, gérés, étiquetés et fusionnés dans Sonny.
- Équipe et rôles
Invitez des utilisateurs, créez des équipes et comprenez les accès propriétaire, admin, agent et lecteur.