นักพัฒนา
เว็บฮุก Beta
รับการแจ้งเตือนที่มีลายเซ็นกำกับและเชื่อถือได้ เมื่อผู้ติดต่อ บทสนทนา ข้อความ แท็ก หรือสมาชิกในทีมมีการเปลี่ยนแปลง
เปิดเอกสารอ้างอิง APIสร้างเอนด์พอยต์
- 01
เปิด การตั้งค่า เลือก นักพัฒนา หา เว็บฮุก แล้วเลือก เพิ่มเอนด์พอยต์
- 02
ใส่ ชื่อ และ URL ของเอนด์พอยต์ ที่เป็น HTTPS สาธารณะ จากนั้นเลือกอย่างน้อยหนึ่งรายการใน อีเวนต์
- 03
เลือก เพิ่มเอนด์พอยต์ หรือคุณจะสร้างด้วย
POST /api/v1/webhooksก็ได้
Sonny จะปฏิเสธ URL ที่มีข้อมูลเข้าสู่ระบบ, localhost, ช่วง IP แบบส่วนตัวหรือ link-local และชื่อ DNS ที่ชี้ไปยังที่อยู่ที่ไม่ใช่สาธารณะ
บันทึกคีย์ลับสำหรับลงนามทันที
คีย์นี้ขึ้นต้นด้วย whsec_ และจะแสดงเพียงครั้งเดียว คัดลอกไปเก็บในระบบจัดการข้อมูลลับของคุณก่อนเลือก ฉันบันทึกแล้ว
จำกัดเอนด์พอยต์ไว้เฉพาะช่องทางที่เลือก
ใน การตั้งค่า → นักพัฒนา แต่ละเอนด์พอยต์รับฟังได้ทุกช่องทางหรือเฉพาะช่องทางที่คุณเลือก ทั้งตอนเพิ่มและตอนแก้ไขภายหลัง เอนด์พอยต์ที่สร้างก่อนจะมีการจำกัดช่องทางจะยังรับทุกช่องทางจนกว่าคุณจะเปลี่ยน
ไคลเอนต์ API และ MCP ตั้งตัวกรองเดียวกันได้ด้วย sourceIds เมื่อสร้างหรืออัปเดตเอนด์พอยต์ อาร์เรย์ว่างหมายถึงทุกช่องทางในเวิร์กสเปซ เมื่อระบุ ID ไว้ อีเวนต์ของบทสนทนาและข้อความจะส่งเฉพาะเมื่อบทสนทนานั้นอยู่ในช่องทางที่เลือก อีเวนต์ระดับเวิร์กสเปซ เช่น การเปลี่ยนแปลงผู้ติดต่อหรือสมาชิก จะไม่ส่งไปยังเอนด์พอยต์ที่กรองตามช่องทาง
{
"name": "Product A agent",
"url": "https://agent.example.com/sonny",
"events": ["conversation.created", "message.created"],
"sourceIds": ["cm_source_id"]
}หยุดส่งสิ่งที่การเชื่อมต่อของคุณทิ้งไปอยู่แล้ว
เอเจนต์ที่ตอบกลับผ่าน API จะถูกปลุกจากคำตอบของตัวเอง เว้นแต่คุณจะกำหนดเป็นอย่างอื่น มีตัวกรองที่เลือกใช้ได้สามตัวเพื่อจำกัดสิ่งที่เอนด์พอยต์ได้รับ ทั้งหมดปิดไว้เป็นค่าเริ่มต้น เอนด์พอยต์ที่มีอยู่แล้วจึงไม่เปลี่ยนแปลง
agentGroupIds- เฉพาะบทสนทนาที่กลุ่มเจ้าหน้าที่ในรายการเป็นเจ้าของ ตัวกรองนี้อิงตามความเป็นเจ้าของ ไม่ใช่ช่องทางที่บทสนทนาเข้ามา บทสนทนาจากกล่องข้อความที่ใช้ร่วมกันจึงไปถึงกลุ่มที่เป็นเจ้าของ บทสนทนาที่ไม่มีกลุ่มจะไม่ถูกส่ง และเนื่องจากโดยปกติกลุ่มจะถูกกำหนดหลังจากบทสนทนาเริ่มขึ้นแล้ว
conversation.createdจึงมักเกิดขึ้นก่อนที่จะมีกลุ่มให้จับคู่ customerMessagesOnly- เฉพาะข้อความที่ลูกค้าเขียน ข้ามคำตอบจากทีมของคุณ โน้ตภายใน และทุกอย่างที่ส่งผ่าน API, MCP หรือ AI ตอบกลับอัตโนมัติ รวมถึงคำตอบของเอนด์พอยต์นี้เอง
excludedSenderIds- ข้ามข้อความที่ส่งโดยสมาชิกทีมในรายการ สร้างบัญชีสมาชิกทีมแยกให้การเชื่อมต่อแล้วยกเว้นบัญชีนั้น เพื่อไม่ให้ถูกปลุกจากคำตอบของตัวเอง แต่ยังรับรู้ได้เมื่อมีคนเข้ามารับช่วงบทสนทนาต่อ ซึ่งเป็นสัญญาณที่เอเจนต์ AI ต้องใช้เพื่อถอยออก
ตัวกรองข้อความมีผลกับอีเวนต์ message.* เท่านั้น ส่วนอีเวนต์อื่นๆ ยังควบคุมด้วยประเภทอีเวนต์ตามเดิม อีเวนต์ที่ถูกกรองจะถูกทิ้งก่อนกลายเป็นการส่ง จึงไม่มีค่าใช้จ่ายและไม่แสดงเป็นความล้มเหลว เพย์โหลดและ apiVersion ไม่เปลี่ยนแปลงไม่ว่าในกรณีใด
รวมข้อความที่เข้ามาติดๆ กันเป็นการส่งครั้งเดียว
ระบบที่อ่านทั้งเธรดเมื่อถูกปลุกจะไม่ได้ประโยชน์อะไรจากการส่งสี่ครั้งแยกกันภายในสิบวินาที ตั้งค่า coalesceSeconds แล้วข้อความในบทสนทนาเดียวกันจะถูกรวบรวมไว้ตามระยะเวลานั้น จากนั้นส่งเป็นการส่งครั้งเดียว ตัวเลือกนี้ปิดไว้เป็นค่าเริ่มต้น เอนด์พอยต์ที่ไม่ได้ตั้งค่าจะยังได้รับแต่ละข้อความแยกกันตามเดิม
การส่งแบบรวมกลุ่มจะมาถึงเป็น conversation.activity พร้อมบทสนทนาและ ID ของทุกข้อความที่รวบรวมไว้ ให้อ่านเธรดเพียงครั้งเดียวแทนการอ่านทีละข้อความ นี่คือการตั้งค่าเดียวที่เปลี่ยนรูปแบบของสิ่งที่คุณได้รับ จึงต้องเลือกเปิดใช้เอง ตัวกรองอื่นๆ ของคุณยังมีผลอยู่ ข้อความที่ถูกตัวกรองยกเว้นจะไม่ถูกรวมเข้ากลุ่ม แต่ละบทสนทนามีช่วงเวลาของตัวเอง และการลองส่งใหม่จะถือว่ากลุ่มเป็นการส่งครั้งเดียว
{
"type": "conversation.activity",
"data": {
"conversationId": "cm_conversation_id",
"messageIds": ["cm_first_message", "cm_second_message"]
}
}เอนด์พอยต์ที่ใช้การยืนยันตัวตนแบบ bearer หรือคีย์ API
หากผู้รับของคุณอยู่หลังเกตเวย์ที่ต้องใช้เฮดเดอร์ ให้เพิ่มเฮดเดอร์ใน เฮดเดอร์ที่กำหนดเอง เมื่อสร้างหรือแก้ไขเอนด์พอยต์ หรือส่ง headers ผ่าน API ค่าจะถูกเข้ารหัสขณะจัดเก็บและไม่ถูกส่งกลับมา เฮดเดอร์ที่บันทึกไว้จะส่งกลับมาเพียงชื่อ และการส่งชื่อนั้นซ้ำโดยไม่มีค่าจะคงค่าที่จัดเก็บไว้ ส่งอาร์เรย์ว่างเพื่อนำเฮดเดอร์ทั้งหมดออก
การย้ายเอนด์พอยต์ไปยังโฮสต์อื่นจะยกเลิกการใช้ค่าเดิมซ้ำ ต้องใส่ค่าที่บันทึกไว้ใหม่อีกครั้ง เพื่อไม่ให้ข้อมูลเข้าสู่ระบบถูกส่งต่อไปยังปลายทางที่ไม่ได้ออกให้ การเปลี่ยนเฉพาะพาธจะคงค่าไว้
เฮดเดอร์ของ Sonny มีลำดับความสำคัญเหนือเฮดเดอร์ของคุณ เฮดเดอร์ที่กำหนดเองจึงไม่สามารถแทนที่ sonny-signature, content-type หรือเฮดเดอร์ระบุตัวตนของการส่งได้ หากทำได้ ควรตรวจสอบลายเซ็นเป็นหลัก เพราะลายเซ็นยืนยันความถูกต้องของทุกเพย์โหลด ในขณะที่โทเค็นแบบคงที่ระบุได้เพียงว่าใครเป็นผู้เรียก
{
"name": "Gateway",
"url": "https://api.example.com/hooks/sonny",
"events": ["conversation.created"],
"headers": [{ "name": "Authorization", "value": "Bearer …" }]
}ความปลอดภัยและความน่าเชื่อถือ
- ลงนามบนเนื้อหาดิบ
- HMAC-SHA256 ครอบคลุม Unix timestamp จุดหนึ่งตัว และเนื้อหาคำขอ UTF-8 ที่ไม่ถูกแก้ไข
- การป้องกันการส่งซ้ำ
- ปฏิเสธ timestamp ที่ห่างจากปัจจุบันเกินห้านาทีทั้งในอดีตหรืออนาคต แม้ว่า HMAC จะถูกต้องก็ตาม
- การลองส่งใหม่ที่เชื่อถือได้
- การตอบกลับที่ไม่ใช่ 2xx จะลองส่งใหม่หลังจาก 1m, 5m, 30m, 2h, 6h ความพยายามครั้งที่ 6 เป็นครั้งสุดท้าย
ข้อกำหนดของคำขอ
sonny-signature- t=<unix-seconds>,v1=<sha256-hex>
sonny-event- ประเภทอีเวนต์ สำหรับการกำหนดเส้นทางอย่างรวดเร็ว
sonny-delivery-id- ID ที่คงที่ สำหรับ idempotency และการติดต่อฝ่ายสนับสนุน
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"
}
}ตรวจสอบลายเซ็น
อ่านเนื้อหาดิบก่อน การแยกวิเคราะห์ JSON แล้วแปลงกลับเป็นข้อความใหม่จะทำให้ช่องว่างเปลี่ยนไป และทำให้ลายเซ็นที่ถูกต้องตรวจสอบไม่ผ่าน
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");
}รายการอีเวนต์
webhook.heartbeatส่งทุกห้านาทีไปยังผู้สมัครรับที่เปิดใช้งาน (รวมถึง *) แม้ไม่มีข้อความใหม่ ใช้เส้นทางการส่งและการลองส่งใหม่แบบมีลายเซ็นตามปกติ และไม่สนใจตัวกรองบทสนทนา ตั้งการแจ้งเตือนเมื่อสัญญาณ heartbeat หายไปหรือล่าช้า ทั้งนี้อีเวนต์นี้ไม่ได้ตรวจหาบทสนทนาที่รอการตอบกลับ
{"intervalSeconds":300,"nextExpectedAt":"2026-09-29T16:05:00.000Z"}contact.createdมีการสร้างผู้ติดต่อ
{"contactId":"cm_contact_id"}contact.updatedมีการอัปเดตหรือรวมผู้ติดต่อ
{"contactId":"cm_contact_id"}contact.property_updatedค่าฟิลด์ของผู้ติดต่อมีการเปลี่ยนแปลง โดยสมาชิกทีม API หรือแบบฟอร์มเป็นผู้ตั้งค่า ส่งเฉพาะเมื่อค่าเปลี่ยนจริง (การเลือกค่าเดิมจะไม่ส่งอะไร) และมีค่าก่อนหน้ารวมอยู่ด้วย โดย null หมายถึงยังไม่ได้ตั้งค่า ค่าที่วิดเจ็ตบนเว็บไซต์กรอกให้ (identify) จะไม่ส่งอีเวนต์นี้ ผู้ติดต่อใหม่ที่เริ่มต้นด้วยค่าเริ่มต้นของฟิลด์แบบตัวเลือกก็จะส่งอีเวนต์นี้เช่นกัน โดยค่าก่อนหน้าเป็น null
{"contactId":"cm_contact_id","propertyId":"cm_property_id","propertyName":"Stage","previousValue":"Prospect","value":"Qualified"}contact.deletedผู้ติดต่อถูกเก็บถาวรหรือถูกรวมเข้ากับผู้ติดต่ออื่น ยังอ่านข้อมูลได้ด้วย archived=true
{"contactId":"cm_contact_id"}contact.erasedผู้ติดต่อถูกลบอย่างถาวร (เช่น ตามคำขอลบข้อมูลภายใต้ GDPR) พร้อมบทสนทนา ข้อความ และไฟล์แนบทั้งหมด จะไม่สามารถอ่านข้อมูลได้อีก โปรดลบสำเนาที่คุณเก็บไว้
{"contactId":"cm_contact_id"}conversation.createdมีการสร้างบทสนทนา
{"conversationId":"cm_conversation_id"}conversation.updatedบทสนทนามีการเปลี่ยนแปลง
{"conversationId":"cm_conversation_id"}conversation.closedบทสนทนาถูกปิด
{"conversationId":"cm_conversation_id"}conversation.deletedบทสนทนาถูกย้ายไปที่ถังขยะและออกจาก API สาธารณะแล้ว
{"conversationId":"cm_conversation_id"}message.createdมีการสร้างข้อความหรือโน้ตภายใน โดยจะมีบริบท ชื่อ อีเมล และเบอร์โทรศัพท์ของผู้ติดต่อ รวมถึงตัวอย่างข้อความสั้นๆ เมื่อมีข้อมูล แต่จะไม่รวมเนื้อหาของโน้ตภายในเด็ดขาด
{"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"},"contact":{"id":"cm_contact_id","email":"shiney@example.com","phone":"27821234567","name":"Shiney"},"message":{"id":"cm_message_id","type":"incoming","senderType":"human","senderId":null,"senderName":"Shiney","textPreview":"Could you check my bill?","textTruncated":false}}message.updatedคำตอบที่ส่งไปแล้วถูกแก้ไขในบันทึกบทสนทนา อีเมลที่ส่งถึงผู้รับแล้วจะไม่เปลี่ยนแปลง
{"conversationId":"cm_conversation_id","messageId":"cm_message_id"}tag.createdมีการสร้างแท็ก
{"tagId":"cm_tag_id"}tag.updatedมีการอัปเดตแท็ก
{"tagId":"cm_tag_id"}tag.deletedมีการลบแท็ก
{"tagId":"cm_tag_id"}member.invitedมีการเชิญสมาชิกเวิร์กสเปซ
{"invitationId":"cm_invitation_id"}member.updatedบทบาท สถานะ สิทธิ์เข้าถึงช่องทาง ชื่อ ตำแหน่งงาน หรือการตั้งค่าการมอบหมายของสมาชิกมีการเปลี่ยนแปลง
{"memberId":"cm_membership_id"}member.removedมีการนำสมาชิกออก
{"memberId":"cm_membership_id"}invitation.acceptedมีการตอบรับคำเชิญเข้าเวิร์กสเปซ
{"invitationId":"cm_invitation_id","memberId":"cm_membership_id"}invitation.cancelledคำเชิญเข้าเวิร์กสเปซที่รอดำเนินการถูกยกเลิก
{"invitationId":"cm_invitation_id"}conversation.activityข้อความในบทสนทนาเดียวกันที่รวมกลุ่มเป็นการส่งครั้งเดียว มีสแนปช็อตของแต่ละข้อความเมื่อมีข้อมูล ส่งแทน message.created ให้เอนด์พอยต์ที่ตั้งช่วงเวลารวมกลุ่มไว้ และไม่สามารถสมัครรับโดยตรงได้
{"conversationId":"cm_conversation_id","messageIds":["cm_message_id","cm_other_message_id"]}webhook.testอีเวนต์ทดสอบที่ผู้ดูแลระบบร้องขอ
{"message":"This is a test webhook from Sonny."}
ลักษณะการส่ง
- ส่งกลับสถานะ 2xx ใดก็ได้ภายใน 10 วินาทีเพื่อระบุว่าการส่งสำเร็จ
- ทำให้ตัวจัดการเป็นแบบ idempotent ใช้
idของอีเวนต์หรือsonny-delivery-idเพื่อข้ามรายการที่ซ้ำ - ระบบจะไม่ติดตามการเปลี่ยนเส้นทาง โปรดอัปเดต URL ของเอนด์พอยต์ใน Sonny แทน
- เนื้อหาการตอบกลับมีขนาดจำกัด และจะเก็บไว้เฉพาะ 4 KiB แรกเพื่อใช้วินิจฉัยปัญหา
- เลือก ทดสอบ เพื่อส่งอีเวนต์
webhook.testเลือก การส่ง เพื่อดูอีเวนต์ 50 รายการล่าสุด เมื่อการส่งล้มเหลวถึงขั้นสุดท้าย ให้เลือก ลองใหม่ เพื่อส่งอีกครั้ง
ตรวจจับฟีดที่เงียบหายไป
เพิ่ม webhook.heartbeat ในอีเวนต์ของเอนด์พอยต์ในการตั้งค่านักพัฒนา หรือผ่าน update_webhook ผู้สมัครรับที่เปิดใช้งาน (รวมถึง *) จะได้รับสัญญาณ heartbeat ที่มีลายเซ็นทุกห้านาที แม้ไม่มีข้อความใหม่เข้ามา สัญญาณ heartbeat ไม่สนใจตัวกรองช่องทาง ทีม และข้อความ และไม่มีข้อมูลบทสนทนา โดยใช้เส้นทางการส่งและการลองส่งใหม่แบบเดียวกับข้อความ ตรวจสอบ createdAt และ nextExpectedAt เพื่อไม่ให้การลองส่งใหม่ที่ค้างอยู่ดูเหมือนสัญญาณ heartbeat ใหม่ และเผื่อเวลาสำหรับการโพลและความล่าช้าของเครือข่ายก่อนแจ้งเตือน การส่งที่ค้างสะสมอาจทำให้สัญญาณ heartbeat ล่าช้าหรือไม่ถูกส่ง
ใช้ list_webhook_deliveries ร่วมกับ webhooks:read เพื่อดูความพยายามส่งและความล้มเหลว get_status ตรวจสอบการเชื่อมต่อฐานข้อมูล ไม่ใช่การส่งเว็บฮุก ตั้งเวลาเรียก list_conversations แยกต่างหากด้วย status=open, awaitingReply=true, sort=waitingSince และ direction=asc เพื่อหาเธรดเก่าที่ยังไม่ได้ตอบ แม้ในขณะที่ฟีดเงียบอยู่
เปิดใช้เพย์โหลดแบบย่อในการตั้งค่านักพัฒนา หรือตั้ง compact=true บนเอนด์พอยต์ เพื่อตัดป้ายกำกับบริบทออก โดยยังเก็บ ID สำหรับการกำหนดเส้นทาง ผู้ส่ง เวลา และตัวอย่างข้อความ 200 อักขระไว้ ตัวเลือกนี้มีผลกับการส่งแบบรวมกลุ่มด้วย และเนื้อหาโน้ตภายในยังคงไม่ถูกรวม เพย์โหลดเริ่มต้นไม่เปลี่ยนแปลง ยกเว้นมีการเพิ่ม timestamp ของข้อความ แก้ไขขอบเขตสิทธิ์ของคีย์ API ที่มีอยู่เพื่อให้สิทธิ์ webhooks:read และ contacts:read สำหรับบันทึกการส่งและการค้นหาผู้ติดต่อโดยตรง ทั้งสองขอบเขตต้องใช้คีย์ที่เข้าถึงทุกช่องทางและสิทธิ์สมาชิกที่เข้าถึงทุกช่องทาง คีย์ที่จำกัดขอบเขตไว้ไม่สามารถขยายได้ด้วยการเพิ่มสิทธิ์เพียงอย่างเดียว
เอกสารที่เกี่ยวข้อง
- API สาธารณะBeta
คัดแยกและซิงค์บทสนทนา แชร์บริบทของลูกค้า อ่านรายงาน และส่งไฟล์ด้วยคีย์ API ที่จำกัดขอบเขต
- กล่องข้อความ
ทำความเข้าใจสถานะ ความสำคัญ การมอบหมาย การพักไว้ การดำเนินการแบบกลุ่ม และคีย์ลัด
- ผู้ติดต่อ
เรียนรู้วิธีที่ผู้ติดต่อถูกสร้าง จัดการ ติดแท็ก และรวมกันใน Sonny
- ทีมและบทบาท
เชิญผู้ใช้ สร้างทีม และทำความเข้าใจสิทธิ์ของเจ้าของ ผู้ดูแลระบบ เจ้าหน้าที่ และผู้ดู