Webhooks
Get a signed HTTP request when reports and feature requests are created or change status.
Webhooks are on the Pro plan. Each project can send events to one HTTPS endpoint: your own server, or a Zapier, Make or n8n hook. Set it in the project's settings, under Capture, then use Send test event to check it.
Events
| Event | Sent when |
|---|---|
report.created | A report arrives from the widget or the API. |
report.status_changed | A report moves to a new status, from the dashboard, the API, the MCP server, or a synced Linear or GitHub issue. |
comment.created | Someone comments on a report. |
feature_request.created | Someone suggests a feature, in the widget, the dashboard or the API. |
feature_request.status_changed | A feature request moves to a new status. |
webhook.test | You press Send test event. It carries an example report. |
Payloads
Every event is a JSON POST with the same envelope:
{
"id": "5d7c0a4e-…",
"event": "report.status_changed",
"occurred_at": "2026-10-18T14:03:22.104Z",
"project": { "id": "1f0a…", "name": "Acme storefront" },
"report": {
"id": "c3d1…",
"title": "Checkout button does nothing",
"status": "Done",
"previous_status": "In Review",
"severity": "High",
"area": "Checkout",
"description": "Clicked Pay, nothing happened",
"page_url": "https://shop.example.com/checkout",
"reporter": { "name": "Ana", "email": "ana@example.com" },
"created_at": "2026-10-17T09:12:44Z",
"dashboard_url": "https://feedbackdots.com/reports/c3d1…",
"issues": { "linear": "https://linear.app/…", "jira": null, "github": null }
}
}
previous_status is only present on *.status_changed events. comment.created adds a comment object with id, author_name, body and created_at.
Feature request events carry a feature_request object instead of report:
{
"id": "0b8e…",
"event": "feature_request.status_changed",
"occurred_at": "2026-10-18T14:05:00.000Z",
"project": { "id": "1f0a…", "name": "Acme storefront" },
"feature_request": {
"id": "8b7e…",
"title": "Dark mode",
"description": "Our team works late",
"status": "shipped",
"previous_status": "in_progress",
"votes": 42,
"source": "widget",
"author": { "name": "Sam", "email": null },
"page_url": "https://app.example.com/settings",
"created_at": "2026-10-12T09:30:00Z",
"dashboard_url": "https://feedbackdots.com/feedback"
}
}
Fields may be added to payloads over time, so ignore ones you don't use.
Headers
| Header | Value |
|---|---|
X-FeedbackDots-Event | The event name. |
X-FeedbackDots-Delivery | A unique ID for this delivery, the same as the body's id. Use it to ignore duplicates. |
X-FeedbackDots-Timestamp | Unix seconds when the request was signed. |
X-FeedbackDots-Signature | sha256= followed by the hex HMAC-SHA256 of timestamp.body, keyed with the project's signing secret. |
Verifying the signature
Find the signing secret (whsec_...) under the project's webhook settings. Compute the HMAC over the timestamp, a dot, and the raw request body, exactly as received, and compare it with the header in constant time. Reject requests whose timestamp is more than five minutes old.
With the SDKs this is one call; see SDKs. By hand in Node.js:
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verify(secret, rawBody, headers) {
const timestamp = headers['x-feedbackdots-timestamp'];
const signature = headers['x-feedbackdots-signature'] || '';
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected = 'sha256=' + createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex');
return (
expected.length === signature.length && timingSafeEqual(Buffer.from(expected), Buffer.from(signature))
);
}
In Python:
import hashlib, hmac, time
def verify(secret, raw_body, headers):
timestamp = headers["X-FeedbackDots-Timestamp"]
if abs(time.time() - int(timestamp)) > 300:
return False
expected = "sha256=" + hmac.new(secret.encode(), f"{timestamp}.{raw_body}".encode(), hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, headers["X-FeedbackDots-Signature"])
Delivery
- Respond with any 2xx status within 5 seconds. Do slow work after responding.
- Redirects are not followed, and endpoints must be public HTTPS addresses.
- Each event is sent once. The settings page shows the result of the last delivery, so you can spot a failing endpoint.
- Rotating the secret in settings takes effect for the next event.