Webhook Integration Guide

Webhooks allow you to receive real-time notifications when events occur in FeedbackEcho. This guide explains how to set up, secure, and handle webhook deliveries.

Overview

When you configure a webhook, FeedbackEcho sends HTTP POST requests to your specified URL whenever subscribed events occur. This enables you to:

  • Sync feedback data with external systems
  • Trigger automated workflows
  • Update dashboards in real-time
  • Send notifications to team channels

Setting Up Webhooks

Creating a Webhook

  1. Navigate to your organization settings
  2. Go to Webhooks section
  3. Click Create Webhook
  4. Configure the webhook:
    • Name: A descriptive name for identification
    • URL: Your endpoint that will receive events
    • Events: Select which events to subscribe to
    • Boards (optional): Filter events to specific boards

Webhook Secret

When you create a webhook, a secret key is generated. This secret is used to verify that webhook deliveries are genuine. Copy and store the secret immediately - it won't be shown again.

Event Types

Feedback Events

Event Description
feedback.created New feedback item submitted
feedback.updated Feedback item details changed
feedback.deleted Feedback item deleted
feedback.status_changed Status changed (e.g., open → in_progress)
feedback.voted Someone voted on feedback

Comment Events

Event Description
comment.created New comment added
comment.deleted Comment removed

Board Events

Event Description
board.created New feedback board created
board.updated Board settings changed

Member Events

Event Description
member.joined User joined the organization
member.left User left the organization
member.role_changed Member's role updated

Payload Format

All webhook payloads follow this structure:

{
  "event": "feedback.created",
  "webhook_id": "whk_abc123xyz",
  "organization_id": "org_01HQXYZ",
  "timestamp": "2024-01-15T10:30:00Z",
  "data": {
    // Event-specific data
  }
}

Feedback Created Payload

{
  "event": "feedback.created",
  "webhook_id": "whk_abc123xyz",
  "organization_id": "org_01HQXYZ",
  "timestamp": "2024-01-15T10:30:00Z",
  "data": {
    "id": "fb_xyz789",
    "title": "Add dark mode",
    "description": "Please add a dark mode option",
    "status": "open",
    "category": "feature_request",
    "priority": null,
    "board": {
      "id": "brd_abc123",
      "name": "Product Feedback",
      "slug": "product-feedback"
    },
    "submitter": {
      "name": "John Doe",
      "email": "john@example.com"
    },
    "tags": [],
    "created_at": "2024-01-15T10:30:00Z"
  }
}

Feedback Status Changed Payload

{
  "event": "feedback.status_changed",
  "webhook_id": "whk_abc123xyz",
  "organization_id": "org_01HQXYZ",
  "timestamp": "2024-01-15T14:00:00Z",
  "data": {
    "id": "fb_xyz789",
    "title": "Add dark mode",
    "previous_status": "open",
    "new_status": "in_progress",
    "changed_by": {
      "id": "usr_abc123",
      "name": "Admin User"
    }
  }
}

Comment Created Payload

{
  "event": "comment.created",
  "webhook_id": "whk_abc123xyz",
  "organization_id": "org_01HQXYZ",
  "timestamp": "2024-01-15T11:00:00Z",
  "data": {
    "id": "cmt_abc123",
    "content": "Great idea! We'll look into this.",
    "is_internal": false,
    "feedback_item": {
      "id": "fb_xyz789",
      "title": "Add dark mode"
    },
    "author": {
      "id": "usr_abc123",
      "name": "Support Team"
    },
    "created_at": "2024-01-15T11:00:00Z"
  }
}

Verifying Webhooks

To ensure webhook deliveries are genuine, verify the signature included in each request.

Signature Header

Each webhook request includes these headers:

X-FeedbackEcho-Event: feedback.created
X-FeedbackEcho-Delivery: whd_delivery123
X-FeedbackEcho-Timestamp: 1705315800
X-FeedbackEcho-Signature: sha256=5d4174...

Verification Algorithm

The signature is computed using HMAC-SHA256:

signature = HMAC-SHA256(secret, timestamp + "." + payload)

Verification Examples

Node.js

const crypto = require('crypto');

function verifyWebhook(payload, signature, timestamp, secret) {
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}.${payload}`)
    .digest('hex');

  const receivedSignature = signature.replace('sha256=', '');

  return crypto.timingSafeEqual(
    Buffer.from(expectedSignature),
    Buffer.from(receivedSignature)
  );
}

// Express.js handler
app.post('/webhooks/feedbackecho', express.raw({type: 'application/json'}), (req, res) => {
  const signature = req.headers['x-feedbackecho-signature'];
  const timestamp = req.headers['x-feedbackecho-timestamp'];
  const payload = req.body.toString();

  if (!verifyWebhook(payload, signature, timestamp, process.env.WEBHOOK_SECRET)) {
    return res.status(401).send('Invalid signature');
  }

  const event = JSON.parse(payload);

  // Handle the event
  switch (event.event) {
    case 'feedback.created':
      handleNewFeedback(event.data);
      break;
    case 'feedback.status_changed':
      handleStatusChange(event.data);
      break;
    // ... handle other events
  }

  res.status(200).send('OK');
});

Python

import hmac
import hashlib

def verify_webhook(payload: bytes, signature: str, timestamp: str, secret: str) -> bool:
    expected = hmac.new(
        secret.encode(),
        f"{timestamp}.{payload.decode()}".encode(),
        hashlib.sha256
    ).hexdigest()

    received = signature.replace('sha256=', '')

    return hmac.compare_digest(expected, received)

# Flask handler
@app.route('/webhooks/feedbackecho', methods=['POST'])
def handle_webhook():
    signature = request.headers.get('X-FeedbackEcho-Signature')
    timestamp = request.headers.get('X-FeedbackEcho-Timestamp')
    payload = request.get_data()

    if not verify_webhook(payload, signature, timestamp, WEBHOOK_SECRET):
        return 'Invalid signature', 401

    event = request.get_json()

    # Handle event based on type
    if event['event'] == 'feedback.created':
        handle_new_feedback(event['data'])

    return 'OK', 200

Ruby

require 'openssl'

def verify_webhook(payload, signature, timestamp, secret)
  expected = OpenSSL::HMAC.hexdigest(
    'SHA256',
    secret,
    "#{timestamp}.#{payload}"
  )

  received = signature.gsub('sha256=', '')

  ActiveSupport::SecurityUtils.secure_compare(expected, received)
end

# Rails controller
class WebhooksController < ApplicationController
  skip_before_action :verify_authenticity_token

  def feedbackecho
    signature = request.headers['X-FeedbackEcho-Signature']
    timestamp = request.headers['X-FeedbackEcho-Timestamp']
    payload = request.raw_post

    unless verify_webhook(payload, signature, timestamp, ENV['WEBHOOK_SECRET'])
      return head :unauthorized
    end

    event = JSON.parse(payload)

    case event['event']
    when 'feedback.created'
      FeedbackHandler.new_feedback(event['data'])
    when 'feedback.status_changed'
      FeedbackHandler.status_changed(event['data'])
    end

    head :ok
  end
end

Best Practices

Respond Quickly

Your endpoint should return a 2xx status code within 30 seconds. If you need to do heavy processing, acknowledge the webhook immediately and process asynchronously:

app.post('/webhooks/feedbackecho', async (req, res) => {
  // Immediately acknowledge receipt
  res.status(200).send('OK');

  // Process asynchronously
  queue.add('process-webhook', req.body);
});

Handle Retries

If your endpoint returns an error (non-2xx status) or times out, FeedbackEcho will retry the delivery:

Attempt Delay
1 Immediate
2 1 minute
3 5 minutes
4 15 minutes
5 30 minutes

After 5 failed attempts, the webhook will be marked as failed.

Implement Idempotency

Use the X-FeedbackEcho-Delivery header to handle duplicate deliveries:

const processedDeliveries = new Set();

app.post('/webhooks/feedbackecho', (req, res) => {
  const deliveryId = req.headers['x-feedbackecho-delivery'];

  if (processedDeliveries.has(deliveryId)) {
    return res.status(200).send('Already processed');
  }

  // Process the webhook...

  processedDeliveries.add(deliveryId);
  res.status(200).send('OK');
});

Validate Timestamps

Reject webhooks with old timestamps to prevent replay attacks:

const MAX_AGE_SECONDS = 300; // 5 minutes

function isTimestampValid(timestamp) {
  const now = Math.floor(Date.now() / 1000);
  return Math.abs(now - parseInt(timestamp)) < MAX_AGE_SECONDS;
}

Testing Webhooks

Test Delivery

Use the "Send Test" button in the webhook settings to send a test payload:

{
  "event": "test",
  "webhook_id": "whk_abc123xyz",
  "organization_id": "org_01HQXYZ",
  "timestamp": "2024-01-15T10:30:00Z",
  "data": {
    "message": "This is a test webhook delivery from FeedbackEcho."
  }
}

Local Development

For local development, use a tunnel service like ngrok:

ngrok http 3000

Then use the ngrok URL as your webhook endpoint.

Troubleshooting

Webhook Not Receiving Events

  1. Check webhook is enabled (green status)
  2. Verify the correct events are selected
  3. Check board filters aren't excluding events
  4. Review recent delivery history for errors

Signature Verification Failing

  1. Ensure you're using the raw request body (not parsed JSON)
  2. Check the timestamp format is correct
  3. Verify the secret hasn't been regenerated

Webhook Auto-Disabled

Webhooks are automatically disabled after 10 consecutive failures. To re-enable:

  1. Fix the underlying issue
  2. Click Enable on the webhook
  3. Use Send Test to verify it works

Support

Need help? Contact us: - Email: support@feedbackecho.com - Documentation: https://docs.feedbackecho.com