docs
Dashboard

Webhook Payloads

Receive real-time notifications when issues are created, resolved, reopened, or receive new events.

Looking for setup instructions? See the Integrations page for how to connect Discord, Slack, or custom webhook endpoints. This page covers payload formats and signature verification.

Verifying signatures

Every webhook request includes an HMAC-SHA256 signature so your server can verify that the request genuinely came from booboo.dev and wasn't tampered with. Always verify signatures in production.

Signature headers

HeaderDescription
X-Booboo-SignatureHMAC-SHA256 hex digest of the raw request body, using your webhook secret as the key
X-Booboo-TimestampUnix timestamp (seconds) when the request was sent
X-Booboo-EventEvent type: issue_created, issue_resolved, issue_reopened, event_received, or test

Verification steps

  1. Read the raw request body as bytes (do not parse JSON first)
  2. Compute HMAC-SHA256(body, your_webhook_secret)
  3. Compare the result to the X-Booboo-Signature header using a constant-time comparison
  4. If they don't match, reject the request with a 401

Python

import hashlib
import hmac
import json

def verify_webhook(body: bytes, secret: str, signature: str) -> bool:
    expected = hmac.new(
        secret.encode(),
        body,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, signature)

Node.js

import crypto from "node:crypto";

function verifyWebhook(body, secret, signature) {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(body)
    .digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signature),
  );
}

Event types

All issue event payloads share the same structure. The event_type field indicates which event triggered the webhook.

FieldDescription
event_typeOne of the event types listed below
issue.idDisplay ID in PREFIX-NUMBER format (e.g. BACK-42)
issue.titleException type and message
issue.culpritSource location of the error (file and function)
issue.statusCurrent issue status: open, resolved, or ignored
issue.first_seenISO 8601 timestamp of the first occurrence
issue.last_seenISO 8601 timestamp of the most recent occurrence
issue.urlDirect link to the issue in the booboo.dev dashboard

issue_created

Sent when a new issue is detected (first occurrence of a unique error fingerprint).

{
  "event_type": "issue_created",
  "issue": {
    "id": "BACK-42",
    "title": "ZeroDivisionError: division by zero",
    "culprit": "app/utils.py in safe_divide",
    "status": "open",
    "first_seen": "2026-02-18T12:34:56.789Z",
    "last_seen": "2026-02-18T12:34:56.789Z",
    "url": "https://app.booboo.dev/my-org/projects/backend/issues/42"
  },
  "project": {
    "name": "Backend",
    "slug": "backend"
  },
  "organization": {
    "name": "My Org",
    "slug": "my-org"
  }
}

issue_resolved

Sent when a user marks an issue as resolved in the dashboard.

{
  "event_type": "issue_resolved",
  "issue": {
    "id": "BACK-42",
    "title": "ZeroDivisionError: division by zero",
    "culprit": "app/utils.py in safe_divide",
    "status": "resolved",
    "first_seen": "2026-02-18T12:34:56.789Z",
    "last_seen": "2026-02-18T14:20:00.000Z",
    "url": "https://app.booboo.dev/my-org/projects/backend/issues/42"
  },
  "project": {
    "name": "Backend",
    "slug": "backend"
  },
  "organization": {
    "name": "My Org",
    "slug": "my-org"
  }
}

issue_reopened

Sent when a resolved issue receives a new event (regression detected) or is manually reopened by a user.

{
  "event_type": "issue_reopened",
  "issue": {
    "id": "BACK-42",
    "title": "ZeroDivisionError: division by zero",
    "culprit": "app/utils.py in safe_divide",
    "status": "open",
    "first_seen": "2026-02-18T12:34:56.789Z",
    "last_seen": "2026-02-19T09:15:30.000Z",
    "url": "https://app.booboo.dev/my-org/projects/backend/issues/42"
  },
  "project": {
    "name": "Backend",
    "slug": "backend"
  },
  "organization": {
    "name": "My Org",
    "slug": "my-org"
  }
}

event_received

Sent when a new event arrives for an existing open or ignored issue. Throttled to at most 1 notification per 5 minutes per issue per routing rule to avoid flooding.

{
  "event_type": "event_received",
  "issue": {
    "id": "BACK-42",
    "title": "ZeroDivisionError: division by zero",
    "culprit": "app/utils.py in safe_divide",
    "status": "open",
    "first_seen": "2026-02-18T12:34:56.789Z",
    "last_seen": "2026-02-18T15:45:12.000Z",
    "url": "https://app.booboo.dev/my-org/projects/backend/issues/42"
  },
  "project": {
    "name": "Backend",
    "slug": "backend"
  },
  "organization": {
    "name": "My Org",
    "slug": "my-org"
  }
}

test

Sent when you click the Test button in organization settings. Use this to verify your endpoint is reachable.

{
  "event_type": "test",
  "message": "This is a test webhook from booboo.dev",
  "integration": {
    "id": "a1b2c3d4-...",
    "name": "Slack #errors"
  }
}

Full examples

Express (Node.js)

app.post("/webhooks/booboo", express.raw({ type: "application/json" }), (req, res) => {
  const signature = req.headers["x-booboo-signature"];
  if (!verifyWebhook(req.body, process.env.BOOBOO_WEBHOOK_SECRET, signature)) {
    return res.status(401).send("Invalid signature");
  }

  const event = JSON.parse(req.body);

  switch (event.event_type) {
    case "issue_created":
      console.log("New issue:", event.issue.id, event.issue.title);
      break;
    case "issue_resolved":
      console.log("Resolved:", event.issue.id);
      break;
    case "issue_reopened":
      console.log("Reopened:", event.issue.id);
      break;
    case "event_received":
      console.log("New event for:", event.issue.id);
      break;
  }

  res.sendStatus(200);
});

Flask (Python)

@app.route("/webhooks/booboo", methods=["POST"])
def booboo_webhook():
    signature = request.headers.get("X-Booboo-Signature", "")
    if not verify_webhook(request.data, WEBHOOK_SECRET, signature):
        return "Invalid signature", 401

    event = request.get_json()

    issue = event.get("issue", {})
    match event["event_type"]:
        case "issue_created":
            print(f"New issue: {issue['id']} {issue['title']}")
        case "issue_resolved":
            print(f"Resolved: {issue['id']}")
        case "issue_reopened":
            print(f"Reopened: {issue['id']}")
        case "event_received":
            print(f"New event for: {issue['id']}")

    return "", 200

Delivery behavior

  • Webhooks are sent with a 10-second timeout
  • Your endpoint must return a 2xx status code — any 4xx or 5xx is treated as a failure
  • Redirects are not followed to prevent SSRF attacks
  • In production, webhook URLs must use HTTPS and must not resolve to private/internal IP addresses
  • There are currently no automatic retries — if delivery fails, the webhook is dropped

Security notes

  • Your webhook secret is shown once at creation time. Store it securely (e.g. in environment variables)
  • If your secret is compromised, delete the integration and create a new one
  • Always verify the X-Booboo-Signature header before processing webhook payloads
  • Consider checking the X-Booboo-Timestamp header to reject stale requests (e.g. older than 5 minutes) to prevent replay attacks