Webhooks & Slack: Push Feedback Into Your Stack
Post new feedback, escalations and quest answers to a Slack channel, or send them as signed JSON to your own endpoint.
Owners add webhooks on the project’s Integrations tab. A Slack webhook posts a short message with a link to the item; a Generic one posts signed JSON to your URL. Three events can fire them: new feedback, an escalation, and a finished quest.
Webhooks are Pro. On Free, Create and Test open the plans window and nothing is sent. Free vs Pro
The events
| Event | Fires when |
|---|---|
feedback.created |
Feedback arrives, from the thumbs embed or as a recording |
feedback.escalated |
One of your escalation rules flags an item, the first time |
quest.submitted |
A visitor finishes a quest, the first time they submit it |
The thumbs embed sends feedback at the click, before the visitor types. So feedback.created from the thumbs usually has no message, and its Slack post has no message line. The words arrive a moment later. A keyword rule that matches them fires feedback.escalated with the message in it. A recording carries its description from the start.
Post feedback to Slack
Get a Slack address
In Slack, add an Incoming Webhook to the channel and copy the address it gives you.
Start a webhook
Open the project’s Settings and click Integrations, a tab only owners see. Click New Webhook and give it a name if you like, such as “Team Slack”.
Paste and pick Slack
Paste the address into Endpoint URL and click Slack under Type. The type can’t be changed later.
Choose events and create
Feedback created starts ticked. Tick what else you want, then click Create.
A Slack post has a bold first line, “:speech_balloon: New feedback received” or “:rotating_light: Feedback escalated”. Under it come the message when there is one, the page, and View in Qaid, which opens the item. A quest post reads “:clipboard: New quest response” and links to the project.
Send feedback to your own endpoint
Leave Type on Generic and give an https:// address. Each event becomes a POST with a JSON body and two headers: X-Qaid-Event, the event name, and X-Qaid-Signature, sha256= followed by the signature.
A feedback event looks like this:
{
"event": "feedback.escalated",
"createdAt": "2026-09-25T15:40:00.000Z",
"data": {
"id": "clx8a1b2c3",
"feedbackType": "down",
"message": "Checkout says payment failed, but my card is fine",
"url": "https://acme.com/checkout",
"elementSelector": "#pay-button",
"elementText": "Pay now",
"hasScreenshot": true,
"consoleErrorCount": 2,
"isEscalated": true,
"projectId": "clp0z9y8x7",
"dashboardUrl": "https://qaid.dev/dashboard/projects/clp0z9y8x7/feedback/clx8a1b2c3"
}
}
feedbackType is up, down or neutral from the thumbs embed, and video for a recording. Screenshots are never sent, only hasScreenshot. dashboardUrl opens the item. The payload does not name the rule that matched. A quest.submitted body carries just responseId, questId and projectId in data.
Your signing secret is in the webhook’s Edit window, under Signing secret, with a Copy button. Only Generic webhooks show one.
Check the signature
Anyone who learns your address can post to it, so check X-Qaid-Signature first. It is an HMAC-SHA256 of the raw body, keyed with the signing secret:
import { createHmac, timingSafeEqual } from 'crypto';
import express from 'express';
const SIGNING_SECRET = process.env.QAID_WEBHOOK_SECRET;
const app = express();
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.raw keeps 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, then answer
res.sendStatus(200);
});
Hash the bytes you received. Parsing the JSON and writing it out again changes the spacing, and the signatures stop matching.
Test a webhook
Click Test on the webhook. It sends one sample feedback.created, whatever events you ticked: a thumbs up on https://example.com/test-page saying “This is a test webhook delivery from Qaid.” Its id is test-feedback, so its dashboardUrl leads nowhere. You see “Test delivery succeeded”, or the reason it failed, such as HTTP 404.
Any 2xx answer counts as delivered. Qaid tries each delivery once, with no retries, and the dashboard shows no delivery log. Answer quickly and do slow work afterwards.
Pause, change or delete a webhook
- Disable stops a webhook sending and keeps it. Enable starts it again.
- Edit changes the name, address, events and Enabled. Click Update to save.
- Delete asks first, then removes it for good.
The signing secret is set when the webhook is made and never changes. To replace it, create a new webhook, move your endpoint to the new secret, then delete the old one.
On Free, or after Pro ends, these three still work, but nothing is sent.