Learn how to receive real-time updates from Bugbop
Webhooks allow you to receive real-time notifications when events happen in your Bugbop programs. Webhooks push data to your server as events occur.
Bugbop webhooks can notify your application when:
These events can trigger your systems to perform actions like:
Bugbop signs all webhook events using your webhook's secret key. Each webhook has its own unique secret, allowing you to verify that events were sent by Bugbop, not a third party.
When you create a webhook in the Bugbop dashboard, we automatically generate a secure secret key. You should use this secret to validate webhook payloads before processing them.
To set up a webhook, follow these steps:
After creating the webhook, you'll see the secret key. Make sure to note this down as it will be needed to verify webhook payloads.
Your webhook endpoint should:
To verify that a webhook request came from Bugbop and not a third party, you should validate the signature using your webhook secret.
Bugbop signs webhook payloads by including a Bugbop-Signature
header with each request. This
header contains the timestamp and signatures:
Bugbop-Signature: t=1617022962,signature=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
Here's how to verify the signature in different languages:
# Ruby example (Rails)
def verify_webhook_signature
payload = request.body.read
signature_header = request.headers['Bugbop-Signature']
webhook_secret = 'whsec_...' # Get from your webhook settings
# Parse header to extract timestamp and signature
header_parts = {}
signature_header.split(',').each do |part|
key, value = part.split('=')
header_parts[key] = value if key.present? && value.present?
end
timestamp = header_parts['t']
signature = header_parts['signature']
# Skip verification if missing timestamp or signature
return false unless timestamp && signature
# Verify timestamp is not too old
now = Time.now.to_i
if now - timestamp.to_i > 300
return false # Timestamp is older than 5 minutes
end
# Compute expected signature
signed_payload = "#{timestamp}.#{payload}"
expected_signature = OpenSSL::HMAC.hexdigest('sha256', webhook_secret, signed_payload)
# Use secure_compare to prevent timing attacks
ActiveSupport::SecurityUtils.secure_compare(expected_signature, signature)
end
# Python example (Flask)
import hmac
import hashlib
import time
from flask import request, jsonify
def verify_webhook_signature():
payload = request.data
signature_header = request.headers.get('Bugbop-Signature')
webhook_secret = 'whsec_...' # Get from your webhook settings
if not signature_header:
return False
# Parse header
header_parts = dict(part.split('=') for part in signature_header.split(',') if '=' in part)
timestamp = header_parts.get('t')
signature = header_parts.get('signature')
if not timestamp or not signature:
return False
# Verify timestamp is not too old
now = int(time.time())
if now - int(timestamp) > 300:
return False # Timestamp is older than 5 minutes
# Convert webhook secret to bytes
webhook_secret_bytes = webhook_secret.encode('utf-8')
# Compute expected signature
signed_payload = f"{timestamp}.{payload.decode('utf-8')}".encode('utf-8')
expected_signature = hmac.new(webhook_secret_bytes, signed_payload, hashlib.sha256).hexdigest()
# Use constant-time comparison to prevent timing attacks
return hmac.compare_digest(expected_signature, signature)
// Node.js example (Express)
const crypto = require('crypto');
const express = require('express');
const app = express();
app.use(express.json({
verify: (req, res, buf) => {
req.rawBody = buf;
}
}));
app.post('/webhooks/bugbop', (req, res) => {
const isValidSignature = verifyWebhookSignature(
req.rawBody,
req.headers['bugbop-signature'],
'whsec_...' // Get from your webhook settings
);
if (!isValidSignature) {
return res.status(400).send('Invalid signature');
}
// Process the webhook
const event = req.body;
console.log('Received event:', event.type);
res.sendStatus(200);
});
function verifyWebhookSignature(payload, signatureHeader, secret) {
if (!signatureHeader) return false;
// Parse the header
const parts = {};
signatureHeader.split(',').forEach(part => {
const [key, value] = part.split('=');
if (key && value) parts[key] = value;
});
const timestamp = parts['t'];
const signature = parts['signature'];
if (!timestamp || !signature) return false;
// Check if timestamp is too old
const now = Math.floor(Date.now() / 1000);
if (now - parseInt(timestamp, 10) > 300) {
return false; // Timestamp is older than 5 minutes
}
// Compute expected signature
const signedPayload = `${timestamp}.${payload}`;
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
// Constant-time comparison to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(expectedSignature, 'hex'),
Buffer.from(signature, 'hex')
);
}
<?php
function verifyWebhookSignature(string $webhookSecret): bool
{
// Get raw POST data
$payload = file_get_contents('php://input');
$signatureHeader = $_SERVER['HTTP_BUGBOP_SIGNATURE'] ?? '';
if (empty($signatureHeader)) {
return false;
}
// Parse header
$headerParts = [];
foreach (explode(',', $signatureHeader) as $part) {
if (strpos($part, '=') !== false) {
[$key, $value] = explode('=', $part, 2);
$headerParts[$key] = $value;
}
}
$timestamp = $headerParts['t'] ?? '';
$signature = $headerParts['signature'] ?? '';
if (!$timestamp || !$signature) {
return false;
}
// Verify timestamp is not too old
$now = time();
if ($now - (int)$timestamp > 300) {
return false; // Timestamp is older than 5 minutes
}
// Compute expected signature
$signedPayload = $timestamp . '.' . $payload;
$expectedSignature = hash_hmac('sha256', $signedPayload, $webhookSecret);
// Use hash_equals for constant-time comparison to prevent timing attacks
return hash_equals($expectedSignature, $signature);
}
?>
After verifying the webhook signature, you can process the event payload. Here are example webhook payloads for different event types:
{
"id": "evt_1a2b3c4d5e6f",
"event_type": "report.created",
"timestamp": 1617022962,
"data": {
"object": {
"id": "6f5e4d3c2b1a",
"title": "XSS vulnerability in login form",
"status": "new",
"severity": "high",
"program": {
"id": "prog_1a2b3c4d5e6f",
"name": "Web Security Program",
"visibility": "invite_only",
"url": "https://bugbop.com/programs/prog_1a2b3c4d5e6f"
},
"url": "https://bugbop.com/programs/prog_1a2b3c4d5e6f/reports/6f5e4d3c2b1a",
"user": {
"id": "user_1a2b3c4d5e6f",
"username": "security_researcher",
"profile_url": "https://bugbop.com/users/security_researcher"
},
"created_at": "2025-04-03T09:32:53+11:00",
"updated_at": "2025-04-03T09:32:53+11:00",
"description": "<div class=\"trix-content\"><h1>XSS Vulnerability</h1><p>I discovered an XSS vulnerability in your login form.</p><p><strong>Steps:</strong></p><ol><li>Go to login page</li><li>Enter script tag</li><li>Submit form</li></ol></div>",
"attachments": [
{
"id": "att_1a2b3c4d5e",
"filename": "proof_of_concept.png",
"content_type": "image/png",
"file_size": 256000
}
]
}
}
}
{
"id": "evt_2b3c4d5e6f7g",
"event_type": "comment.created",
"timestamp": 1617023962,
"data": {
"object": {
"id": "com_3c4d5e6f7g8h",
"report_id": "6f5e4d3c2b1a",
"url": "https://bugbop.com/programs/prog_1a2b3c4d5e6f/reports/6f5e4d3c2b1a#comment-com_3c4d5e6f7g8h",
"content": "<div class=\"trix-content\"><p>Thank you for your report!</p><p><strong>Next steps:</strong></p><ul><li>Patch in development</li><li>Deploy by Friday</li></ul></div>",
"is_internal": false,
"created_at": "2025-04-03T09:45:53+11:00",
"updated_at": "2025-04-03T09:45:53+11:00",
"attachments": [],
"user": {
"id": "user_2b3c4d5e6f7g",
"username": "program_admin",
"profile_url": "https://bugbop.com/users/program_admin"
},
"report": {
"id": "6f5e4d3c2b1a",
"title": "XSS vulnerability in login form",
"url": "https://bugbop.com/programs/prog_1a2b3c4d5e6f/reports/6f5e4d3c2b1a"
},
"program": {
"id": "prog_1a2b3c4d5e6f",
"url": "https://bugbop.com/programs/prog_1a2b3c4d5e6f"
}
}
}
}
{
"id": "evt_3d4e5f6g7h8i",
"event_type": "program_access_request.created",
"timestamp": 1617024962,
"data": {
"object": {
"id": "par_4e5f6g7h8i9j",
"status": "pending",
"message": "<div class=\"trix-content\"><p>I'm a security researcher with experience in authentication systems.</p><p><strong>Experience:</strong></p><ul><li>5+ years security testing</li><li>OAuth specialist</li></ul></div>",
"url": "https://bugbop.com/programs/prog_1a2b3c4d5e6f/access_requests",
"program": {
"id": "prog_1a2b3c4d5e6f",
"name": "Web Security Program",
"visibility": "invite_only",
"url": "https://bugbop.com/programs/prog_1a2b3c4d5e6f"
},
"user": {
"id": "user_3d4e5f6g7h8i",
"username": "security_researcher",
"profile_url": "https://bugbop.com/users/security_researcher"
},
"created_at": "2025-04-03T10:02:53+11:00",
"updated_at": "2025-04-03T10:02:53+11:00"
}
}
}
Your webhook handler should:
Here's a basic example of a webhook handler:
# Ruby example (Rails)
class WebhooksController < ApplicationController
skip_before_action :verify_authenticity_token
def handle
# Read payload and get signature
payload = request.body.read
signature_header = request.headers['Bugbop-Signature']
# Verify signature
unless verify_webhook_signature
return render json: { error: 'Invalid signature' }, status: :unauthorized
end
# Parse the event
event = JSON.parse(payload)
event_type = event['event_type']
event_id = event['id']
# Check if we've processed this event before (idempotency)
if ProcessedEvent.exists?(event_id: event_id)
return render json: { message: 'Event already processed' }, status: :ok
end
# Process the event based on type
case event_type
when 'report.created'
# Handle new report
ReportProcessor.process_new_report(event['data']['object'])
when 'comment.created'
# Handle new comment
CommentProcessor.process_new_comment(event['data']['object'])
# Add more event handlers here
end
# Mark event as processed
ProcessedEvent.create!(event_id: event_id)
# Return success
render json: { message: 'Event processed successfully' }, status: :ok
end
end
Bugbop automatically retries webhook deliveries that fail. We use an industry-standard exponential backoff strategy with jitter:
This retry strategy is designed to be reliable while preventing excessive load during outages. You can view webhook delivery statuses and retry history in your program's webhook dashboard.
You can test your webhook implementation in several ways:
In your webhook dashboard, click the "Test" button to send a webhook.ping
event to your
endpoint. This event contains minimal data but lets you verify your signature verification code.
Create test reports, comments, etc., in your program to trigger real webhook events.
Tools like webhook.site or ngrok can help you develop and debug webhook handlers locally.
Bugbop sends the following webhook events:
Event Type | Description |
---|---|
report.created |
Triggered when a new report is created |
report.updated |
Triggered when a report is updated |
report.deleted |
Triggered when a report is deleted |
comment.created |
Triggered when a comment is created |
comment.updated |
Triggered when a comment is updated |
comment.deleted |
Triggered when a comment is deleted |
program_invite.created |
Triggered when a program invite is created |
program_invite.updated |
Triggered when a program invite is updated |
program_invite.deleted |
Triggered when a program invite is deleted |
program_access_request.created |
Triggered when an access request is created |
program_access_request.updated |
Triggered when an access request is updated |
program_access_request.deleted |
Triggered when an access request is deleted |
webhook.ping |
Triggered manually for testing webhooks |
Each event type includes different data in the data.object
property. Here are the structures
for each object type:
{
"id": "string",
"title": "string",
"description": "string (HTML)",
"status": "new|open|needs_info|fixed|wont_fix|duplicate|not_applicable",
"severity": "low|medium|high|critical",
"url": "string (URL to view the report)",
"program": {
"id": "string",
"name": "string",
"url": "string (URL to view the program)"
},
"user": {
"id": "string",
"username": "string",
"profile_url": "string (URL to view the user's profile)"
},
"duplicate_of_id": "string|null",
"created_at": "string (ISO 8601 datetime)",
"updated_at": "string (ISO 8601 datetime)"
}
{
"id": "string",
"report_id": "string",
"url": "string (URL to view the comment)",
"content": "string (HTML)",
"is_internal": "boolean",
"metadata": "object",
"created_at": "string (ISO 8601 datetime)",
"updated_at": "string (ISO 8601 datetime)",
"program": {
"id": "string",
"name": "string",
"url": "string (URL to view the program)"
},
"user": {
"id": "string",
"username": "string",
"profile_url": "string (URL to view the user's profile)"
},
"report": {
"id": "string",
"title": "string",
"url": "string (URL to view the report)"
}
}
{
"id": "string",
"email": "string",
"role": "string",
"url": "string (URL to view program invites)",
"program": {
"id": "string",
"name": "string",
"url": "string (URL to view the program)"
},
"user": {
"id": "string",
"username": "string",
"profile_url": "string (URL to view the user's profile)"
},
"expires_at": "string (ISO 8601 datetime)",
"accepted_at": "string (ISO 8601 datetime)|null",
"created_at": "string (ISO 8601 datetime)",
"updated_at": "string (ISO 8601 datetime)"
}
{
"id": "string",
"status": "pending|approved|rejected",
"message": "string",
"rejection_reason": "string|null",
"acceptance_message": "string|null",
"url": "string (URL to view access requests)",
"program": {
"id": "string",
"name": "string",
"url": "string (URL to view the program)"
},
"user": {
"id": "string",
"username": "string",
"profile_url": "string (URL to view the user's profile)"
},
"created_at": "string (ISO 8601 datetime)",
"updated_at": "string (ISO 8601 datetime)"
}