Ontwikkelaars
Webhooks Beta
Ontvang betrouwbare, ondertekende meldingen wanneer contacten, gesprekken, berichten, labels of teamleden veranderen.
Open de API-referentieEen endpoint aanmaken
- 01
Open Instellingen, kies Ontwikkelaar, zoek Webhooks en kies Endpoint toevoegen.
- 02
Vul een Naam en een openbare HTTPS-Endpoint-URL in en kies minstens één item onder Events.
- 03
Kies Endpoint toevoegen. Je kunt er ook een aanmaken met
POST /api/v1/webhooks.
Sonny weigert inloggegevens in URL's, localhost, privé- en link-local IP-bereiken en DNS-namen die naar een niet-openbaar adres verwijzen.
Bewaar het ondertekeningsgeheim meteen
Het begint met whsec_ en wordt maar één keer getoond. Kopieer het naar je geheimenbeheer voordat je Ik heb hem bewaard kiest.
Een endpoint beperken tot geselecteerde kanalen
In Instellingen → Ontwikkelaar kan elk endpoint naar alle kanalen luisteren of alleen naar de kanalen die jij kiest, zowel bij het toevoegen als later bij het bewerken. Endpoints die zijn aangemaakt voordat kanaalbeperking bestond, blijven op alle kanalen staan tot je ze wijzigt.
API- en MCP-clients stellen hetzelfde filter in met sourceIds bij het aanmaken of bijwerken van een endpoint. Een lege array betekent alle bronnen in de werkruimte. Zijn er ID's opgegeven, dan worden gespreks- en bericht-events alleen afgeleverd als het gesprek bij een van de geselecteerde bronnen hoort. Events op werkruimteniveau, zoals wijzigingen in contacten of lidmaatschappen, worden niet naar een op bron gefilterd endpoint gestuurd.
{
"name": "Product A agent",
"url": "https://agent.example.com/sonny",
"events": ["conversation.created", "message.created"],
"sourceIds": ["cm_source_id"]
}Lever niet af wat je integratie toch weggooit
Een agent die via de API antwoordt, wordt door zijn eigen antwoord gewekt, tenzij je anders aangeeft. Drie optionele filters beperken wat een endpoint ontvangt; ze staan standaard allemaal uit, dus een bestaand endpoint verandert niet.
agentGroupIds- Alleen gesprekken die bij de opgegeven medewerkersgroepen horen. Dit volgt het eigenaarschap, niet het kanaal waarlangs een gesprek binnenkwam, dus een gesprek uit een gedeelde inbox komt bij de groep die het beheert. Gesprekken zonder groep worden niet afgeleverd. Omdat groepen meestal pas worden toegewezen nadat een gesprek is gestart, komt
conversation.createdvaak binnen voordat er een groep is om op te matchen. customerMessagesOnly- Alleen berichten van klanten. Slaat antwoorden van je team, interne notities en alles wat via de API, MCP of de AI-responder is verzonden over — inclusief de eigen antwoorden van dit endpoint.
excludedSenderIds- Slaat berichten van de opgegeven teamleden over. Geef een integratie een eigen teamlidaccount en sluit dat uit, zodat de integratie niet wakker wordt van haar eigen antwoorden maar wel merkt wanneer een mens het gesprek overneemt — precies het signaal dat een AI-agent nodig heeft om zich terug te trekken.
De berichtfilters gelden alleen voor message.*-events; voor al het andere bepalen de eventtypes wat je ontvangt. Een gefilterd event wordt weggelaten voordat het een aflevering wordt, dus het kost je niets en verschijnt nooit als mislukt. De payload en apiVersion blijven in beide gevallen gelijk.
Een reeks berichten bundelen tot één aflevering
Een consumer die bij het wakker worden de hele thread leest, heeft niets aan vier losse afleveringen in tien seconden. Stel coalesceSeconds in, dan worden de berichten in één gesprek zo lang verzameld en daarna als één aflevering verstuurd. Dit staat standaard uit; endpoints zonder deze instelling blijven elk bericht afzonderlijk ontvangen.
Een gebundelde aflevering komt binnen als conversation.activity, met het gesprek en elk verzameld bericht-ID. Lees de thread dus één keer in plaats van per bericht. Dit is de enige instelling die de vorm verandert van wat je ontvangt, en daarom moet je hem zelf aanzetten. Je andere filters blijven gelden — een bericht dat daardoor wordt uitgesloten, komt nooit in een bundel. Elk gesprek heeft een eigen venster, en bij nieuwe pogingen telt de bundel als één aflevering.
{
"type": "conversation.activity",
"data": {
"conversationId": "cm_conversation_id",
"messageIds": ["cm_first_message", "cm_second_message"]
}
}Endpoints achter bearer- of API-sleutel-authenticatie
Staat je ontvanger achter een gateway die een header vereist? Voeg die toe onder Aangepaste headers bij het aanmaken of bewerken van het endpoint, of stuur headers mee via de API. Waarden worden versleuteld opgeslagen en nooit teruggegeven — een opgeslagen header komt terug als alleen een naam, en als je alleen die naam opnieuw instuurt, blijft de opgeslagen waarde behouden. Stuur een lege array om alle headers te verwijderen.
Verplaats je een endpoint naar een andere host, dan vervalt dat hergebruik: je moet de opgeslagen waarden opnieuw invullen, zodat inloggegevens nooit worden doorgestuurd naar een bestemming waarvoor ze niet bedoeld zijn. Wijzig je alleen het pad, dan blijven ze behouden.
De headers van Sonny gaan voor die van jou, dus een aangepaste header kan nooit sonny-signature, content-type of de identiteitsheaders van de aflevering vervangen. Verifieer waar mogelijk liever de handtekening: die authenticeert elke payload, terwijl een vaste token alleen de aanroeper identificeert.
{
"name": "Gateway",
"url": "https://api.example.com/hooks/sonny",
"events": ["conversation.created"],
"headers": [{ "name": "Authorization", "value": "Bearer …" }]
}Beveiliging en betrouwbaarheid
- Ondertekende ruwe body
- HMAC-SHA256 dekt de Unix-tijdstempel, een punt en de onaangeroerde UTF-8-requestbody.
- Bescherming tegen replay
- Weiger tijdstempels die meer dan vijf minuten in het verleden of de toekomst liggen, ook als de HMAC geldig is.
- Betrouwbare nieuwe pogingen
- Bij een niet-2xx-response volgt een nieuwe poging na 1m, 5m, 30m, 2h, 6h. Poging 6 is de laatste.
Requestcontract
sonny-signature- t=<unix-seconds>,v1=<sha256-hex>
sonny-event- Het eventtype, voor snelle routering.
sonny-delivery-id- Een vaste ID voor idempotentie en 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"
}
}De handtekening verifiëren
Lees eerst de ruwe body. Als je de JSON parseert en opnieuw serialiseert, verandert de witruimte en faalt een geldige handtekening.
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");
}Eventcatalogus
webhook.heartbeatWordt elke vijf minuten verzonden naar ingeschakelde abonnees (inclusief *), ook zonder nieuwe berichten. Gebruikt de normale ondertekende aflever- en herhaalroute en negeert gespreksfilters. Stel een alert in op ontbrekende of verouderde heartbeats; er wordt niet gezocht naar gesprekken die op een antwoord wachten.
{"intervalSeconds":300,"nextExpectedAt":"2026-09-29T16:05:00.000Z"}contact.createdEr is een contact aangemaakt.
{"contactId":"cm_contact_id"}contact.updatedEen contact is bijgewerkt of samengevoegd.
{"contactId":"cm_contact_id"}contact.deletedEen contact is gearchiveerd of samengevoegd met een ander contact. Het kan nog steeds worden gelezen met archived=true.
{"contactId":"cm_contact_id"}contact.erasedEen contact is definitief gewist (bijvoorbeeld na een AVG-verwijderverzoek), met al zijn gesprekken, berichten en bijlagen. Het kan niet meer worden gelezen; verwijder eventuele kopieën die je bewaart.
{"contactId":"cm_contact_id"}conversation.createdEr is een gesprek aangemaakt.
{"conversationId":"cm_conversation_id"}conversation.updatedEen gesprek is gewijzigd.
{"conversationId":"cm_conversation_id"}conversation.closedEen gesprek is gesloten.
{"conversationId":"cm_conversation_id"}conversation.deletedEen gesprek is naar de prullenbak verplaatst en niet meer beschikbaar in de openbare API.
{"conversationId":"cm_conversation_id"}message.createdEr is een bericht of interne notitie aangemaakt. Context en een kort berichtvoorbeeld worden meegestuurd als die beschikbaar zijn; de tekst van interne notities wordt nooit meegestuurd.
{"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.updatedEen verzonden antwoord is bewerkt in het transcript. Afgeleverde e-mails blijven ongewijzigd.
{"conversationId":"cm_conversation_id","messageId":"cm_message_id"}tag.createdEr is een label aangemaakt.
{"tagId":"cm_tag_id"}tag.updatedEen label is bijgewerkt.
{"tagId":"cm_tag_id"}tag.deletedEen label is verwijderd.
{"tagId":"cm_tag_id"}member.invitedEr is een lid uitgenodigd voor de werkruimte.
{"invitationId":"cm_invitation_id"}member.updatedDe rol of status van een lid is gewijzigd.
{"memberId":"cm_membership_id"}member.removedEen lid is verwijderd.
{"memberId":"cm_membership_id"}invitation.acceptedEen uitnodiging voor de werkruimte is geaccepteerd.
{"invitationId":"cm_invitation_id","memberId":"cm_membership_id"}invitation.cancelledEen openstaande uitnodiging voor de werkruimte is geannuleerd.
{"invitationId":"cm_invitation_id"}conversation.activityBerichten in één gesprek, gegroepeerd tot één aflevering. Bevat een momentopname van elk bericht als die beschikbaar is. Wordt verzonden in plaats van message.created naar endpoints met een groeperingsvenster; je kunt je er niet rechtstreeks op abonneren.
{"conversationId":"cm_conversation_id","messageIds":["cm_message_id","cm_other_message_id"]}webhook.testEen testevent dat door een beheerder is aangevraagd.
{"message":"This is a test webhook from Sonny."}
Gedrag bij aflevering
- Geef binnen 10 seconden een willekeurige 2xx-status terug om de aflevering als geslaagd te markeren.
- Houd handlers idempotent. Gebruik de event-
idofsonny-delivery-idom dubbele events te negeren. - Redirects worden niet gevolgd. Werk in plaats daarvan de endpoint-URL in Sonny bij.
- Responsebodies zijn begrensd: alleen de eerste 4 KiB wordt bewaard voor diagnose.
- Kies Testen om een
webhook.test-event te versturen. Kies Afleveringen om de laatste 50 events te bekijken. Is een aflevering definitief mislukt, kies dan Opnieuw om hem nogmaals te versturen.
Een stille feed opmerken
Voeg webhook.heartbeat toe aan de events van je endpoint in de Ontwikkelaar-instellingen, of via update_webhook. Ingeschakelde abonnees (inclusief *) ontvangen elke vijf minuten een ondertekende heartbeat, ook als er geen nieuwe berichten binnenkomen. Heartbeats negeren kanaal-, team- en berichtfilters en bevatten geen gespreksgegevens. Ze gebruiken dezelfde afleverroute en nieuwe pogingen als berichten. Controleer createdAt en nextExpectedAt, zodat een oude nieuwe poging er niet uitziet als een verse heartbeat; houd rekening met polling- en netwerkvertraging voordat je een alert stuurt. Een achterstand in afleveringen kan heartbeats vertragen of tegenhouden.
Gebruik list_webhook_deliveries met webhooks:read om pogingen en fouten te bekijken. get_status controleert de databaseverbinding, niet de webhookaflevering. Plan daarnaast los list_conversations in met status=open, awaitingReply=true, sort=waitingSince en direction=asc, zodat je oude onbeantwoorde threads vindt, ook als de feed stil is.
Zet Compacte payloads aan in de Ontwikkelaar-instellingen of stel compact=true in op het endpoint om contextlabels weg te laten, terwijl routing-ID's, afzender, tijd en berichtvoorbeelden van 200 tekens behouden blijven. Dit geldt ook voor gebundelde afleveringen; de tekst van interne notities blijft uitgesloten. Standaardpayloads blijven gelijk, op de toegevoegde tijdstempel van het bericht na. Bewerk de rechten van je bestaande API-sleutel om webhooks:read en contacts:read toe te kennen voor afleverlogs en het direct opzoeken van contacten. Beide rechten vereisen een sleutel voor alle kanalen en een lid met toegang tot alle kanalen; een beperkte sleutel kun je niet verruimen door alleen rechten toe te voegen.
Gerelateerde handleidingen
- Openbare APIBeta
Sorteer en synchroniseer gesprekken, deel klantcontext, lees rapportages en stuur bestanden met API-sleutels met beperkte rechten.
- Inbox
Begrijp statussen, prioriteiten, toewijzen, snoozen, bulkacties en sneltoetsen.
- Contacten
Lees hoe contacten in Sonny worden aangemaakt, beheerd, gelabeld en samengevoegd.
- Team en rollen
Nodig gebruikers uit, maak teams aan en begrijp de toegang van Eigenaar, Beheerder, Medewerker en Kijker.