Webhooks

Receive real-time notifications when events occur in your projects.

Overview

Webhooks allow you to receive real-time HTTP notifications when events occur in your BugToMe projects. Instead of polling the API for changes, webhooks push data to your server as events happen.

Supported Events

BugToMe supports the following webhook events:

Event Description
ticket_created A new ticket has been created
ticket_status_changed A ticket's status has changed (e.g., open → in_progress)
ticket_assigned A ticket has been assigned to a team member
ticket_priority_changed A ticket's priority has been updated
ticket_commented A comment has been added to a ticket
ticket_closed A ticket has been closed

Webhook Payload

All webhook payloads follow a consistent structure:

{
  "event": "ticket_created",
  "timestamp": "2024-01-15T10:30:00Z",
  "data": {
    "ticket": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "code": "PROJ-42",
      "title": "Login button not working",
      "status": "open",
      "priority": "high",
      "given": "User is on the login page",
      "when": "User clicks the login button",
      "then": "Nothing happens",
      "created_at": "2024-01-15T10:30:00Z",
      "updated_at": "2024-01-15T10:30:00Z",
      "url": "https://bugto.me/workspace/projects/proj-id/tickets/ticket-id"
    },
    "actor": {
      "id": "actor-uuid",
      "email": "user@example.com",
      "name": "John Doe"
    },
    "organization": {
      "id": "org-uuid",
      "name": "Acme Corp",
      "slug": "acme-corp"
    },
    "project": {
      "id": "project-uuid",
      "name": "Web App",
      "code": "PROJ",
      "slug": "web-app"
    },
    "changes": {
      // Event-specific data
    }
  }
}

Event-Specific Changes

The changes object contains event-specific data:

ticket_status_changed

"changes": {
  "old_status": "open",
  "new_status": "in_progress"
}

ticket_assigned

"changes": {
  "assignee": {
    "id": "assignee-uuid",
    "email": "dev@example.com",
    "name": "Jane Smith"
  }
}

ticket_commented

"changes": {
  "comment": {
    "id": "comment-uuid",
    "content": "I can reproduce this issue...",
    "created_at": "2024-01-15T11:00:00Z"
  }
}

HTTP Headers

Every webhook request includes the following headers:

Header Description
X-BugToMe-Event The event type (e.g., ticket_created)
X-BugToMe-Delivery-ID Unique ID for this delivery (for idempotency)
X-BugToMe-Signature HMAC-SHA256 signature for payload verification
X-BugToMe-Timestamp Unix timestamp of when the webhook was sent
Content-Type application/json

Verifying Webhook Signatures

Every webhook request is signed using HMAC-SHA256. You should always verify the signature to ensure the request came from BugToMe and wasn't tampered with.

The signature is computed as:

HMAC-SHA256(webhook_secret, timestamp + "." + payload)

The signature header format is: sha256=<hex_signature>

Ruby Example

require 'openssl'

def verify_webhook(request, secret)
  signature = request.headers['X-BugToMe-Signature']
  timestamp = request.headers['X-BugToMe-Timestamp']
  payload = request.body.read

  return false unless signature && timestamp

  expected = "sha256=" + OpenSSL::HMAC.hexdigest(
    'sha256',
    secret,
    "#{timestamp}.#{payload}"
  )

  ActiveSupport::SecurityUtils.secure_compare(expected, signature)
end

Node.js Example

const crypto = require('crypto');

function verifyWebhook(req, secret) {
  const signature = req.headers['x-bugtome-signature'];
  const timestamp = req.headers['x-bugtome-timestamp'];
  const payload = JSON.stringify(req.body);

  if (!signature || !timestamp) return false;

  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}.${payload}`)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signature)
  );
}

Python Example

import hmac
import hashlib

def verify_webhook(request, secret):
    signature = request.headers.get('X-BugToMe-Signature')
    timestamp = request.headers.get('X-BugToMe-Timestamp')
    payload = request.data.decode('utf-8')

    if not signature or not timestamp:
        return False

    expected = 'sha256=' + hmac.new(
        secret.encode('utf-8'),
        f'{timestamp}.{payload}'.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(expected, signature)

Retry Policy

If your endpoint doesn't respond with a 2xx status code, BugToMe will retry the delivery. The default retry schedule is:

  • Attempt 1: Immediate
  • Attempt 2: After 1 minute
  • Attempt 3: After 5 minutes
  • Attempt 4: After 15 minutes
  • Attempt 5: After 1 hour
  • Attempt 6: After 4 hours (final attempt)

After all retries are exhausted, the delivery is marked as failed.

Best Practices

1. Respond Quickly

Your endpoint should respond within 30 seconds. Process webhooks asynchronously if you need to perform time-consuming operations.

# Good: Queue for async processing
post '/webhooks/bugtome' do
  verify_webhook!(request, ENV['WEBHOOK_SECRET'])
  WebhookProcessorJob.perform_async(params)
  status 200
end

2. Handle Duplicates

Use the X-BugToMe-Delivery-ID header to ensure idempotency. Store processed delivery IDs and skip duplicates.

3. Verify Signatures

Always verify the webhook signature before processing. Never trust unverified webhook data.

4. Use HTTPS

Only use HTTPS endpoints for webhooks. HTTP endpoints are not supported in production.

5. Handle Failures Gracefully

If your endpoint is temporarily unavailable, BugToMe will retry. Return a 5xx status code for temporary failures so retries are attempted.

Testing Webhooks

During development, you can use tools like ngrok or webhook.site to receive webhooks on your local machine.

# Start ngrok tunnel
ngrok http 3000

# Use the ngrok URL as your webhook endpoint
# https://abc123.ngrok.io/webhooks/bugtome