Webhooks
Receive real-time notifications about events in your GrenFeedback projects
Real-time Notifications
Get instant notifications when feedback is created, updated, or when API key errors occur. All webhooks are signed with HMAC-SHA256 for security.
Overview
Webhooks allow you to receive real-time notifications about events that occur in your GrenFeedback projects. When an event occurs, GrenFeedback sends an HTTP POST request to your configured webhook URL with a JSON payload containing the event data.
All webhook requests are signed with HMAC-SHA256 using a secret that is unique to each webhook. You must verify the signature to ensure the request is authentic and comes from GrenFeedback.
Getting Started
1. Create a Webhook
Navigate to your project settings in the GrenFeedback dashboard and go to the Webhooks tab. Click "New Webhook" and configure:
- URL: The HTTPS endpoint where you want to receive webhook notifications
- Events: Select which events you want to receive (you can select multiple events if your tier allows it)
- Description: Optional description to help you identify the webhook
After creating the webhook, you'll receive a secret that you must store securely. This secret is only shown once and is used to verify webhook signatures.
Save the webhook secret immediately. You won't be able to see it again. If you lose it, you'll need to recreate the webhook.
2. Configure Your Endpoint
Your webhook endpoint must:
- Accept POST requests
- Use HTTPS (required for security)
- Return a 2xx status code within 10 seconds
- Verify the webhook signature before processing
Available Events
The following events can trigger webhooks:
Feedback Events
feedback.created- Triggered when new feedback is submittedfeedback.status_updated- Triggered when feedback status changesfeedback.voted- Triggered when a user votes on feedback
API Key Events
apikey.created- Triggered when a new API key is createdapikey.revoked- Triggered when an API key is revokedapikey.invalid- Triggered when an invalid API key is usedapikey.rate_limit_exceeded- Triggered when rate limit is exceededapikey.quota_exceeded- Triggered when monthly quota is exceeded
Project Events
project.updated- Triggered when project settings are updatedproject.deleted- Triggered when a project is deleted
Category Events
category.created- Triggered when a category is createdcategory.updated- Triggered when a category is updatedcategory.deleted- Triggered when a category is deleted
Payload Structure
All webhook payloads follow this structure:
{
"eventType": "feedback.created",
"projectId": "project_123",
"organizationId": "org_456",
"timestamp": "2024-01-15T10:30:00.000Z",
"data": {
"feedbackId": "fb_789",
"title": "Bug in login screen",
"description": "Users cannot log in...",
"categoryId": "cat_abc",
"authorId": "user_xyz",
"status": "new",
"platform": "ios",
"createdAt": "2024-01-15T10:30:00.000Z"
}
}Error Payloads
Some events (like API key errors) include an error object:
{
"eventType": "apikey.quota_exceeded",
"projectId": "project_123",
"organizationId": "org_456",
"timestamp": "2024-01-15T10:30:00.000Z",
"data": {
"apiKeyPrefix": "abc12345",
"current": 1000,
"limit": 500
},
"error": {
"code": "QUOTA_EXCEEDED",
"message": "Monthly feedback limit exceeded",
"details": {
"current": 1000,
"limit": 500
}
}
}Signature Verification
Every webhook request includes a signature in the X-GrenFeedback-Signature header. You must verify this signature to ensure the request is authentic.
How It Works
- GrenFeedback calculates
HMAC-SHA256of the request body using your webhook secret - The signature is sent in the
X-GrenFeedback-Signatureheader - Your endpoint calculates the same HMAC-SHA256 using the raw request body and your stored secret
- Compare the signatures - if they match, the request is authentic
Always verify the signature before processing webhook data. Never trust unverified requests, as they could be malicious.
Headers
Each webhook request includes these headers:
Content-Type: application/jsonX-GrenFeedback-Signature: <hmac_hex>- HMAC-SHA256 signatureX-GrenFeedback-Event: <event_type>- Event type (for quick routing)User-Agent: GrenFeedback-Webhooks/1.0
Code Examples
Node.js / Express
const express = require('express');
const crypto = require('crypto');
const app = express();
// Store your webhook secret securely
const WEBHOOK_SECRET = 'your_webhook_secret_here';
// Middleware to verify webhook signature
function verifyWebhookSignature(req, res, next) {
const signature = req.headers['x-grenfeedback-signature'];
const eventType = req.headers['x-grenfeedback-event'];
if (!signature) {
return res.status(401).json({ error: 'Missing signature' });
}
// Get raw body (must be raw, not parsed JSON)
const rawBody = JSON.stringify(req.body);
// Calculate expected signature
const expectedSignature = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(rawBody)
.digest('hex');
// Compare signatures (use constant-time comparison)
if (signature !== expectedSignature) {
return res.status(401).json({ error: 'Invalid signature' });
}
next();
}
// Parse JSON body
app.use(express.json());
// Webhook endpoint
app.post('/webhooks/grenfeedback', verifyWebhookSignature, (req, res) => {
const { eventType, data, timestamp } = req.body;
// Process the webhook
switch (eventType) {
case 'feedback.created':
console.log('New feedback:', data);
// Handle new feedback
break;
case 'feedback.status_updated':
console.log('Status updated:', data);
// Handle status update
break;
// ... handle other events
}
// Always return 200 to acknowledge receipt
res.status(200).json({ received: true });
});
app.listen(3000, () => {
console.log('Webhook server listening on port 3000');
});Python / Flask
from flask import Flask, request, jsonify
import hmac
import hashlib
import json
app = Flask(__name__)
# Store your webhook secret securely
WEBHOOK_SECRET = 'your_webhook_secret_here'
def verify_webhook_signature(request):
"""Verify the webhook signature"""
signature = request.headers.get('X-GrenFeedback-Signature')
if not signature:
return False
# Get raw body
raw_body = request.get_data()
# Calculate expected signature
expected_signature = hmac.new(
WEBHOOK_SECRET.encode('utf-8'),
raw_body,
hashlib.sha256
).hexdigest()
# Compare signatures (use constant-time comparison)
return hmac.compare_digest(signature, expected_signature)
@app.route('/webhooks/grenfeedback', methods=['POST'])
def webhook():
# Verify signature
if not verify_webhook_signature(request):
return jsonify({'error': 'Invalid signature'}), 401
# Parse JSON payload
payload = request.get_json()
event_type = payload.get('eventType')
data = payload.get('data')
# Process the webhook
if event_type == 'feedback.created':
print(f'New feedback: {data}')
# Handle new feedback
elif event_type == 'feedback.status_updated':
print(f'Status updated: {data}')
# Handle status update
# ... handle other events
# Always return 200 to acknowledge receipt
return jsonify({'received': True}), 200
if __name__ == '__main__':
app.run(port=3000)PHP
<?php
// Store your webhook secret securely
define('WEBHOOK_SECRET', 'your_webhook_secret_here');
function verifyWebhookSignature($rawBody, $signature) {
if (empty($signature)) {
return false;
}
// Calculate expected signature
$expectedSignature = hash_hmac('sha256', $rawBody, WEBHOOK_SECRET);
// Compare signatures (use constant-time comparison)
return hash_equals($signature, $expectedSignature);
}
// Get raw request body
$rawBody = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_GRENFEEDBACK_SIGNATURE'] ?? '';
// Verify signature
if (!verifyWebhookSignature($rawBody, $signature)) {
http_response_code(401);
echo json_encode(['error' => 'Invalid signature']);
exit;
}
// Parse JSON payload
$payload = json_decode($rawBody, true);
$eventType = $payload['eventType'] ?? '';
$data = $payload['data'] ?? [];
// Process the webhook
switch ($eventType) {
case 'feedback.created':
error_log('New feedback: ' . json_encode($data));
// Handle new feedback
break;
case 'feedback.status_updated':
error_log('Status updated: ' . json_encode($data));
// Handle status update
break;
// ... handle other events
}
// Always return 200 to acknowledge receipt
http_response_code(200);
echo json_encode(['received' => true]);
?>Ruby / Sinatra
require 'sinatra'
require 'json'
require 'openssl'
# Store your webhook secret securely
WEBHOOK_SECRET = 'your_webhook_secret_here'
def verify_webhook_signature(request)
signature = request.env['HTTP_X_GRENFEEDBACK_SIGNATURE']
return false if signature.nil? || signature.empty?
# Get raw body
raw_body = request.body.read
request.body.rewind
# Calculate expected signature
expected_signature = OpenSSL::HMAC.hexdigest(
OpenSSL::Digest.new('sha256'),
WEBHOOK_SECRET,
raw_body
)
# Compare signatures (use constant-time comparison)
ActiveSupport::SecurityUtils.secure_compare(signature, expected_signature)
rescue
false
end
post '/webhooks/grenfeedback' do
# Verify signature
unless verify_webhook_signature(request)
status 401
return { error: 'Invalid signature' }.to_json
end
# Parse JSON payload
payload = JSON.parse(request.body.read)
event_type = payload['eventType']
data = payload['data']
# Process the webhook
case event_type
when 'feedback.created'
puts "New feedback: #{data.inspect}"
# Handle new feedback
when 'feedback.status_updated'
puts "Status updated: #{data.inspect}"
# Handle status update
# ... handle other events
end
# Always return 200 to acknowledge receipt
status 200
{ received: true }.to_json
endBest Practices
Always Verify Signatures
Never process webhook data without verifying the signature. This ensures the request is authentic and hasn't been tampered with.
Use Raw Request Body
When verifying signatures, use the raw request body (before JSON parsing). The signature is calculated on the exact bytes sent.
Return 200 Quickly
Acknowledge receipt of the webhook with a 200 status code immediately, then process the data asynchronously. GrenFeedback will retry if it doesn't receive a 200 response within 10 seconds.
Handle Idempotency
Webhooks may be delivered multiple times. Use the event timestamp and event type to ensure you don't process the same event twice.
Store Secrets Securely
Never commit webhook secrets to version control. Use environment variables or a secure secrets management system.
Use HTTPS
Webhook URLs must use HTTPS. This ensures the data is encrypted in transit and protects against man-in-the-middle attacks.
Retry Logic
GrenFeedback automatically retries failed webhook deliveries:
- 3 retry attempts with exponential backoff (1s, 2s, 4s)
- 10 second timeout per request
- Retries occur if your endpoint returns a non-2xx status code or times out
- After all retries fail, the webhook failure count is incremented
Monitor your webhook success/failure rates in the dashboard to ensure reliable delivery.