Skip to content

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.

Open Settings → Developers → Webhooks. The navigation and button names below match the current English catalog.

Access uses granular capabilities:

CapabilityProduct contract
webhooks.viewOpens the section and allows the backend to return destinations and history
webhooks.writeCreates, edits, tests, rotates the key, turns destinations on/off, and schedules replays
webhooks.deleteDeletes 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.

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.

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.

The current catalog contains 25 events. Select all in a family does not include the three internal-note events, which start unchecked.

Messages

UI nameDelivered typeWhen it happens
Message receivedmessage.receivedA contact sends a message
Message sentmessage.sentAn AI agent or teammate replies; for streaming, only after the final text
Message editedmessage.updatedA message’s text changes
Message deletedmessage.deletedThe message receives its deletion marker

Conversations

UI nameDelivered type
Conversation openedconversation.created
Conversation assignedconversation.assigned
Conversation transferredconversation.transferred
Conversation back to AIconversation.transferred_to_agent
Conversation closedconversation.resolved
Conversation reopenedconversation.reopened
AI turned on or offconversation.ai_toggled
Conversation snoozedconversation.snoozed
Conversation backconversation.unsnoozed
Contacts merged in the conversationconversation.contacts_merged
Internal note createdconversation.internal_note_created
Internal note editedconversation.internal_note_updated
Internal note deletedconversation.internal_note_deleted
Tag added to conversationconversation.tag_added
Tag removed from conversationconversation.tag_removed

Rating and contacts

UI nameDelivered type
Rating finishedsatisfaction.completed
Contact createdcontact.created
Contact updatedcontact.updated
Contacts mergedcontact.merged
Tag added to contactcontact.tag_added
Tag removed from contactcontact.tag_removed

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.

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.

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.

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:

FamilyMain blocks
Messagemessage, conversation, contact; assistant messages also include model, tokens, credits, and cost when available
Conversationconversation, contact, actor, event; tag changes also include tag
Ratingsatisfaction, conversation, contact
Contactcontact; 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.

HeaderContents
X-Squados-EventEvent type
X-Squados-DeliveryDelivery ID; remains stable across automatic retries and changes on a manual replay
X-Squados-TimestampAttempt time in Unix seconds
X-Squados-SignatureHMAC formatted as sha256=<hex>

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

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:

AttemptWait after the previous failure
1stNext queue cycle
2nd1 minute
3rd5 minutes
4th15 minutes
5th1 hour
6th and terminal3 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.

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.

  1. Return 2xx quickly and move heavy work outside the request.
  2. Validate X-Squados-Timestamp and X-Squados-Signature against the raw body.
  3. Deduplicate by the envelope id.
  4. Ignore unknown fields and handle optional or null blocks.
  5. Monitor failures and use history before the destination is turned off.
  6. To recover current state or data outside retention, query the REST API.