Webhooks
Send real-time HTTPS notifications to your endpoint: create webhooks in the admin or API, all 36 events, payloads, signature verification, and retry behavior.
Webhooks send an HTTPS POST to your endpoint when something happens in your workspace, such as a new post, a status change, a new conversation or a ticket reply. Use them to connect custom bots, data warehouses, internal tools, or automation platforms like Zapier and n8n. This article covers creating webhooks, the event list, payloads, signature verification and delivery behavior.
Note: On Quackback Cloud, webhooks are included from the Pro plan. Each workspace can have up to 25 webhooks.
Create a webhook in the admin
- Go to Admin > Settings > Developers and open the Webhooks tab.
- Click Create webhook.
- Enter the Endpoint URL. It must use HTTPS.
- Under Events, select at least one event.
- Click Create webhook.
- Copy the Signing secret now. It is shown only once.
From the list you can edit a webhook's URL and events, turn it off (an off webhook receives no events), rotate its secret, or delete it. Turning a webhook back on resets its failure count.
Create a webhook with the API
You can also manage webhooks through the REST API with an API key:
curl -X POST https://feedback.example.com/api/v1/webhooks \
-H "Authorization: Bearer qb_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-server.com/webhook",
"events": ["post.created", "post.status_changed"],
"boardIds": ["board_01h455vb4pex5vsknk084sn02q"]
}'The response includes the signing secret (whsec_...), shown only once. Other endpoints:
Method | Path | Does |
|---|---|---|
|
| List webhooks |
|
| Get one webhook |
|
| Update |
|
| Rotate the signing secret (the response contains the new one) |
|
| Delete the webhook |
boardIds limits post and comment events to specific boards. Leave it out to receive events from all boards. Conversation, message and ticket events aren't tied to a board, so they are always delivered for the events you subscribed to.
Events
Posts
Event | Sent when |
|---|---|
| New feedback is submitted |
| A post's status changes |
| A post's title, content, tags or owner changes |
| A post is deleted |
| A deleted post is restored |
| A duplicate is merged into another post |
| A merged post is separated again |
| Someone votes on a post |
Comments
Event | Sent when |
|---|---|
| A new comment is posted |
| A comment is edited |
| A comment is deleted |
Note: Private comments never trigger a webhook.
Changelog
Event | Sent when |
|---|---|
| A changelog entry is published |
Conversations
Event | Sent when |
|---|---|
| A customer starts a conversation |
| A conversation moves between open, snoozed and closed |
| A conversation is assigned or unassigned (including auto-routing) |
| A conversation's priority changes |
| A conversation attribute is set or cleared by AI, a teammate or a customer |
| A customer submits a satisfaction rating |
| A customer adds a comment to their rating |
| An internal note @-mentions a teammate |
| The customer has been silent for a workflow-configured time |
| No teammate has replied for a workflow-configured time |
Messages
Event | Sent when |
|---|---|
| A customer or teammate sends a public message |
| A teammate adds an internal note |
| The author edits a public message |
| A public message is deleted |
Note: Conversation and message events need the Support module. System messages (such as "chat ended") arrive as
conversation.*events, notmessage.created.
Tickets
Event | Sent when |
|---|---|
| A ticket is opened |
| A ticket moves between open, pending and closed |
| A ticket is assigned to or unassigned from a teammate or team |
| A teammate or the requester replies on the ticket |
| A teammate adds an internal note to a ticket |
| A linked issue in an issue tracker changes status |
Quinn and SLAs
Event | Sent when |
|---|---|
| Quinn hands a conversation to your team |
| Quinn settles a conversation without a handoff |
| An SLA clock enters its warning window |
| An SLA clock passes its due time |
Note: Anonymous visitors' placeholder emails are never included in a payload.
Request headers
Header | Value |
|---|---|
|
|
|
|
| The event type, for example |
|
|
| Unix timestamp (seconds) of the delivery |
Payload
Every delivery sends a JSON body with this shape:
{
"id": "evt_a1b2c3d4e5f6...",
"type": "post.created",
"createdAt": "2026-01-15T10:30:00.000Z",
"data": {
"post": {
"id": "post_01h455vb4pex5vsknk084sn02q",
"title": "Add dark mode support",
"content": "It would be great to have a dark mode...",
"boardId": "board_01h455vb4pex5vsknk084sn02q",
"boardSlug": "feature-requests",
"authorEmail": "[email protected]",
"voteCount": 12
}
}
}data holds objects named after the event family:
Object | Key fields |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Status-change events add previousStatus and newStatus (or previousPriority and newPriority). Assignment events add the new and previous assignee ids. ticket.replied and ticket.note_added also carry messageId, content, senderType and an attachments array.
Verify the signature
Every delivery is signed with HMAC-SHA256 over {timestamp}.{raw body} using your signing secret. Always verify it, using the raw request body exactly as received:
import crypto from 'crypto';
import express from 'express';
const app = express();
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.header('x-quackback-signature');
const timestamp = req.header('x-quackback-timestamp');
const rawBody = req.body.toString('utf8');
const expected = 'sha256=' + crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
const valid =
signature &&
signature.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
const fresh = Math.abs(Date.now() / 1000 - Number(timestamp)) < 300;
if (!valid || !fresh) return res.status(401).send('Invalid signature');
const event = JSON.parse(rawBody);
// Handle event.type here, then respond quickly
res.status(200).send('OK');
});The admin's Webhooks tab also shows a verification guide with examples for other frameworks.
Delivery and retries
- Each delivery attempt times out after 5 seconds. Respond with a
2xxquickly and do slow work afterwards. - Server errors (
5xx), rate limits (429) and timeouts are retried: twice within a few seconds, then after about 1, 2 and 4 hours. That is up to 6 attempts over roughly 7 hours. - Other client errors (
4xx) are not retried. - After 50 consecutive permanent failures, the webhook is turned off automatically. A successful delivery resets the count.
Best practices
- Always verify signatures and reject requests older than 5 minutes.
- Use HTTPS endpoints only (required).
- Rotate the secret if it may have leaked.
- Respond within a few seconds and process events in the background.
- Use the event
idto ignore duplicates if your handler might see an event twice.
Related articles
Was this helpful?
Your feedback shapes what we write next.
