Webhooks
Subscribe to real-time events from Mandraki -- event types, payload format, signature verification, retries, and filters.
Overview
Webhooks deliver real-time event notifications to your HTTPS endpoint when things happen in your Mandraki organization. Instead of polling the API, register a webhook to receive push notifications for message, task, call, and other events.
Each webhook subscription belongs to a service account and can subscribe to any combination of the 35 available event types with optional resource filters.
Creating a Webhook
Create a webhook subscription through the Mandraki settings UI or the service account management API.
Required Fields
| Field | Type | Description |
|---|---|---|
url | URL | Your HTTPS endpoint (must use https://) |
events | string[] | At least one event type to subscribe to |
Optional Filters
| Field | Type | Description |
|---|---|---|
channelFilter | UUID[] | Only deliver events for these channels |
teamFilter | UUID[] | Only deliver events for these teams |
projectFilter | UUID[] | Only deliver events for these projects |
folderFilter | UUID[] | Only deliver events for these Drive folders |
Empty filters (the default) match all resources of that type. Filters are additive — a webhook must pass all non-empty filters to be delivered.
Example
{
"url": "https://your-app.example.com/webhooks/mandraki",
"events": ["message.created", "task.created", "task.completed"],
"channelFilter": ["channel-uuid-1", "channel-uuid-2"],
"projectFilter": ["project-uuid-1"]
}
This webhook receives message.created events only from the two specified channels, and task events only from the specified project.
Signing Secret
When a webhook is created, Mandraki generates a signing secret and returns it once. The secret is encrypted at rest and never stored in plaintext. Store it securely — you will need it to verify webhook signatures.
You can rotate the signing secret at any time through the API. After rotation, the old secret stops working immediately.
Payload Format
Every webhook delivery sends a POST request with a JSON body:
{
"type": "message.created",
"event_id": "evt_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
"timestamp": "2026-03-27T12:00:00.000Z",
"data": {
"message": {
"id": "message-uuid",
"channel_id": "channel-uuid",
"sender": {
"id": "user-uuid",
"name": "Alice"
},
"content": "Hello, world!",
"is_encrypted": false,
"created_at": "2026-03-27T12:00:00.000Z"
}
}
}
Envelope Fields
| Field | Type | Description |
|---|---|---|
type | string | The event type (e.g., message.created) |
event_id | string | Unique delivery ID prefixed with evt_ |
timestamp | ISO 8601 | When the event occurred |
data | object | Event-specific payload |
HTTP Headers
| Header | Description |
|---|---|
Content-Type | application/json |
X-Mandraki-Signature | HMAC-SHA256 signature of the request body |
X-Mandraki-Event | The event type |
X-Mandraki-Delivery-ID | Unique ID for this delivery attempt |
User-Agent | Mandraki-Webhooks/1.0 |
Signature Verification
Every webhook delivery is signed with HMAC-SHA256 using your signing secret. Always verify the signature before processing the payload.
The signature is in the X-Mandraki-Signature header, formatted as sha256={hex}.
Verification Example (Node.js)
import { createHmac, timingSafeEqual } from 'crypto';
function verifyWebhookSignature(
body: string,
signature: string,
secret: string
): boolean {
const expected = 'sha256=' + createHmac('sha256', secret)
.update(body)
.digest('hex');
if (signature.length !== expected.length) return false;
return timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
// In your webhook handler:
app.post('/webhooks/mandraki', (req, res) => {
const signature = req.headers['x-mandraki-signature'];
const rawBody = req.rawBody; // raw request body string
if (!verifyWebhookSignature(rawBody, signature, WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
const payload = JSON.parse(rawBody);
// Process the event...
res.status(200).send('OK');
});
Verification Example (Python)
import hmac
import hashlib
def verify_signature(body: bytes, signature: str, secret: str) -> bool:
expected = 'sha256=' + hmac.new(
secret.encode(), body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
Retry Policy
If your endpoint returns a non-2xx status code or is unreachable, Mandraki retries the delivery:
| Attempt | Delay |
|---|---|
| 1st retry | 1 second |
| 2nd retry | 5 seconds |
| 3rd retry | 30 seconds |
Each delivery attempt has a 10-second timeout. If your endpoint does not respond within 10 seconds, the attempt is considered failed.
Client errors (4xx) are not retried — they indicate a permanent problem that retrying won’t fix.
Auto-Disable
After 10 consecutive delivery failures, the webhook is automatically disabled. The disabledAt timestamp is recorded for troubleshooting.
To re-enable a disabled webhook, update it with isActive: true. This resets the failure counter.
Event Types Reference
Message Events
| Event | Trigger | Key Payload Fields |
|---|---|---|
message.created | New message sent to a channel | message.id, message.channel_id, message.sender, message.content, message.is_encrypted |
message.updated | Message content edited | message.id, message.channel_id, message.content, message.is_encrypted |
message.deleted | Message deleted | message.id, message.channel_id |
For E2EE-encrypted messages, the content field is null and is_encrypted is true.
Channel Events
| Event | Trigger | Key Payload Fields |
|---|---|---|
channel.created | New channel created | channel.id, channel.name, channel.slug, channel.is_private |
channel.archived | Channel archived | channel.id, channel.name, channel.slug |
channel.member_joined | User joined a channel | channel_id, user.id, user.name |
channel.member_left | User left a channel | channel_id, user.id, user.name |
Call Events
| Event | Trigger | Key Payload Fields |
|---|---|---|
call.started | Call session begins | call.id, call.title, call.status |
call.ended | Call session ends | call.id, call.title, call.status, call.duration |
call.participant_joined | User joins a call | call_id, participant.id, participant.name |
call.participant_left | User leaves a call | call_id, participant.id, participant.name |
User Events
| Event | Trigger | Key Payload Fields |
|---|---|---|
user.joined_org | User added to organization | user.id, user.name, user.email |
user.left_org | User removed from organization | user.id, user.name |
user.role_changed | User’s org role changed | user.id, user.name, role |
Task Events
| Event | Trigger | Key Payload Fields |
|---|---|---|
task.created | New task created | task.id, task.title, task.project_id, task.assignee_id, task.created_by |
task.updated | Task fields modified | task.id, task.title, task.project_id, task.status |
task.completed | Task marked as done | task.id, task.title, task.project_id, task.completed_by |
task.assigned | Task assigned to user | task.id, task.title, task.project_id, assignee.id, assignee.name |
task.commented | Comment added to task | task.id, task.title, task.project_id, comment.id, comment.author |
task.deleted | Task deleted | task.id |
project.created | New project created | project.id, project.name, project.team_id |
project.updated | Project modified | project.id, project.name, project.team_id |
Drive Events
| Event | Trigger | Key Payload Fields |
|---|---|---|
drive.file.uploaded | File uploaded | file.id, file.name, file.folder_id, file.size, file.mime_type, file.uploaded_by |
drive.file.deleted | File deleted | file.id |
drive.file.shared | File shared with user | file.id, file.name, file.folder_id, shared_with.id, shared_with.name |
drive.folder.created | Folder created | folder.id, folder.name, folder.parent_id, folder.created_by |
drive.folder.deleted | Folder deleted | folder.id |
Calendar Events
| Event | Trigger | Key Payload Fields |
|---|---|---|
calendar.event.created | Event created | event.id, event.title, event.start_time, event.end_time, event.created_by |
calendar.event.updated | Event modified | event.id, event.title, event.start_time, event.end_time |
calendar.event.cancelled | Event cancelled | event.id |
calendar.event.rsvp | User RSVPs to event | event.id, event.title, user.id, user.name, response |
Mail Events
Mail webhooks deliver metadata only — message body content is never included.
| Event | Trigger | Key Payload Fields |
|---|---|---|
mail.received | Inbound email received | mail.id, mail.subject, mail.from, mail.to, mail.received_at |
mail.sent | Outbound email sent | mail.id, mail.subject, mail.from, mail.to, mail.sent_at |
Team Events
| Event | Trigger | Key Payload Fields |
|---|---|---|
team.created | Team created | team.id, team.name, team.slug, team.created_by |
team.archived | Team archived | team.id, team.name |
team.deleted | Team deleted | team.id, team.name |
team.member_joined | User joined a team | team_id, user.id, user.name |
team.member_left | User left a team | team_id, user.id, user.name |
Test Webhooks
You can send a test event to verify your endpoint is configured correctly. The test payload uses the event type test:
{
"type": "test",
"event_id": "evt_...",
"timestamp": "2026-03-27T12:00:00.000Z",
"data": {
"message": "This is a test webhook from Mandraki"
}
}
Best Practices
- Respond quickly: Return a
200status within 10 seconds. Process events asynchronously if needed. - Use idempotent processing: Use the
event_idto deduplicate — retries may deliver the same event more than once. - Verify signatures: Always validate the
X-Mandraki-Signatureheader using constant-time comparison. - Use filters: Subscribe only to the events and resources you need to reduce noise and processing load.
- Monitor failures: Check your webhook status regularly. After 10 consecutive failures, the webhook is auto-disabled.
- Handle E2EE: Encrypted message content is
null. Checkis_encryptedbefore processing message payloads.