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.

Important:

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 submitted
  • feedback.status_updated - Triggered when feedback status changes
  • feedback.voted - Triggered when a user votes on feedback

API Key Events

  • apikey.created - Triggered when a new API key is created
  • apikey.revoked - Triggered when an API key is revoked
  • apikey.invalid - Triggered when an invalid API key is used
  • apikey.rate_limit_exceeded - Triggered when rate limit is exceeded
  • apikey.quota_exceeded - Triggered when monthly quota is exceeded

Project Events

  • project.updated - Triggered when project settings are updated
  • project.deleted - Triggered when a project is deleted

Category Events

  • category.created - Triggered when a category is created
  • category.updated - Triggered when a category is updated
  • category.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

  1. GrenFeedback calculates HMAC-SHA256 of the request body using your webhook secret
  2. The signature is sent in the X-GrenFeedback-Signature header
  3. Your endpoint calculates the same HMAC-SHA256 using the raw request body and your stored secret
  4. Compare the signatures - if they match, the request is authentic
Security Note:

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/json
  • X-GrenFeedback-Signature: <hmac_hex> - HMAC-SHA256 signature
  • X-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
end

Best 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.