Entwickler
Webhooks Beta
Erhalte zuverlässige, signierte Benachrichtigungen, wenn sich Kontakte, Unterhaltungen, Nachrichten, Labels oder Team-Mitgliedschaften ändern.
API-Referenz öffnenEndpunkt erstellen
- 01
Öffne Einstellungen, wähle Entwickler, suche Webhooks und wähle dann Endpunkt hinzufügen.
- 02
Gib unter Name einen Namen und unter Endpunkt-URL eine öffentliche HTTPS-Adresse ein und wähle dann unter Ereignisse mindestens einen Eintrag.
- 03
Wähle Endpunkt hinzufügen. Du kannst einen Endpunkt auch mit
POST /api/v1/webhookserstellen.
Sonny lehnt Zugangsdaten in URLs, localhost, private bzw. link-lokale IP-Bereiche und DNS-Namen ab, die auf eine nicht öffentliche Adresse auflösen.
Signaturgeheimnis sofort speichern
Es beginnt mit whsec_ und wird nur einmal angezeigt. Kopiere es in deinen Secret-Manager, bevor du Ich habe ihn gespeichert wählst.
Endpunkt auf ausgewählte Kanäle beschränken
Unter Einstellungen → Entwickler kann jeder Endpunkt auf alle Kanäle hören oder nur auf die Kanäle, die du auswählst – beim Hinzufügen ebenso wie beim späteren Bearbeiten. Endpunkte, die vor der Kanal-Beschränkung erstellt wurden, bleiben auf allen Kanälen, bis du sie änderst.
API- und MCP-Clients setzen denselben Filter mit sourceIds beim Erstellen oder Aktualisieren eines Endpunkts. Ein leeres Array steht für alle Quellen im Arbeitsbereich. Sind IDs angegeben, werden Unterhaltungs- und Nachrichtenereignisse nur zugestellt, wenn ihre Unterhaltung zu einer der ausgewählten Quellen gehört. Ereignisse auf Arbeitsbereichsebene, etwa Kontakt- oder Mitgliedschaftsänderungen, gehen nicht an einen nach Quellen gefilterten Endpunkt.
{
"name": "Product A agent",
"url": "https://agent.example.com/sonny",
"events": ["conversation.created", "message.created"],
"sourceIds": ["cm_source_id"]
}Nur zustellen, was deine Integration auch braucht
Ein Agent, der über die API antwortet, wird von seiner eigenen Antwort geweckt, sofern du nichts anderes festlegst. Drei optionale Filter schränken ein, was ein Endpunkt erhält; alle sind standardmäßig aus, bestehende Endpunkte bleiben also unverändert.
agentGroupIds- Nur Unterhaltungen, die den aufgeführten Agent-Gruppen gehören. Das richtet sich nach der Zuständigkeit, nicht nach dem Kanal, über den eine Unterhaltung kam – eine Unterhaltung aus einem gemeinsamen Posteingang erreicht also die Gruppe, der sie gehört. Unterhaltungen ohne Gruppe werden nicht zugestellt, und weil Gruppen meist erst nach dem Start einer Unterhaltung zugewiesen werden, wird
conversation.createdoft ausgelöst, bevor es eine passende Gruppe gibt. customerMessagesOnly- Nur von Kunden verfasste Nachrichten. Überspringt Antworten deines Teams, interne Notizen und alles, was über die API, MCP oder den KI-Responder gesendet wird – einschließlich der eigenen Antworten dieses Endpunkts.
excludedSenderIds- Überspringt Nachrichten der aufgeführten Teammitglieder. Gib einer Integration ein eigenes Teammitglied-Konto und schließe es aus: So wird sie nicht von ihren eigenen Antworten geweckt, merkt aber weiterhin, wenn ein Mensch die Unterhaltung übernimmt – das Signal, das ein KI-Agent zum Zurücktreten braucht.
Die Nachrichtenfilter gelten nur für message.*-Ereignisse; für alles andere steuerst du weiterhin über die Ereignistypen. Ein gefiltertes Ereignis wird verworfen, bevor eine Zustellung daraus wird – es kostet dich also nichts und erscheint nie als Fehler. Payload und apiVersion bleiben in jedem Fall unverändert.
Mehrere Nachrichten in einer Zustellung bündeln
Ein Empfänger, der beim Aufwachen den ganzen Thread liest, hat nichts von vier einzelnen Zustellungen in zehn Sekunden. Setze coalesceSeconds, dann werden die Nachrichten einer Unterhaltung so lange gesammelt und als eine einzige Zustellung gesendet. Standardmäßig ist das aus; Endpunkte ohne diese Einstellung erhalten weiterhin jede Nachricht einzeln.
Eine gebündelte Zustellung kommt als conversation.activity mit der Unterhaltung und allen gesammelten Nachrichten-IDs an – lies den Thread also einmal statt pro Nachricht. Das ist die einzige Einstellung, die die Form dessen ändert, was du erhältst, deshalb musst du sie aktiv einschalten. Deine anderen Filter gelten weiterhin – eine durch sie ausgeschlossene Nachricht landet nie in einer Bündelung. Jede Unterhaltung hat ihr eigenes Zeitfenster, und Wiederholungen behandeln die Bündelung als eine Zustellung.
{
"type": "conversation.activity",
"data": {
"conversationId": "cm_conversation_id",
"messageIds": ["cm_first_message", "cm_second_message"]
}
}Endpunkte hinter Bearer- oder API-Key-Authentifizierung
Liegt dein Empfänger hinter einem Gateway, das einen Header verlangt, füge ihn beim Erstellen oder Bearbeiten des Endpunkts unter Eigene Header hinzu oder sende headers über die API. Die Werte werden verschlüsselt gespeichert und nie zurückgegeben – ein gespeicherter Header kommt nur mit seinem Namen zurück, und wenn du nur diesen Namen erneut sendest, bleibt der gespeicherte Wert erhalten. Sende ein leeres Array, um alle Header zu entfernen.
Wird ein Endpunkt auf einen anderen Host verschoben, entfällt diese Wiederverwendung: Die gespeicherten Werte müssen neu eingegeben werden, damit Zugangsdaten nie an ein Ziel weitergegeben werden, für das sie nicht ausgestellt wurden. Ändert sich nur der Pfad, bleiben sie erhalten.
Sonnys eigene Header haben Vorrang vor deinen, ein eigener Header kann also niemals sonny-signature, content-type oder die Header zur Zustellungsidentität ersetzen. Prüfe nach Möglichkeit lieber die Signatur: Sie authentifiziert jede Payload, während ein statisches Token nur den Aufrufer identifiziert.
{
"name": "Gateway",
"url": "https://api.example.com/hooks/sonny",
"events": ["conversation.created"],
"headers": [{ "name": "Authorization", "value": "Bearer …" }]
}Sicherheit und Zuverlässigkeit
- Signierter Roh-Body
- HMAC-SHA256 umfasst den Unix-Zeitstempel, einen Punkt und den unveränderten UTF-8-Request-Body.
- Schutz vor Replay-Angriffen
- Lehne Zeitstempel ab, die mehr als fünf Minuten in der Vergangenheit oder Zukunft liegen – auch wenn der HMAC gültig ist.
- Zuverlässige Wiederholungen
- Antworten außerhalb von 2xx werden nach 1m, 5m, 30m, 2h, 6h wiederholt. Versuch 6 ist der letzte.
Request-Vertrag
sonny-signature- t=<unix-seconds>,v1=<sha256-hex>
sonny-event- Der Ereignistyp, für schnelles Routing.
sonny-delivery-id- Eine stabile ID für Idempotenz und 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"
}
}Signatur prüfen
Lies zuerst den Roh-Body. Wenn du JSON parst und wieder serialisierst, ändern sich Leerzeichen, und eine gültige Signatur schlägt fehl.
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");
}Ereigniskatalog
webhook.heartbeatWird alle fünf Minuten an aktivierte Abonnenten gesendet (einschließlich *), auch ohne neue Nachrichten. Nutzt den normalen signierten Zustell- und Wiederholungsweg und ignoriert Unterhaltungsfilter. Richte Alarme für fehlende oder veraltete Heartbeats ein; es wird nicht nach Unterhaltungen gesucht, die auf eine Antwort warten.
{"intervalSeconds":300,"nextExpectedAt":"2026-09-29T16:05:00.000Z"}contact.createdEin Kontakt wurde erstellt.
{"contactId":"cm_contact_id"}contact.updatedEin Kontakt wurde aktualisiert oder zusammengeführt.
{"contactId":"cm_contact_id"}contact.deletedEin Kontakt wurde archiviert oder mit einem anderen Kontakt zusammengeführt. Er kann weiterhin mit archived=true gelesen werden.
{"contactId":"cm_contact_id"}contact.erasedEin Kontakt wurde endgültig gelöscht (zum Beispiel aufgrund eines DSGVO-Löschantrags), samt aller Unterhaltungen, Nachrichten und Anhänge. Er kann nicht mehr gelesen werden; lösche alle Kopien, die du aufbewahrst.
{"contactId":"cm_contact_id"}conversation.createdEine Unterhaltung wurde erstellt.
{"conversationId":"cm_conversation_id"}conversation.updatedEine Unterhaltung wurde geändert.
{"conversationId":"cm_conversation_id"}conversation.closedEine Unterhaltung wurde geschlossen.
{"conversationId":"cm_conversation_id"}conversation.deletedEine Unterhaltung wurde in den Papierkorb verschoben und ist nicht mehr über die öffentliche API verfügbar.
{"conversationId":"cm_conversation_id"}message.createdEine Nachricht oder interne Notiz wurde erstellt. Kontext und eine kurze Nachrichtenvorschau sind enthalten, sofern verfügbar; der Text interner Notizen ist nie enthalten.
{"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.updatedEine gesendete Antwort wurde im Verlauf bearbeitet. Bereits zugestellte E-Mails bleiben unverändert.
{"conversationId":"cm_conversation_id","messageId":"cm_message_id"}tag.createdEin Label wurde erstellt.
{"tagId":"cm_tag_id"}tag.updatedEin Label wurde aktualisiert.
{"tagId":"cm_tag_id"}tag.deletedEin Label wurde gelöscht.
{"tagId":"cm_tag_id"}member.invitedEin Mitglied wurde in den Arbeitsbereich eingeladen.
{"invitationId":"cm_invitation_id"}member.updatedRolle oder Status eines Mitglieds hat sich geändert.
{"memberId":"cm_membership_id"}member.removedEin Mitglied wurde entfernt.
{"memberId":"cm_membership_id"}invitation.acceptedEine Einladung in den Arbeitsbereich wurde angenommen.
{"invitationId":"cm_invitation_id","memberId":"cm_membership_id"}invitation.cancelledEine offene Einladung in den Arbeitsbereich wurde zurückgezogen.
{"invitationId":"cm_invitation_id"}conversation.activityNachrichten einer Unterhaltung, gebündelt in einer Zustellung. Enthält, sofern verfügbar, einen Snapshot jeder Nachricht. Wird an Endpunkte mit Bündelungsfenster statt message.created gesendet; kann nicht direkt abonniert werden.
{"conversationId":"cm_conversation_id","messageIds":["cm_message_id","cm_other_message_id"]}webhook.testEin von einem Administrator angefordertes Testereignis.
{"message":"This is a test webhook from Sonny."}
Zustellverhalten
- Gib innerhalb von 10 Sekunden einen beliebigen 2xx-Status zurück, damit die Zustellung als erfolgreich gilt.
- Halte deine Handler idempotent. Nutze die Ereignis-
idodersonny-delivery-id, um Duplikate zu ignorieren. - Weiterleitungen werden nicht verfolgt. Aktualisiere stattdessen die Endpunkt-URL in Sonny.
- Antwort-Bodys werden gekürzt; zur Diagnose werden nur die ersten 4 KiB aufbewahrt.
- Wähle Testen, um ein
webhook.test-Ereignis zu senden. Wähle Zustellungen, um die letzten 50 Ereignisse anzusehen. Ist eine Zustellung endgültig fehlgeschlagen, wähle Wiederholen, um sie erneut zu senden.
Einen verstummten Feed erkennen
Füge webhook.heartbeat in den Entwickler-Einstellungen oder über update_webhook zu den Ereignissen deines Endpunkts hinzu. Aktivierte Abonnenten (einschließlich *) erhalten alle fünf Minuten einen signierten Heartbeat, auch wenn keine neuen Nachrichten eingehen. Heartbeats ignorieren Kanal-, Team- und Nachrichtenfilter und enthalten keine Unterhaltungsdaten. Sie nutzen denselben Zustellweg und dieselben Wiederholungen wie Nachrichten. Prüfe createdAt und nextExpectedAt, damit ein alter Wiederholungsversuch nicht wie ein frischer Heartbeat aussieht; plane Polling- und Netzwerkverzögerungen ein, bevor du alarmierst. Ein Rückstau bei der Zustellung kann Heartbeats verzögern oder unterdrücken.
Nutze list_webhook_deliveries mit webhooks:read, um Versuche und Fehler zu prüfen. get_status prüft die Datenbankverbindung, nicht die Webhook-Zustellung. Plane unabhängig davon list_conversations mit status=open, awaitingReply=true, sort=waitingSince und direction=asc ein, um alte unbeantwortete Threads zu finden, auch während der Feed still ist.
Aktiviere Kompakte Payloads in den Entwickler-Einstellungen oder setze compact=true am Endpunkt, um Kontext-Labels wegzulassen und dabei Routing-IDs, Absender, Zeitpunkt und Nachrichtenvorschauen mit 200 Zeichen zu behalten. Das gilt auch für gebündelte Zustellungen; Texte interner Notizen bleiben ausgeschlossen. Standard-Payloads bleiben bis auf den hinzugefügten Nachrichten-Zeitstempel unverändert. Bearbeite die Berechtigungen deines bestehenden API-Schlüssels, um webhooks:read und contacts:read für Zustellprotokolle und direkte Kontaktabfragen zu erteilen. Beide Berechtigungen erfordern einen Schlüssel und einen Mitgliederzugriff für alle Kanäle; ein eingeschränkter Schlüssel lässt sich nicht allein durch zusätzliche Berechtigungen erweitern.
Verwandte Anleitungen
- Öffentliche APIBeta
Sichte und synchronisiere Unterhaltungen, teile Kundenkontext, lies Berichte und sende Dateien – mit API-Schlüsseln mit gezielten Berechtigungen.
- Posteingang
Status, Prioritäten, Zuweisung, Zurückstellen, Sammelaktionen und Tastenkürzel verstehen.
- Kontakte
So werden Kontakte in Sonny angelegt, verwaltet, mit Labels versehen und zusammengeführt.
- Team & Rollen
Lade Nutzer ein, erstelle Teams und versteh die Rechte von Inhaber, Admin, Agent und Betrachter.