Qaid
ARTICLE

Webhooks & Slack: Push Feedback Into Your Stack

Send feedback and quest events to Slack or your own signed HTTP endpoint the moment they happen.

Qaid Team

Feedback is most useful where your team already works. Webhooks let Qaid push an event to Slack — or to your own service — the instant it happens, so you can ping a channel, open a ticket, or kick off any automation you like without polling the dashboard.

0:00 / 0:00
Adding a Slack webhook from the Integrations tab

What can trigger a webhook

A webhook fires on the events you subscribe it to:

EventFires when
feedback.createdSomeone submits feedback
feedback.escalatedFeedback matches one of your escalation rules
quest.submittedSomeone completes a quest

Each webhook is one of two kinds:

  • Slack — posts a ready-to-read message to a Slack channel. No code required.
  • Generic — sends a signed JSON payload to a URL you control, for your own automations.

Send feedback to Slack

  1. In Slack, add an Incoming Webhook to the channel you want (Slack → AppsIncoming WebhooksAdd to Slack), and copy the webhook URL it gives you.
  2. In Qaid, open your project's Settings and choose the Integrations tab. Click New Webhook.
  3. Set the Type to Slack, paste the Slack URL into Endpoint URL, tick the events you care about, and click Create.
  4. Click Test on the new webhook to post a sample message and confirm it lands in your channel.

Slack messages are formatted for you — a headline for the event, the feedback message, the page it came from, and a View in Qaid link straight to the item:

💬 New feedback received
The save button does nothing when I click it
Page: https://example.com/account
View in Qaid

Send events to your own endpoint

Choose Generic as the type and point it at any https:// URL. Qaid will POST a compact JSON body every time a subscribed event fires.

Every generic delivery carries two headers you can rely on:

HeaderValue
X-Qaid-EventThe event name, e.g. feedback.created
X-Qaid-Signaturesha256= followed by an HMAC signature of the body

The payload

The body is small and predictable. Screenshots are never sent inline — you get a hasScreenshot flag and a dashboardUrl to view the full item instead:

{
  "event": "feedback.created",
  "createdAt": "2026-07-10T15:40:00.000Z",
  "data": {
    "id": "clx8a1b2c3",
    "feedbackType": "negative",
    "message": "The save button does nothing when I click it",
    "url": "https://example.com/account",
    "elementSelector": "button.save",
    "elementText": "Save",
    "hasScreenshot": true,
    "consoleErrorCount": 2,
    "isEscalated": false,
    "projectId": "clp0z9y8x7",
    "dashboardUrl": "https://qaid.dev/dashboard/projects/clp0z9y8x7?feedback=clx8a1b2c3"
  }
}

quest.submitted deliveries carry the quest response id, quest id, and project id in data so you can look the response up.

Verifying the signature

Because anyone who learns your URL could POST to it, verify the X-Qaid-Signature header before trusting a payload. Each webhook has its own signing secret — open the webhook in the Integrations tab and use Copy next to Signing secret.

Compute an HMAC-SHA256 of the raw request body with that secret and compare it to the header:

import { createHmac, timingSafeEqual } from 'crypto';

const SIGNING_SECRET = process.env.QAID_WEBHOOK_SECRET;

function isValidSignature(rawBody, header) {
  const expected =
    'sha256=' + createHmac('sha256', SIGNING_SECRET).update(rawBody).digest('hex');
  const a = Buffer.from(header || '');
  const b = Buffer.from(expected);
  return a.length === b.length && timingSafeEqual(a, b);
}

// Express example — note express.raw() so you hash the exact bytes Qaid signed
app.post('/qaid-webhook', express.raw({ type: 'application/json' }), (req, res) => {
  if (!isValidSignature(req.body, req.get('X-Qaid-Signature'))) {
    return res.sendStatus(401);
  }
  const { event, data } = JSON.parse(req.body.toString());
  // ...handle the event
  res.sendStatus(200);
});

Sign against the exact bytes you received (don't re-serialize the parsed JSON first), or the signatures won't match.

Testing, enabling, and disabling

  • Test sends a sample feedback.created delivery to a single webhook so you can confirm the wiring — for Slack you'll see a message; for generic endpoints your server should receive a signed request.
  • Enable / Disable toggles a webhook without deleting it. A disabled webhook stops firing but keeps its settings and secret.
  • Edit lets you rename a webhook, change its URL, or adjust which events it listens to.

Best practices

Return quickly. Respond with a 2xx as soon as you've accepted the payload and do slow work asynchronously. Qaid records the response status for each delivery.

Subscribe narrowly. A Slack channel that only wants urgent items should listen to feedback.escalated, not every feedback.created. Pair escalation rules with a Slack webhook to route just the critical reports.

Keep the secret secret. Store the signing secret as an environment variable, never in client code. If it leaks, edit the webhook to rotate it.

Follow the link, not the payload. The payload is intentionally small. Use dashboardUrl to jump to the full item — including its screenshot and captured context — rather than trying to stuff everything into the webhook.

Pro feature

Webhooks and Slack delivery require a Pro plan. On the free plan you can view any existing webhooks, but creating, testing, and firing them requires an upgrade — if a Pro project is downgraded, its webhooks stop delivering until it's upgraded again.

Back to all articles