Skip to content

Webhooks

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)

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"
  }
}

Store the secret immediately. It cannot be retrieved again. If you lose it, use the rotate endpoint to generate a new one.

Event Types

Quackback exposes 36 webhook-subscribable events, grouped by resource family.

Post events

EventDescription
post.createdNew feedback post submitted
post.status_changedPost status updated
post.updatedPost title, content, tags, or owner changed
post.deletedPost soft-deleted
post.restoredDeleted post restored
post.mergedDuplicate post merged into a canonical post
post.unmergedMerged post separated back out
post.votedA user votes on a post

Comment events

EventDescription
comment.createdNew comment posted
comment.updatedComment edited
comment.deletedComment deleted

Board filtering applies only to post and comment events (see Board Filtering). Private comments never trigger a webhook delivery.

Changelog

EventDescription
changelog.publishedChangelog entry published

Conversation events

EventDescription
conversation.createdA visitor starts a new conversation
conversation.status_changedA conversation moves between open, snoozed, and closed
conversation.assignedA conversation is assigned to or unassigned from an agent (includes auto-routing)
conversation.priority_changedA conversation's priority changes
conversation.attribute_changedA conversation attribute is set or cleared by AI, a teammate, or a customer
conversation.csat_submittedA visitor submits a satisfaction rating
conversation.csat_comment_addedA visitor adds the optional comment to a satisfaction rating
conversation.note_mentionedAn internal note @-mentions a teammate (private, opt-in)
conversation.customer_unresponsiveThe customer has been silent for a workflow-configured time after the last reply
conversation.teammate_unresponsiveNo teammate has replied for a workflow-configured time after the customer's last message

Message events

EventDescription
message.createdA visitor or agent sends a public message
message.note_createdAn agent adds an internal note (private, opt-in)
message.deletedA public message is deleted
message.updatedThe author edits a public message

Conversation and message events require the support inbox to be enabled. System messages (like "chat ended") are represented by conversation.* events rather than message.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

EventDescription
ticket.createdTicket opened
ticket.status_changedTicket moves between open, pending, and closed
ticket.assignedTicket assigned to (or unassigned from) an agent or team
ticket.repliedAn agent or the requester replies on the ticket thread
ticket.note_addedAn agent adds an internal note to a ticket (private, opt-in)
ticket.external_status_changedA tracker issue linked to the ticket changes status on the external platform

Ticket events require the support tickets feature to be enabled.

Assistant

EventDescription
assistant.handed_offThe AI assistant hands a conversation to the team
assistant.resolvedThe AI assistant settles a conversation without handing off

SLA

EventDescription
sla.approaching_breachAn applied SLA clock enters its lead-time warning window
sla.breachedAn applied SLA clock passes its due date unsettled

Anonymous visitors' emails are never included in any payload. Synthetic placeholder addresses are stripped before delivery.

Request Headers

Each webhook delivery includes these headers:

HeaderDescription
Content-Typeapplication/json
User-AgentQuackback-Webhook/1.0 (+https://quackback.io)
X-Quackback-EventEvent type (e.g., post.created)
X-Quackback-SignatureHMAC-SHA256 signature for verification
X-Quackback-TimestampUnix 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": "jane@example.com",
      "voteCount": 12
    }
  }
}
FieldDescription
idUnique event ID (prefixed with evt_)
typeEvent type (e.g., post.created)
createdAtISO 8601 timestamp of when the event occurred
dataEvent-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.

ObjectKey fields
post (on post.created)id, title, content, boardId, boardSlug, authorEmail, authorName, voteCount
post (other post events)id, title, boardId, boardSlug
commentid, content, authorEmail, authorName, isPrivate
changelogid, title, contentPreview, publishedAt, linkedPostCount
conversationid, status, channel, priority, assignedTeamId
messageid, conversationId, senderType, authorPrincipalId, authorName, authorEmail, content, createdAt
ticketid, number, type, priority, assignedPrincipalId, assignedTeamId

ticket.replied and ticket.note_added also carry messageId, content, senderType, and an attachments array (name, url, contentType, size). ticket.external_status_changed carries title, integrationType, externalDisplayId, externalUrl, externalStatus, and transition (closed, reopened, or null when 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');
});

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.

Board filtering applies only to post and comment events. Conversation, message, and ticket events aren't tied to a board, so a webhook with boardIds set 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

  1. [Zapier / n8n] Create a Zap or n8n workflow with a webhook trigger
  2. Copy the webhook URL
  3. [Your App] Create a Quackback webhook pointing to that URL
  4. [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

  1. Always verify signatures - reject unverified requests
  2. Use HTTPS - never use HTTP endpoints for webhook receivers
  3. Check timestamps - reject requests older than 5 minutes
  4. Rotate secrets periodically - use the rotate endpoint
  5. Respond quickly - return a 200 status within a few seconds; process asynchronously if needed

Next Steps