Register a webhook
Outgoing webhooks are workspace integrations of type webhook. Create them in Workspace Settings → Integrations, or through the API as a workspace owner or admin. Each webhook counts towards your plan's integration limit (10 on Free, unlimited on Pro and above).
{
"name": "My CI pipeline",
"type": "webhook",
"config": {
"url": "https://yourapp.com/disqua-webhook",
"secret": "your_signing_secret", // required — e.g. openssl rand -hex 32
"events": ["message.new"], // [] = every event type
"custom_headers": { "X-Env": "prod" } // optional
}
}
{
"ok": true,
"data": {
"integration": {
"id": "3f6c…",
"workspace_id": "…",
"name": "My CI pipeline",
"type": "webhook",
"config": { "url": "https://yourapp.com/disqua-webhook", "secret": "••••••", "events": ["message.new"] },
"is_active": 1
}
}
}
The secret is never returned by the API after creation — store it on your side.
| Method | Endpoint (prefix /v1/workspaces/:workspaceId) | Description |
|---|---|---|
| GET | /integrations | List integrations (webhooks, bots, slash commands) |
| POST | /integrations | Create a webhook (admin) |
| GET | /integrations/:integrationId | Get one integration |
| PATCH | /integrations/:integrationId | Update name, config or is_active (admin) |
| POST | /integrations/:integrationId/toggle | Pause / resume deliveries (admin) |
| DELETE | /integrations/:integrationId | Delete the webhook (admin) |
| GET | /integrations/:integrationId/delivery-log?limit=50 | Recent delivery attempts with status code and the first 1,000 characters of the response (max 200) |
Payload format
Every delivery is an HTTP POST with Content-Type: application/json and this envelope:
{
"id": "1842", // delivery ID — identical across retries
"type": "message.new", // event type
"created_at": "2026-10-16T10:05:00.000Z",
"data": {
// event-specific payload — see catalog below
}
}
Request headers sent with every delivery
| Header | Value |
|---|---|
| X-Disqua-Signature | sha256=<hex> — HMAC-SHA256 of the raw request body with your secret |
| X-Disqua-Event | Event type, e.g. message.new |
| X-Disqua-Delivery | Delivery ID (same value as id in the body, stable across retries) |
| User-Agent | Disqua-Webhooks/1.0 |
| custom_headers | Any headers you configured on the webhook |
Signature verification
Disqua signs the exact bytes of the request body with HMAC-SHA256 and your webhook secret. Verify the signature on the raw body before parsing it as JSON.
Outgoing deliveries carry no timestamp header. To protect against replays, remember the X-Disqua-Delivery IDs you have processed and ignore repeats, and check that created_at is recent.
Verification — Node.js
import crypto from 'crypto';
import express from 'express';
function verifyDisquaWebhook(rawBody: Buffer, signatureHeader: string, secret: string): boolean {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
const received = signatureHeader || '';
// Constant-time comparison; lengths must match first.
return received.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
}
const app = express();
app.post('/disqua-webhook', express.raw({ type: 'application/json' }), (req, res) => {
if (!verifyDisquaWebhook(req.body, req.get('x-disqua-signature') ?? '', process.env.WEBHOOK_SECRET!)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const event = JSON.parse(req.body.toString('utf8'));
// handle event.type === 'message.new' …
res.status(200).send('ok');
});
Verification — Python
import hmac, hashlib
def verify_disqua_webhook(raw_body: bytes, signature_header: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(secret.encode("utf-8"), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature_header or "")
Event catalog
Set events to a list of event types, or to an empty list to receive every event type. Today Disqua emits one event type; more are planned and will be listed here when they ship.
| Event type | Triggered when | Data fields |
|---|---|---|
| message.new | A message (or thread reply) is posted in a public channel. Messages from private channels and DMs are never sent to webhooks. | message.id, channel_id, user_id, content, type, thread_id, created_at |
Need more event types today? The outgoing side of the Zapier / Make / n8n connector also emits message.updated, channel.created, member.joined, ticket.created, ticket.assigned and ticket.resolved.
Example: message.new payload
{
"id": "1842",
"type": "message.new",
"created_at": "2026-10-16T10:05:00.123Z",
"data": {
"message": {
"id": "8b1d…",
"channel_id": "7a0c…",
"user_id": "7f9e…",
"content": "Hello **world**!",
"type": "text",
"thread_id": null,
"created_at": "2026-10-16 10:05:00.100"
}
}
}
Retries & reliability
Retry schedule
- Attempt 1 — immediately
- Attempt 2 — about 5 seconds later
- Attempt 3 — about 10 seconds after that
After three failed attempts the event is dropped. The webhook itself stays active — pause it yourself with the toggle endpoint if your receiver is down for longer.
Success criteria
A delivery succeeds when your endpoint returns any 2xx status within 10 seconds. Redirects are followed only to public addresses; private and loopback targets are rejected.
Respond with 200 right away and process the event asynchronously.
Use the id field (or X-Disqua-Delivery) as an idempotency key — a retry after a timeout can deliver the same event twice.
Testing locally
Disqua only delivers to public HTTPS-reachable addresses, so expose your local server with a tunnel during development:
# Start your local server on port 3000, then:
ngrok http 3000
# Register the tunnel URL as the webhook URL, e.g.
# https://abc123.ngrok.io/disqua-webhook
# and post a message in a public channel to trigger message.new.
Check what Disqua sent and what your endpoint answered with GET /v1/workspaces/:workspaceId/integrations/:integrationId/delivery-log. There is no separate test-ping endpoint.