Register a webhook
Webhooks are registered per workspace. You can create up to 10 webhooks on Free, unlimited on Pro+.
{
"name": "My CI pipeline",
"url": "https://yourapp.com/disqua-webhook",
"events": [
"message.created",
"channel.created",
"member.joined"
],
"secret": "your_signing_secret" // optional — generate with openssl rand -hex 32
}
{
"ok": true,
"data": {
"id": "wh_01HQ...",
"workspaceId": "...",
"name": "My CI pipeline",
"url": "https://yourapp.com/disqua-webhook",
"events": ["message.created", "channel.created", "member.joined"],
"status": "active",
"createdAt": "2026-03-18T10:00:00.000Z"
}
}
| Method | Endpoint | Description |
|---|---|---|
| GET | /v1/workspaces/:wId/webhooks | List all webhooks |
| POST | /v1/workspaces/:wId/webhooks | Create webhook |
| PATCH | /v1/webhooks/:id | Update URL, events, name, status (active|paused) |
| DELETE | /v1/webhooks/:id | Delete webhook |
| GET | /v1/webhooks/:id/deliveries | List delivery attempts (30 day log) |
| POST | /v1/webhooks/:id/test | Send a test ping to the endpoint |
Payload format
All webhook payloads are sent as HTTP POST with Content-Type: application/json. Every payload shares this envelope:
{
"id": "evt_01HQ9A...", // Unique event ID (idempotency key)
"type": "message.created", // Event type string
"workspaceId": "01HQ6Z...",
"createdAt": "2026-03-18T10:05:00.000Z",
"data": {
// Event-specific payload — see catalog below
}
}
Request headers sent with every delivery
| Header | Value |
|---|---|
| X-Disqua-Event | Event type, e.g. message.created |
| X-Disqua-Delivery | Unique delivery ID (evt_...) |
| X-Disqua-Signature-256 | HMAC-SHA256 signature — see verification below |
| X-Disqua-Timestamp | Unix timestamp (seconds) when delivery was triggered |
| User-Agent | Disqua-Webhook/1.0 |
Signature verification
Disqua signs every webhook with HMAC-SHA256 using your signing secret. Always verify the signature before processing the payload to prevent spoofing.
Reject deliveries with a timestamp older than 5 minutes to protect against replay attacks.
Verification — Node.js
import crypto from 'crypto';
function verifyDisquaWebhook(
rawBody: string,
signature: string,
timestamp: string,
secret: string
): boolean {
// Reconstruct the signed string
const signedString = `${timestamp}.${rawBody}`;
// Compute HMAC-SHA256
const expected = crypto
.createHmac('sha256', secret)
.update(signedString, 'utf8')
.digest('hex');
const received = signature.replace('sha256=', '');
// Constant-time comparison to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(expected, 'hex'),
Buffer.from(received, 'hex')
);
}
// Express example
app.post('/disqua-webhook', express.raw({ type: 'application/json' }), (req, res) => {
const isValid = verifyDisquaWebhook(
req.body.toString(),
req.headers['x-disqua-signature-256'] as string,
req.headers['x-disqua-timestamp'] as string,
process.env.WEBHOOK_SECRET!
);
if (!isValid) {
return res.status(401).json({ error: 'Invalid signature' });
}
const event = JSON.parse(req.body.toString());
// handle event...
res.status(200).send('ok');
});
Verification — Python
import hmac, hashlib, time
def verify_disqua_webhook(raw_body: bytes, signature: str, timestamp: str, secret: str) -> bool:
signed_string = f"{timestamp}.{raw_body.decode('utf-8')}"
expected = hmac.new(
secret.encode('utf-8'),
signed_string.encode('utf-8'),
hashlib.sha256
).hexdigest()
received = signature.replace("sha256=", "")
return hmac.compare_digest(expected, received)
Event catalog
Subscribe to individual events or use "*" to receive all events.
Messages
| Event type | Triggered when | Key data fields |
|---|---|---|
| message.created | New message sent to a channel | message, channel, author |
| message.updated | Message content edited | message, previousContent |
| message.deleted | Message deleted | messageId, channelId, deletedBy |
| reaction.added | Reaction added to a message | messageId, emoji, userId |
| reaction.removed | Reaction removed from a message | messageId, emoji, userId |
| thread.reply.created | Reply posted in a thread | message, threadId, parentMessage |
Channels
| Event type | Triggered when | Key data fields |
|---|---|---|
| channel.created | New channel created | channel, createdBy |
| channel.updated | Channel name, topic, or settings changed | channel, changes |
| channel.archived | Channel archived | channelId, archivedBy |
| channel.member.added | User added to a channel | channelId, userId, addedBy |
| channel.member.removed | User removed from a channel | channelId, userId, removedBy |
Members & Workspace
| Event type | Triggered when | Key data fields |
|---|---|---|
| member.joined | New member joins workspace | user, workspace, invitedBy |
| member.left | Member leaves workspace | userId, workspaceId |
| member.role_changed | Member role updated | userId, previousRole, newRole |
| workspace.updated | Workspace settings changed | workspace, changes |
Files
| Event type | Triggered when | Key data fields |
|---|---|---|
| file.uploaded | File confirmed + processed | file, uploadedBy, channelId |
| file.deleted | File deleted | fileId, deletedBy |
Example: message.created payload
{
"id": "evt_01HQ9A...",
"type": "message.created",
"workspaceId": "01HQ6Z...",
"createdAt": "2026-03-18T10:05:00.000Z",
"data": {
"message": {
"id": "01HQ8B...",
"channelId": "01HQ7A...",
"authorId": "01HQ7Z...",
"content": "Hello **world**!",
"contentHtml": "<p>Hello <strong>world</strong>!</p>",
"type": "message",
"createdAt": "2026-03-18T10:05:00.000Z"
},
"channel": {
"id": "01HQ7A...",
"name": "general",
"slug": "general"
},
"author": {
"id": "01HQ7Z...",
"username": "janedoe",
"displayName": "Jane Doe"
}
}
}
Retries & reliability
Retry schedule
- Attempt 1 — immediate
- Attempt 2 — 30 seconds
- Attempt 3 — 5 minutes
- Attempt 4 — 30 minutes
- Attempt 5 — 2 hours
After 5 failed attempts, the webhook is automatically paused.
Success criteria
A delivery is considered successful when your endpoint returns any 2xx status within 10 seconds.
Your endpoint should respond quickly and process the event asynchronously. Return 200 immediately, then process.
Use the id field as an idempotency key — the same event may be delivered more than once in rare cases.
Testing locally
Use a tunnel tool to expose your local server to the internet during development:
# Start your local server on port 3000
# Then expose it:
ngrok http 3000
# Register the ngrok URL as your webhook endpoint:
# https://abc123.ngrok.io/disqua-webhook
# Send a test ping without a live server:
POST /v1/webhooks/:id/test
# Disqua will send a test payload to your configured URL
# and show you the response in the delivery log.
View delivery history and response bodies in Workspace Settings → Integrations → Webhooks → Delivery Log.