Developers
Webhooks Beta
Receive durable, signed notifications when contacts, conversations, messages, tags, or team membership change.
Open API referenceCreate an endpoint
- 01
Open Settings, select Developer, find Webhooks, then select Add endpoint.
- 02
Enter a Name and public HTTPS Endpoint URL, then choose at least one item under Events.
- 03
Select Add endpoint. You can also create one with
POST /api/v1/webhooks.
Sonny rejects credentials in URLs, localhost, private/link-local IP ranges, and DNS names that resolve to a non-public address.
Save the signing secret immediately
It starts with whsec_ and is shown once. Copy it into your secret manager before selecting I saved it.
Limit an endpoint to selected sources
API and MCP clients can set sourceIds when creating or updating an endpoint. An empty array means all sources in the workspace. When IDs are present, conversation and message events are delivered only when their conversation belongs to one of the selected sources. Workspace-level events, such as contact or membership changes, are not sent to a source-filtered endpoint.
{
"name": "Product A agent",
"url": "https://agent.example.com/sonny",
"events": ["conversation.created", "message.created"],
"sourceIds": ["cm_source_id"]
}Security and reliability
- Signed raw body
- HMAC-SHA256 covers the Unix timestamp, a dot, and the untouched UTF-8 request body.
- Replay protection
- Reject timestamps more than five minutes in the past or future, even when the HMAC is valid.
- Durable retries
- Non-2xx responses retry after 1m, 5m, 30m, 2h, 6h. Attempt 6 is terminal.
Request contract
sonny-signature- t=<unix-seconds>,v1=<sha256-hex>
sonny-event- The event type, for fast routing.
sonny-delivery-id- A stable ID for idempotency and 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"
}
}Verify the signature
Read the raw body first. Parsing JSON and serializing it again changes whitespace and makes a valid signature fail.
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");
}Event catalog
contact.createdA contact was created.
{"contactId":"cm_contact_id"}contact.updatedA contact was updated or merged.
{"contactId":"cm_contact_id"}contact.deletedA contact was deleted.
{"contactId":"cm_contact_id"}conversation.createdA conversation was created.
{"conversationId":"cm_conversation_id"}conversation.updatedA conversation changed.
{"conversationId":"cm_conversation_id"}conversation.closedA conversation was closed.
{"conversationId":"cm_conversation_id"}conversation.deletedA conversation moved to Trash and left the public API.
{"conversationId":"cm_conversation_id"}message.createdA message or internal note was created.
{"conversationId":"cm_conversation_id","messageId":"cm_message_id"}tag.createdA tag was created.
{"tagId":"cm_tag_id"}tag.updatedA tag was updated.
{"tagId":"cm_tag_id"}tag.deletedA tag was deleted.
{"tagId":"cm_tag_id"}member.invitedA workspace member was invited.
{"invitationId":"cm_invitation_id"}member.updatedA member role or status changed.
{"memberId":"cm_membership_id"}member.removedA member was removed.
{"memberId":"cm_membership_id"}invitation.acceptedA workspace invitation was accepted.
{"invitationId":"cm_invitation_id","memberId":"cm_membership_id"}invitation.cancelledA pending workspace invitation was cancelled.
{"invitationId":"cm_invitation_id"}webhook.testA test event requested by an administrator.
{"message":"This is a test webhook from Sonny."}
Delivery behavior
- Return any 2xx status within 10 seconds to mark the delivery successful.
- Keep handlers idempotent. Use the event
idorsonny-delivery-idto ignore duplicates. - Redirects are not followed. Update the endpoint URL in Sonny instead.
- Response bodies are capped and only the first 4 KiB is retained for diagnosis.
- Select Test to send a
webhook.testevent. Select Deliveries to inspect the most recent 50 events. When a delivery reaches a terminal failure, select Retry to send it again.
Related docs
- Public APIBeta
Create scoped API keys and integrate contacts, conversations, notes, and webhooks.
- Inbox
Understand statuses, priorities, assignment, snoozing, bulk actions, and keyboard shortcuts.
- Contacts
Learn how contacts are created, managed, tagged, and merged in Sonny.
- Team & roles
Invite users, create teams, and understand owner, admin, agent, and viewer access.