Skip to content

Latest commit

 

History

History
282 lines (216 loc) · 8.6 KB

File metadata and controls

282 lines (216 loc) · 8.6 KB
title Webhooks
description Send real-time event notifications to any HTTP endpoint.
icon webhook

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)

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

Event Description
post.created New feedback post submitted
post.status_changed Post status updated
comment.created New comment added to a post
conversation.created A visitor starts a new conversation
conversation.status_changed A conversation moves between open, pending, and closed
conversation.assigned A conversation is assigned to or unassigned from an agent (includes auto-routing)
conversation.priority_changed A conversation's priority changes
conversation.csat_submitted A visitor submits a satisfaction rating
message.created A visitor or agent sends a public message
message.note_created An agent adds an internal note (private — subscribe only if your endpoint should receive it)
message.deleted A public message is deleted
Conversation and message events require the support inbox. Anonymous visitors' emails are never included (synthetic addresses are stripped). System messages (like "chat ended") are represented by `conversation.*` events rather than `message.created`, and internal-note deletions are not emitted.

Request Headers

Each webhook delivery includes these headers:

Header Description
Content-Type application/json
User-Agent Quackback-Webhook/1.0
X-Quackback-Event Event type (e.g., post.created)
X-Quackback-Signature HMAC-SHA256 signature for verification
X-Quackback-Timestamp 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": "jane@example.com",
      "voteCount": 12
    }
  }
}
Field Description
id Unique event ID (prefixed with evt_)
type Event type (e.g., post.created)
createdAt ISO 8601 timestamp of when the event occurred
data Event-specific payload (varies by event type)

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 and message events aren't tied to a board, so a webhook with `boardIds` set still receives every chat event 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