Webhooks

Subscribe to real-time events from Disqua. When something happens — a message is sent, a member joins, a channel is created — Disqua sends an HTTP POST to your endpoint.

Register a webhook

Webhooks are registered per workspace. You can create up to 10 webhooks on Free, unlimited on Pro+.

Request POST /v1/workspaces/:wId/webhooks
{
  "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
}
Response 201 Created
{
  "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"
  }
}
MethodEndpointDescription
GET/v1/workspaces/:wId/webhooksList all webhooks
POST/v1/workspaces/:wId/webhooksCreate webhook
PATCH/v1/webhooks/:idUpdate URL, events, name, status (active|paused)
DELETE/v1/webhooks/:idDelete webhook
GET/v1/webhooks/:id/deliveriesList delivery attempts (30 day log)
POST/v1/webhooks/:id/testSend 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

HeaderValue
X-Disqua-EventEvent type, e.g. message.created
X-Disqua-DeliveryUnique delivery ID (evt_...)
X-Disqua-Signature-256HMAC-SHA256 signature — see verification below
X-Disqua-TimestampUnix timestamp (seconds) when delivery was triggered
User-AgentDisqua-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 typeTriggered whenKey data fields
message.createdNew message sent to a channelmessage, channel, author
message.updatedMessage content editedmessage, previousContent
message.deletedMessage deletedmessageId, channelId, deletedBy
reaction.addedReaction added to a messagemessageId, emoji, userId
reaction.removedReaction removed from a messagemessageId, emoji, userId
thread.reply.createdReply posted in a threadmessage, threadId, parentMessage

Channels

Event typeTriggered whenKey data fields
channel.createdNew channel createdchannel, createdBy
channel.updatedChannel name, topic, or settings changedchannel, changes
channel.archivedChannel archivedchannelId, archivedBy
channel.member.addedUser added to a channelchannelId, userId, addedBy
channel.member.removedUser removed from a channelchannelId, userId, removedBy

Members & Workspace

Event typeTriggered whenKey data fields
member.joinedNew member joins workspaceuser, workspace, invitedBy
member.leftMember leaves workspaceuserId, workspaceId
member.role_changedMember role updateduserId, previousRole, newRole
workspace.updatedWorkspace settings changedworkspace, changes

Files

Event typeTriggered whenKey data fields
file.uploadedFile confirmed + processedfile, uploadedBy, channelId
file.deletedFile deletedfileId, 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:

Option A — ngrok
# 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
Option B — use the test endpoint
# 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.