Ontwikkelaars

Webhooks Beta

Ontvang duursame, ondertekende kennisgewings wanneer kontakte, gesprekke, boodskappe, etikette of spanlidmaatskap verander.

Maak die API-verwysing oop

Skep 'n eindpunt

  1. 01

    Maak Instellings oop, kies Ontwikkelaar, vind Webhooks en kies dan Voeg eindpunt by.

  2. 02

    Voer 'n Naam en openbare HTTPS-Eindpunt-URL in, en kies dan minstens een item onder Gebeurtenisse.

  3. 03

    Kies Voeg eindpunt by. Jy kan ook een skep met POST /api/v1/webhooks.

Sonny weier geloofsbriewe in URL's, localhost, privaat/skakel-plaaslike IP-reekse en DNS-name wat na 'n nie-openbare adres oplos.

Stoor die ondertekeningsgeheim dadelik

Dit begin met whsec_ en word een keer gewys. Kopieer dit na jou geheimbestuurder voordat jy Ek het dit gestoor kies.

Beperk 'n eindpunt tot gekose kanale

In Instellings → Ontwikkelaar kan elke eindpunt na alle kanale luister of net na die kanale wat jy kies, sowel wanneer jy dit byvoeg as wanneer jy dit later wysig. Eindpunte wat geskep is voordat kanaalbeperking bestaan het, bly op alle kanale totdat jy hulle verander.

API- en MCP-kliënte stel dieselfde filter met sourceIds wanneer hulle 'n eindpunt skep of bywerk. 'n Leë skikking beteken alle bronne in die werkspasie. Wanneer ID's teenwoordig is, word gespreks- en boodskapgebeurtenisse net afgelewer wanneer hul gesprek aan een van die gekose bronne behoort. Gebeurtenisse op werkspasievlak, soos kontak- of lidmaatskapveranderinge, word nie na 'n bron-gefiltreerde eindpunt gestuur nie.

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

Hou op om af te lewer wat jou integrasie weggooi

'n Agent wat deur die API antwoord, word deur sy eie antwoord wakker gemaak, tensy jy anders sê. Drie opsionele filters vernou wat 'n eindpunt ontvang; almal is by verstek af, so 'n bestaande eindpunt bly onveranderd.

agentGroupIds
Net gesprekke wat deur die gelyste agentgroepe besit word. Dit volg eienaarskap, nie die kanaal waardeur 'n gesprek aangekom het nie, so 'n gesprek uit 'n gedeelde inkassie bereik die groep wat dit besit. Gesprekke sonder 'n groep word nie afgelewer nie, en omdat groepe gewoonlik toegewys word nadat 'n gesprek begin het, vuur conversation.created dikwels voordat daar 'n groep is om by te pas.
customerMessagesOnly
Net boodskappe wat deur kliënte geskryf is. Slaan antwoorde van jou span, interne notas en enigiets wat deur die API, MCP of die KI-antwoorder gestuur is oor — insluitend hierdie eindpunt se eie antwoorde.
excludedSenderIds
Slaan boodskappe oor wat deur die gelyste spanmaats gestuur is. Gee 'n integrasie sy eie spanmaatrekening en sluit dit uit sodat dit nie deur sy eie antwoorde wakker gemaak word nie, terwyl dit steeds hoor wanneer 'n mens die gesprek oorneem — die sein wat 'n KI-agent nodig het om terug te staan.

Die boodskapfilters geld net vir message.*-gebeurtenisse; gebeurtenistipes bly die kontrole vir alles anders. 'n Gefiltreerde gebeurtenis word laat val voordat dit 'n aflewering word, so dit kos jou niks en word nooit as 'n mislukking gewys nie. Die vrag en apiVersion bly in albei gevalle onveranderd.

Groepeer 'n stortvloed boodskappe in een aflewering

'n Verbruiker wat die hele draad lees wanneer dit wakker word, wen niks uit vier aparte aflewerings in tien sekondes nie. Stel coalesceSeconds en die boodskappe op een gesprek word so lank versamel, en dan as een aflewering gestuur. Dit is by verstek af; eindpunte daarsonder ontvang steeds elke boodskap afsonderlik.

'n Gegroepeerde aflewering kom aan as conversation.activity met die gesprek en elke versamelde boodskap-ID, so lees die draad een keer eerder as per boodskap. Dit is die een instelling wat die vorm verander van wat jy ontvang, en daarom moet jy self daarvoor kies. Jou ander filters geld steeds — 'n boodskap wat deur hulle uitgesluit word, sluit nooit by 'n groep aan nie. Elke gesprek het sy eie venster, en herprobeerslae behandel die groep as een aflewering.

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

Eindpunte agter bearer- of API-sleutelverifikasie

As jou ontvanger agter 'n poort sit wat 'n kopstuk vereis, voeg dit by onder Pasgemaakte kopstukke wanneer jy die eindpunt skep of wysig, of stuur headers vanaf die API. Waardes word in rus geënkripteer en nooit teruggegee nie — 'n gestoorde kopstuk kom net as 'n naam terug, en om daardie naam op sy eie weer in te dien, behou die gestoorde waarde. Stuur 'n leë skikking om elke kopstuk te verwyder.

Om 'n eindpunt na 'n ander gasheer te skuif, kanselleer die hergebruik: die gestoorde waardes moet weer ingevoer word, sodat 'n geloofsbrief nooit aangestuur word na 'n bestemming waarvoor dit nie uitgereik is nie. Om net die pad te verander, behou hulle.

Sonny se eie kopstukke wen bo joune, so 'n pasgemaakte kopstuk kan nooit sonny-signature, content-type of die afleweringsidentiteit-kopstukke vervang nie. Verifieer eerder die handtekening waar jy kan: dit verifieer elke vrag, terwyl 'n statiese token net die oproeper identifiseer.

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

Sekuriteit en betroubaarheid

Ondertekende rou liggaam
HMAC-SHA256 dek die Unix-tydstempel, 'n punt en die onaangeraakte UTF-8-versoekliggaam.
Beskerming teen herspeel
Weier tydstempels meer as vyf minute in die verlede of toekoms, selfs wanneer die HMAC geldig is.
Duursame herprobeerslae
Nie-2xx-antwoorde word weer probeer ná 1m, 5m, 30m, 2h, 6h. Poging 6 is die laaste.

Versoekkontrak

sonny-signature
t=<unix-seconds>,v1=<sha256-hex>
sonny-event
Die gebeurtenistipe, vir vinnige roetering.
sonny-delivery-id
'n Stabiele ID vir idempotensie en ondersteuning.
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"
  }
}

Verifieer die handtekening

Lees eers die rou liggaam. Om JSON te ontleed en weer te serialiseer, verander witspasie en laat 'n geldige handtekening misluk.

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");
}

Gebeurteniskatalogus

webhook.heartbeat

Word elke vyf minute aan aangeskakelde intekenaars (insluitend *) gestuur, selfs sonder nuwe boodskappe. Gebruik die normale ondertekende aflewerings-/herprobeerpad en ignoreer gespreksfilters. Gee 'n waarskuwing by ontbrekende of verouderde hartslae; dit soek nie na gesprekke wat op 'n antwoord wag nie.

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

'n Kontak is geskep.

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

'n Kontak is bygewerk of saamgevoeg.

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

'n Kontak is geargiveer of met 'n ander kontak saamgevoeg. Dit kan steeds met archived=true gelees word.

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

'n Kontak is permanent uitgevee (byvoorbeeld 'n GDPR-uitveeversoek) met al sy gesprekke, boodskappe en aanhegsels. Dit kan nie meer gelees word nie; skrap enige kopieë wat jy hou.

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

'n Gesprek is geskep.

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

'n Gesprek het verander.

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

'n Gesprek is gesluit.

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

'n Gesprek is na die asblik geskuif en het die openbare API verlaat.

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

'n Boodskap of interne nota is geskep. Konteks en 'n kort boodskapvoorskou word ingesluit wanneer beskikbaar; die teks van interne notas word nooit ingesluit nie.

{"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

'n Gestuurde antwoord is in die transkripsie gewysig. Afgelewerde e-posse bly onveranderd.

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

'n Etiket is geskep.

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

'n Etiket is bygewerk.

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

'n Etiket is geskrap.

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

'n Werkspasielid is genooi.

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

'n Lid se rol of status het verander.

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

'n Lid is verwyder.

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

'n Werkspasie-uitnodiging is aanvaar.

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

'n Hangende werkspasie-uitnodiging is gekanselleer.

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

Boodskappe in een gesprek, in 'n enkele aflewering gegroepeer. Sluit 'n momentopname van elke boodskap in wanneer beskikbaar. Word in plaas van message.created gestuur na eindpunte met 'n groeperingsvenster; jy teken nooit direk daarop in nie.

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

'n Toetsgebeurtenis wat deur 'n administrateur aangevra is.

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

Afleweringsgedrag

  • Gee enige 2xx-status binne 10 sekondes terug om die aflewering as suksesvol te merk.
  • Hou hanteerders idempotent. Gebruik die gebeurtenis se id of sonny-delivery-id om duplikate te ignoreer.
  • Herleidings word nie gevolg nie. Werk eerder die eindpunt-URL in Sonny by.
  • Antwoordliggame is beperk en net die eerste 4 KiB word vir diagnose behou.
  • Kies Toets om 'n webhook.test-gebeurtenis te stuur. Kies Aflewerings om die mees onlangse 50 gebeurtenisse te ondersoek. Wanneer 'n aflewering 'n finale mislukking bereik, kies Probeer weer om dit weer te stuur.

Bespeur 'n stil voer

Voeg webhook.heartbeat by jou eindpunt se gebeurtenisse in Ontwikkelaar-instellings, of deur update_webhook. Aangeskakelde intekenaars (insluitend *) ontvang elke vyf minute 'n ondertekende hartklop, selfs wanneer geen nuwe boodskappe aankom nie. Hartkloppe ignoreer kanaal-, span- en boodskapfilters en bevat geen gespreksdata nie. Hulle gebruik dieselfde afleweringspad en herprobeerslae as boodskappe. Gaan createdAt en nextExpectedAt na sodat 'n ou herprobeerslag nie soos 'n vars hartklop kan lyk nie; maak voorsiening vir peiling- en netwerkvertragings voordat jy 'n waarskuwing gee. 'n Agterstand in aflewerings kan hartkloppe vertraag of onderdruk.

Gebruik list_webhook_deliveries met webhooks:read om pogings en mislukkings te ondersoek. get_status gaan databasisverbinding na, nie webhook-aflewering nie. Skeduleer onafhanklik list_conversations met status=open, awaitingReply=true, sort=waitingSince en direction=asc om ou onbeantwoorde drade te vind, selfs terwyl die voer stil is.

Skakel Kompakte vragte in Ontwikkelaar-instellings aan of stel compact=true op die eindpunt om konteksetikette weg te laat terwyl roeterings-ID's, sender, tyd en boodskapvoorskoue van 200 karakters behou word. Dit geld ook vir gegroepeerde aflewerings; interne notateks bly uitgesluit. Verstekvragte bly onveranderd behalwe vir die bygevoegde boodskaptydstempel. Wysig jou bestaande API-sleutel se bestekke om webhooks:read en contacts:read toe te staan vir afleweringslogs en direkte kontaksoektogte. Albei bestekke vereis 'n sleutel vir alle kanale en lidtoegang tot alle kanale; 'n beperkte sleutel kan nie verbreed word deur net toestemmings by te voeg nie.

Verwante dokumentasie