Webhook Documentation

Learn how to receive real-time updates from Bugbop

Introduction

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:

  • Reports are created, updated, or deleted
  • Comments are created, updated, or deleted
  • Program invites are sent, updated, or deleted
  • Access requests are created, updated, or deleted

These events can trigger your systems to perform actions like:

  • Updating your internal systems
  • Notifying your team of new reports or comments
  • Triggering automated workflows
  • Logging events for auditing purposes

Authentication

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.

Webhook secrets should be kept secure. Never expose them in client-side code or public repositories.

Setting Up Webhooks

To set up a webhook, follow these steps:

  1. Go to your Program's dashboard
  2. Open the "Settings" dropdown and choose "Webhooks"
  3. Click "Create Webhook"
  4. Enter the URL where Bugbop should send webhook events
  5. Select the events you want to receive
  6. Save your webhook

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:

  • Be a publicly accessible HTTPS URL
  • Respond with a 2xx status code promptly (within 10 seconds)
  • Handle retries gracefully (using the idempotency key)

Verifying Webhooks

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
                

Handling Events

After verifying the webhook signature, you can process the event payload. Here are example webhook payloads for different event types:

Report Created Event

{
  "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
        }
      ]
    }
  }
}

Comment Created Event

{
  "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"
      }
    }
  }
}

Program Access Request Event

{
  "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:

  1. Verify the signature (as shown above)
  2. Respond with a 2xx status code as quickly as possible
  3. Handle events idempotently (using the event ID to prevent duplicate processing)
  4. Process the event asynchronously if it requires substantial work

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

Retrying Webhooks

Bugbop automatically retries webhook deliveries that fail. We use an industry-standard exponential backoff strategy with jitter:

  • Starting with 15 seconds after the initial failure
  • Each subsequent retry doubles the delay (30s, 60s, 120s, etc.)
  • A random jitter of ±15% is applied to prevent thundering herd problems
  • The maximum retry delay is capped at 12 hours
  • We'll retry up to 25 times over approximately 21 days

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.

To prevent retries overwhelming your systems during outages, we recommend implementing exponential backoff in your webhook handling code as well.

Testing Webhooks

You can test your webhook implementation in several ways:

1. Using the "Test" button

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.

2. Creating test events

Create test reports, comments, etc., in your program to trigger real webhook events.

3. Using webhook development tools

Tools like webhook.site or ngrok can help you develop and debug webhook handlers locally.

Events Reference

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

Payload Objects

Each event type includes different data in the data.object property. Here are the structures for each object type:

Report Object

{
  "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)"
}

Comment Object

{
  "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)"
  }
}

Program Invite Object

{
  "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)"
}

Program Access Request Object

{
  "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)"
}