Outgoing webhooks

Get notified when something happens in Disqua. When a message is posted in a public channel, Disqua sends a signed HTTP POST to your endpoint.

Looking for the other direction? This page covers Disqua → your service. To push events from your platform into a Disqua channel — sign-ups, new listings, records created or updated — see Incoming webhooks.

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).

Request POST /v1/workspaces/:workspaceId/integrations
{
  "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
  }
}
Response 201 Created
{
  "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.

MethodEndpoint (prefix /v1/workspaces/:workspaceId)Description
GET/integrationsList integrations (webhooks, bots, slash commands)
POST/integrationsCreate a webhook (admin)
GET/integrations/:integrationIdGet one integration
PATCH/integrations/:integrationIdUpdate name, config or is_active (admin)
POST/integrations/:integrationId/togglePause / resume deliveries (admin)
DELETE/integrations/:integrationIdDelete the webhook (admin)
GET/integrations/:integrationId/delivery-log?limit=50Recent 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

HeaderValue
X-Disqua-Signaturesha256=<hex> — HMAC-SHA256 of the raw request body with your secret
X-Disqua-EventEvent type, e.g. message.new
X-Disqua-DeliveryDelivery ID (same value as id in the body, stable across retries)
User-AgentDisqua-Webhooks/1.0
custom_headersAny 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 typeTriggered whenData fields
message.newA 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:

ngrok (or any similar tunnel)
# 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.