Outgoing webhooks
Receive signed workspace events and handle retries, duplicates and delayed file updates.
On this page
An outgoing webhook sends eligible workspace events to your HTTP receiver. Use it to update another system when a message arrives, a conversation changes or a file moves.
Only public events are eligible. Events caused by agents and events in private or direct rooms are excluded.
Create a subscription
The workspace owner manages subscriptions in Settings → Webhooks:
- Under New subscription, enter your receiver's URL.
- Select Events.
- Choose Create subscription and copy the signing secret. It is shown once.
The receiver must be publicly reachable. Prefer HTTPS. Cabane rejects private-network destinations and does not follow redirects.
Subscription management is available in the app, not through API tokens.
Receive and verify a delivery
Cabane sends an HTTP POST with these headers:
Content-Type: application/json
Cabane-Event: room:created
Cabane-Delivery: 99999999-9999-4999-8999-999999999999
Cabane-Signature: <64 lowercase hexadecimal characters>
The signature above is a placeholder. The actual value is an HMAC-SHA256 signature of the exact request-body bytes, using your subscription secret.
An example payload is:
{
"event": "room:created",
"delivery": "99999999-9999-4999-8999-999999999999",
"timestamp": "2026-11-08T12:00:00.000Z",
"actor": "user",
"roomId": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
"name": "Reports",
"slug": "reports",
"roomType": "workroom",
"userId": "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
"user": {
"id": "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
"name": "Example User",
"email": "user@example.com"
}
}
Every payload includes event, delivery, timestamp and actor. The delivery matches Cabane-Delivery; actor is user or system for delivered events.
Verify the raw bytes
Capture the body as bytes before any JSON middleware parses it. Parsing and serializing it again can change whitespace or escaping and invalidate the signature.
This Node.js helper verifies the signature with a constant-time comparison, then parses the JSON:
import { createHmac, timingSafeEqual } from 'node:crypto';
export function parseCabaneWebhook(rawBody, signature, secret) {
if (!Buffer.isBuffer(rawBody)) {
throw new TypeError('rawBody must be the original request body as a Buffer');
}
if (typeof signature !== 'string' || !/^[0-9a-f]{64}$/.test(signature)) {
throw new Error('Missing or invalid Cabane-Signature');
}
const expected = createHmac('sha256', secret).update(rawBody).digest();
const received = Buffer.from(signature, 'hex');
if (!timingSafeEqual(expected, received)) {
throw new Error('Invalid Cabane-Signature');
}
return JSON.parse(rawBody.toString('utf8'));
}
Pass the raw Buffer, the Cabane-Signature header and the subscription secret. In a Node HTTP request, the header is available as request.headers['cabane-signature'].
After verification, durably record or queue the event using delivery as a unique key, then return a 2xx response within 10 seconds. If that delivery is already recorded, acknowledge it without repeating its work. Keep this deduplication state across receiver restarts.
Event fields
The fields below are added to the common payload. For events caused by a person, all types except message:created and notification:created also include userId and user with id, name and email.
| Event | Fields |
|---|---|
message:created | message with id, role, body, source, kind and createdAt; conversation with id, workspaceId and messageCount; thread and threadTruncated |
conversation:created, conversation:updated | conversationId, title, roomId, updatedAt |
conversation:deleted | conversationId |
notification:created | notification with id, kind, userId, payload and createdAt; workspace with id, slug and name; nullable conversation and message |
file:created | path, fileId, parentId, turnId |
file:updated | path, fileId, parentId, turnId, sha256 |
file:deleted, file:purged | path, fileId, parentId |
file:archived, file:unarchived | path, fileId |
folder:created, folder:deleted, folder:purged | path, folderId, parentId |
folder:archived, folder:unarchived | path, folderId |
file:moved | fileId, moveFact and the visible source or destination fields described below |
folder:moved | folderId, moveFact and the visible source or destination fields described below |
room:created | roomId, name, slug, roomType |
room:updated, room:archived, room:unarchived | roomId, name, slug |
For message:created, thread contains up to the last 20 messages of the conversation. threadTruncated says whether older messages were omitted.
Move events preserve the visible side of a move:
moveFact | Included location fields |
|---|---|
whole | fromPath, fromParentId, toPath, toParentId |
departure | fromPath and fromParentId only, for a move into private content |
appearance | toPath and toParentId only, for a move out of private content |
Do not require both paths on every move. A private location is not disclosed to the receiver.
Purging occurs after 30 days in trash and produces the corresponding purged event. Archiving a room also produces folder:archived for its folder.
Retries and order
Any 2xx response received within 10 seconds accepts a delivery. Other responses and timeouts can produce up to 10 attempts in total: the initial attempt, followed by waits of 1, 2, 4, 8, 16, 32, 60, 60 and 60 minutes.
Retries keep the same delivery ID and body bytes. Your receiver must handle duplicates, including the case where it recorded an event but its response never reached Cabane.
Delivery order is not guaranteed. If your application needs current state, read it from the API instead of treating arrival order as a sequence of authoritative updates.
Before each attempt, Cabane rechecks the event's public eligibility and whether the subscription is enabled. A delivery can be dropped if its content becomes private or the subscription is disabled or deleted.
File updates arrive after editing settles
file:updated events are grouped for the same file, person and editing context. Cabane sends the grouped event about 10 minutes after the last edit. Another editor or source forms a separate group.
The event's path and hash describe the first edit in that group. Use it as a signal to read the current file; do not treat the payload's sha256 as the latest version.
Manage a subscription
In Settings → Webhooks, use Disable or Enable to control delivery, Rotate secret to change the signing secret, and Delete to remove the subscription.
Update your receiver when you rotate the secret. Retry signatures use the subscription's current secret even though the saved body bytes stay the same.
There is no delivery-history or manual-replay interface. Keep the delivery records you need in your receiver.