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
- Navigate to your organization settings
- Go to Webhooks section
- Click Create Webhook
- 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
- Check webhook is enabled (green status)
- Verify the correct events are selected
- Check board filters aren't excluding events
- Review recent delivery history for errors
Signature Verification Failing
- Ensure you're using the raw request body (not parsed JSON)
- Check the timestamp format is correct
- Verify the secret hasn't been regenerated
Webhook Auto-Disabled
Webhooks are automatically disabled after 10 consecutive failures. To re-enable:
- Fix the underlying issue
- Click Enable on the webhook
- Use Send Test to verify it works
Support
Need help? Contact us: - Email: support@feedbackecho.com - Documentation: https://docs.feedbackecho.com