Event webhooks
An event webhook sends a POST to your system when something happens in SquadOS. Use it to feed a CRM, CDP, internal dashboard, or external automation without repeatedly polling the API.
Access and permissions
Section titled “Access and permissions”Open Settings → Developers → Webhooks. The navigation and button names below match the current English catalog.
Access uses granular capabilities:
| Capability | Product contract |
|---|---|
webhooks.view | Opens the section and allows the backend to return destinations and history |
webhooks.write | Creates, edits, tests, rotates the key, turns destinations on/off, and schedules replays |
webhooks.delete | Deletes the destination and its associated history |
Owners and admins retain these permissions through the legacy contract. For custom roles, grant webhooks.write and webhooks.delete for complete management; both depend on webhooks.view.
The list shows each destination’s name, URL, state, and up to three events. States are Active, Turned off, and Turned off after failures. An organization can have at most 10 webhooks.
Creating a destination
Section titled “Creating a destination”Select New webhook and complete all four sections. The form only creates when it has a name, HTTPS URL, at least one event, complete filters, and valid headers.
1. Identification
Section titled “1. Identification”Enter a name and the receiver’s final URL. The URL must start with https://.
At delivery time, SquadOS also blocks localhost, private or reserved networks, metadata endpoints, its own Supabase project, and hosts whose DNS cannot be validated. This check happens after the destination has been saved; a blocked URL terminates the delivery without retries.
2. Events
Section titled “2. Events”The current catalog contains 25 events. Select all in a family does not include the three internal-note events, which start unchecked.
Messages
| UI name | Delivered type | When it happens |
|---|---|---|
| Message received | message.received | A contact sends a message |
| Message sent | message.sent | An AI agent or teammate replies; for streaming, only after the final text |
| Message edited | message.updated | A message’s text changes |
| Message deleted | message.deleted | The message receives its deletion marker |
Conversations
| UI name | Delivered type |
|---|---|
| Conversation opened | conversation.created |
| Conversation assigned | conversation.assigned |
| Conversation transferred | conversation.transferred |
| Conversation back to AI | conversation.transferred_to_agent |
| Conversation closed | conversation.resolved |
| Conversation reopened | conversation.reopened |
| AI turned on or off | conversation.ai_toggled |
| Conversation snoozed | conversation.snoozed |
| Conversation back | conversation.unsnoozed |
| Contacts merged in the conversation | conversation.contacts_merged |
| Internal note created | conversation.internal_note_created |
| Internal note edited | conversation.internal_note_updated |
| Internal note deleted | conversation.internal_note_deleted |
| Tag added to conversation | conversation.tag_added |
| Tag removed from conversation | conversation.tag_removed |
Rating and contacts
| UI name | Delivered type |
|---|---|
| Rating finished | satisfaction.completed |
| Contact created | contact.created |
| Contact updated | contact.updated |
| Contacts merged | contact.merged |
| Tag added to contact | contact.tag_added |
| Tag removed from contact | contact.tag_removed |
3. Filters
Section titled “3. Filters”Without filters, the destination receives every selected type. Available criteria are Inbox, Conversation owner, Conversation tag, Contact tag, Channel, and Conversation AI. Different conditions are combined with AND; selected values inside a set criterion form OR.
Filters whose axis does not exist on an event are ignored. Contact events, for example, have no conversation, inbox, owner, channel, or AI state; a filter on those axes does not block contact.created. The Contact tag criterion remains applicable.
4. Security and state
Section titled “4. Security and state”Use Extra headers when the receiver requires something such as Authorization: Bearer .... Content-Type and any name beginning with X-Squados- are reserved and cannot be overridden.
The Active control works when editing an existing destination. During creation, the current product saves the webhook as active even if the control is off. If it must start paused, create it and immediately turn it off from the list.
Verification key and testing
Section titled “Verification key and testing”After creation, copy the Verification key: it appears only once. Generate new key in the edit form invalidates the previous key immediately; following deliveries use the new key, while a request already signed may fail and return through retry.
In the key panel, Send test event sends fictional data through the same guard, signature, and sender as real deliveries. In the edit form, the same button uses the first selected type. The payload contains data.test: true, appears in history, and shows the receiver’s status, duration, and response prefix.
Body and headers
Section titled “Body and headers”Every delivery uses this envelope:
{ "id": "9f2c1b7e-3a44-4d1c-9b0e-52a7c8e1d004", "type": "message.received", "api_version": "2026-08-24", "occurred_at": "2026-09-02T14:03:11.482Z", "organization_id": "6d1b0a52-8f3e-4c77-9a10-2b5e7c9d3311", "data": { "message": { "id": "8f2b1c4e-0000-4000-8000-000000000001", "role": "user", "sender_kind": "contact", "content": "Hello", "attachments": [], "created_at": "2026-09-02T14:03:11.400Z" }, "conversation": { "id": "a41d9e77-0000-4000-8000-000000000001", "status": "open", "ai_enabled": true, "channel_type": "whatsapp_official", "channel_family": "whatsapp", "inbox": { "id": "00000000-0000-4000-8000-0000000010b0", "name": "Sales" }, "agent": { "id": "00000000-0000-4000-8000-000000000a6e", "name": "Receptionist" }, "assigned_to": null }, "contact": { "id": "c7e30b12-0000-4000-8000-000000000001", "display_name": "John Ribeiro", "identity_type": "whatsapp", "identity_value": "+5511988887777" } }}data varies by family:
| Family | Main blocks |
|---|---|
| Message | message, conversation, contact; assistant messages also include model, tokens, credits, and cost when available |
| Conversation | conversation, contact, actor, event; tag changes also include tag |
| Rating | satisfaction, conversation, contact |
| Contact | contact; tag changes include tag, and merges include the destination contact |
These blocks are curated summaries of state when the fact occurred, not complete copies of internal rows. New fields may appear under the same api_version; ignore unknown keys. Use occurred_at for ordering and the envelope id for deduplication.
| Header | Contents |
|---|---|
X-Squados-Event | Event type |
X-Squados-Delivery | Delivery ID; remains stable across automatic retries and changes on a manual replay |
X-Squados-Timestamp | Attempt time in Unix seconds |
X-Squados-Signature | HMAC formatted as sha256=<hex> |
Verifying the signature
Section titled “Verifying the signature”The signature is HMAC-SHA256(key, "<timestamp>.<raw body>"). Compare it in constant time, reject stale timestamps according to your integration’s security window, and only then parse the JSON.
import crypto from "node:crypto";
function signatureMatches(key, timestamp, rawBody, received) { const expected = "sha256=" + crypto.createHmac("sha256", key).update(`${timestamp}.${rawBody}`).digest("hex");
const a = Buffer.from(expected); const b = Buffer.from(received ?? ""); return a.length === b.length && crypto.timingSafeEqual(a, b);}Delivery and retries
Section titled “Delivery and retries”The queue is checked once per minute. A 2xx response completes delivery. 3xx, 4xx, 5xx, refused connections, and the 15-second timeout fail; redirects are not followed. History keeps up to 4,000 characters of the response body and 1,000 of the error.
The current runtime makes at most six attempts:
| Attempt | Wait after the previous failure |
|---|---|
| 1st | Next queue cycle |
| 2nd | 1 minute |
| 3rd | 5 minutes |
| 4th | 15 minutes |
| 5th | 1 hour |
| 6th and terminal | 3 hours |
A delivery that always fails therefore becomes terminal after approximately 4h21, plus queue-cycle timing. A URL blocked by the guard or a webhook with no key dies on its first attempt because retrying cannot change the outcome.
The guarantee is at least once: a lost response or recovered lease can repeat an event, and ordering across events is not guaranteed. Deduplicate by the envelope id, not X-Squados-Delivery.
After five consecutive terminal deliveries for the same problem and URL, the destination becomes Turned off after failures. Owners and admins receive the alert by email. Fix the receiver and use Turn on; reactivation resets counters and resolves the open alert.
History and replay
Section titled “History and replay”Open Delivery history on the destination. The list loads 25 items per page and shows type, state, HTTP status, duration, attempt count, sent payload, received response, and test or replay markers.
dead deliveries — and legacy failed records — offer Send again. The command creates a new pending delivery linked to the original, even when the destination is turned off. It neither erases nor changes the earlier failure and leaves on the next queue cycle.
Events and deliveries are deleted after 30 days. Deleting the webhook also immediately removes its key and history. Events that occur while the destination is off are not accumulated.
Receiver checklist
Section titled “Receiver checklist”- Return
2xxquickly and move heavy work outside the request. - Validate
X-Squados-TimestampandX-Squados-Signatureagainst the raw body. - Deduplicate by the envelope
id. - Ignore unknown fields and handle optional or
nullblocks. - Monitor failures and use history before the destination is turned off.
- To recover current state or data outside retention, query the REST API.