Webhooks
Send real-time event notifications to any HTTP endpoint.
Build any integration you need. Webhooks send real-time HTTP notifications to any endpoint when events occur in Quackback. Connect to Zapier, your data warehouse, custom bots, or internal tools.
Overview
Webhooks are managed entirely through the REST API. Each webhook specifies:
- A URL to receive POST requests
- Which events to listen for
- Optional board filtering to scope events to specific boards
- A signing secret for verifying authenticity (HMAC-SHA256)
Note:
Each workspace can have up to 25 webhooks.
Create a webhook
[Your App] Create a webhook via the API:
curl -X POST https://feedback.example.com/api/v1/webhooks \
-H "Authorization: Bearer qb_your_admin_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, which is only shown once:
{
"data": {
"id": "webhook_01h455vb4pex5vsknk084sn02q",
"url": "https://your-server.com/webhook",
"secret": "whsec_...",
"events": ["post.created", "post.status_changed"],
"boardIds": ["board_01h455vb4pex5vsknk084sn02q"],
"status": "active",
"createdAt": "2025-01-15T10:30:00.000Z"
}
}Warning:
Store thesecretimmediately. It cannot be retrieved again. If you lose it, use the rotate endpoint to generate a new one.
Event Types
Quackback exposes 32 webhook-subscribable events, grouped by resource family.
Post events
Event | Description |
|---|---|
| New feedback post submitted |
| Post status updated |
| Post title, content, tags, or owner changed |
| Post soft-deleted |
| Deleted post restored |
| Duplicate post merged into a canonical post |
| Merged post separated back out |
Comment events
Event | Description |
|---|---|
| New comment posted |
| Comment edited |
| Comment deleted |
Note:
Board filtering applies only to post and comment events (see Board Filtering). Private comments never trigger a webhook delivery.
Changelog
Event | Description |
|---|---|
| Changelog entry published |
Conversation events
Event | Description |
|---|---|
| A visitor starts a new conversation |
| A conversation moves between open, snoozed, and closed |
| A conversation is assigned to or unassigned from an agent (includes auto-routing) |
| A conversation's priority changes |
| A conversation attribute is set or cleared by AI, a teammate, or a customer |
| A visitor submits a satisfaction rating |
| A visitor adds the optional comment to a satisfaction rating |
| An internal note @-mentions a teammate (private, opt-in) |
| The customer has been silent for a workflow-configured time after the last reply |
| No teammate has replied for a workflow-configured time after the customer's last message |
Message events
Event | Description |
|---|---|
| A visitor or agent sends a public message |
| An agent adds an internal note (private, opt-in) |
| A public message is deleted |
Note:
Conversation and message events require the support inbox to be enabled. System messages (like "chat ended") are represented byconversation.*events rather thanmessage.created, and internal-note deletions are never emitted. An internal note never reached the visitor, so its deletion doesn't reach one either.
Ticket events
Event | Description |
|---|---|
| Ticket opened |
| Ticket moves between open, pending, and closed |
| Ticket assigned to (or unassigned from) an agent or team |
| An agent or the requester replies on the ticket thread |
| An agent adds an internal note to a ticket (private, opt-in) |
| A tracker issue linked to the ticket changes status on the external platform |
Note:
Ticket events require the support tickets feature to be enabled.
Assistant
Event | Description |
|---|---|
| The AI assistant hands a conversation to the team |
SLA
Event | Description |
|---|---|
| An applied SLA clock enters its lead-time warning window |
| An applied SLA clock passes its due date unsettled |
Note:
Anonymous visitors' emails are never included in any payload. Synthetic placeholder addresses are stripped before delivery.
Request Headers
Each webhook delivery includes these headers:
Header | Description |
|---|---|
|
|
|
|
| Event type (e.g., |
| HMAC-SHA256 signature for verification |
| Unix timestamp of the request |
Payload Format
Every webhook delivery sends a JSON body with this structure:
{
"id": "evt_a1b2c3d4e5f6...",
"type": "post.created",
"createdAt": "2025-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
}
}
}Field | Description |
|---|---|
| Unique event ID (prefixed with |
| Event type (e.g., |
| ISO 8601 timestamp of when the event occurred |
| Event-specific payload (varies by event type) |
Payload shapes by family
data nests one or more of these objects, named after the event. Status-change events add previousStatus/newStatus (or previousPriority/newPriority); assignment events add assigned*/previous* id pairs.
Object | Key fields |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Note:
ticket.repliedandticket.note_addedalso carrymessageId,content,senderType, and anattachmentsarray (name,url,contentType,size).ticket.external_status_changedcarriestitle,integrationType,externalDisplayId,externalUrl,externalStatus, andtransition(closed,reopened, ornullwhen the provider reports only a status name).
Signature Verification
[Your Server] Every webhook delivery is signed with HMAC-SHA256. Always verify the signature to confirm the request came from Quackback.
The signature is computed over \{timestamp\}.\{json_body\}:
import crypto from 'crypto';
function verifyWebhook(payload, signature, timestamp, secret) {
const signaturePayload = `${timestamp}.${payload}`;
const expected = crypto
.createHmac('sha256', secret)
.update(signaturePayload)
.digest('hex');
return `sha256=${expected}` === signature;
}
// In your handler
app.post('/webhook', (req, res) => {
const signature = req.headers['x-quackback-signature'];
const timestamp = req.headers['x-quackback-timestamp'];
const payload = JSON.stringify(req.body);
if (!verifyWebhook(payload, signature, timestamp, WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
// Process the event
console.log('Event:', req.body.type);
res.status(200).send('OK');
});Tip:
Also check that the timestamp is recent (within 5 minutes) to prevent replay attacks.
Board Filtering
Pass boardIds when creating a webhook to only receive events from specific boards. If omitted, the webhook receives events from all boards.
Note:
Board filtering applies only to post and comment events. Conversation, message, and ticket events aren't tied to a board, so a webhook withboardIdsset still receives every event of those types it subscribed to.
# Only events from a specific board
curl -X POST https://feedback.example.com/api/v1/webhooks \
-H "Authorization: Bearer qb_your_admin_key" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-server.com/webhook",
"events": ["post.created"],
"boardIds": ["board_01h455vb4pex5vsknk084sn02q"]
}'Manage webhooks
List webhooks
curl https://feedback.example.com/api/v1/webhooks \
-H "Authorization: Bearer qb_your_admin_key"Update a webhook
curl -X PATCH https://feedback.example.com/api/v1/webhooks/webhook_01h4... \
-H "Authorization: Bearer qb_your_admin_key" \
-H "Content-Type: application/json" \
-d '{ "events": ["post.created", "comment.created"] }'Rotate the signing secret
If a secret is compromised, rotate it:
curl -X POST https://feedback.example.com/api/v1/webhooks/webhook_01h4.../rotate \
-H "Authorization: Bearer qb_your_admin_key"The response contains the new secret. Update your server immediately.
Delete a webhook
curl -X DELETE https://feedback.example.com/api/v1/webhooks/webhook_01h4... \
-H "Authorization: Bearer qb_your_admin_key"Use Cases
Custom Slack Bot
Build a Slack bot with more control than the built-in integration:
app.post('/webhook', async (req, res) => {
const { type, data } = req.body;
if (type === 'post.created') {
await slack.chat.postMessage({
channel: '#feedback',
text: `New feedback: ${data.post.title}`,
});
}
res.sendStatus(200);
});Zapier / n8n Integration
- [Zapier / n8n] Create a Zap or n8n workflow with a webhook trigger
- Copy the webhook URL
- [Your App] Create a Quackback webhook pointing to that URL
- [Zapier / n8n] Map fields in your automation tool
Data Warehouse Sync
Log events to your analytics platform:
app.post('/webhook', async (req, res) => {
const { type, createdAt, data } = req.body;
await db.insert('feedback_events', {
event_type: type,
event_time: createdAt,
post_id: data.post?.id,
raw_payload: JSON.stringify(data),
});
res.sendStatus(200);
});Retry Behavior
Failed webhook deliveries are retried automatically:
- Attempts: Up to 3 delivery attempts per event
- Backoff: Exponential backoff starting at 1 second
- Retryable errors: Server errors (5xx), rate limits (429), and network timeouts
- Non-retryable errors: Client errors (4xx except 429) are not retried
- Timeout: Each delivery attempt has a 5-second timeout
- Auto-disable: After 50 consecutive permanent failures, the webhook is automatically disabled. The failure counter resets on each successful delivery.
Security Best Practices
- Always verify signatures - reject unverified requests
- Use HTTPS - never use HTTP endpoints for webhook receivers
- Check timestamps - reject requests older than 5 minutes
- Rotate secrets periodically - use the rotate endpoint
- Respond quickly - return a
200status within a few seconds; process asynchronously if needed
Next Steps
- API Overview - Full REST API documentation
- Integrations Overview - All available integrations
- Slack - Built-in Slack notification integration
Was this helpful?
Your feedback shapes what we write next.