# Guide to BugSmash Webhooks: Features and Best Practices

BugSmash webhooks tell your systems when reviewers talk: a new comment lands on a design or a web page, someone replies, a comment is edited, or its status changes. If your team collects visual feedback in BugSmash, webhooks are how that activity reaches your issue tracker, chat tools or internal dashboards as it happens.

This guide covers how BugSmash webhooks are created and signed, the events you can subscribe to, what BugSmash leaves undocumented, and the practices that keep a BugSmash receiver reliable.

## What are BugSmash webhooks?

A BugSmash webhook subscribes a URL to one or more events in a workspace. There is no settings page for them: you create, list, update, delete and test webhooks through BugSmash's API with your API key.

Each delivery is signed. BugSmash computes an HMAC-SHA256 over the `X-Webhook-Timestamp` header, a dot and the raw JSON body, keyed with the webhook's `secret_key`, and sends the hex digest in `X-BugSmash-Signature`. The secret is returned once, in the response to the create call, and can't be fetched again.

## BugSmash webhook features

| Feature | Details |
| --- | --- |
| Configuration | API only: `https://api.bugsmash.io/api/v2`, authenticated with `X-API-Key` |
| Scope | Per workspace. The list endpoint also shows a `project_id` for project-scoped webhooks |
| Events | `new_comment`, `comment_replied`, `status_changed`, `comment_updated` |
| Signature header | `X-BugSmash-Signature`, a bare hex digest |
| Timestamp header | `X-Webhook-Timestamp`, Unix seconds |
| Scheme | HMAC-SHA256, hex |
| Signed content | `<X-Webhook-Timestamp>.<raw body>` |
| Secret | `secret_key`, prefixed `bgsmsh_`, returned once at creation |
| Testing | `POST /webhook/test` fires a sample event |
| Body format | JSON (`content-type: application/json`) |
| Method, retries, timeout, source IPs | Not documented |

## Common events

BugSmash allows four values in a webhook's `events` array. It doesn't describe them, so the descriptions below are read from the names:

| Event | Fires when |
| --- | --- |
| `new_comment` | A comment is added |
| `comment_replied` | Someone replies to a comment |
| `status_changed` | A comment's status changes |
| `comment_updated` | A comment is edited |

BugSmash doesn't publish a delivery body, and its list of delivery headers has no event header, so the docs don't say where a delivery names its event. Fire a test event for each type you subscribe to, log the body, and build your router against what arrives.

## Setting up BugSmash webhooks

Create the webhook with your API key and the events you want:

```bash
curl -X POST "https://api.bugsmash.io/api/v2/webhook" \
  -H "X-API-Key: $BUGSMASH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "events": ["new_comment", "comment_replied", "status_changed", "comment_updated"],
    "webhookUrl": "https://example.com/webhooks/bugsmash"
  }'

```

The response includes a `secret_key` such as `bgsmsh_...`, with a notice that it "is only shown once and cannot be retrieved again". Store the whole value straight away, for example as `BUGSMASH_WEBHOOK_SECRET`. If you lose it, the only fix is to delete the webhook and create a new one.

The other endpoints:

* `GET /webhooks` lists the workspace's webhooks with their events, URL, `is_active` and `last_triggered_at`.
* `PATCH /webhook/{webhookId}` changes the events or URL and keeps the existing secret.
* `DELETE /webhook/{webhookId}` removes the webhook, and its secret stops being valid.
* `POST /webhook/test` with `{"event": "comment_updated"}` sends a test event to every webhook in the workspace that subscribes to that event.

If you receive BugSmash webhooks through Hookdeck, create a source with the BugSmash type and use its URL as the `webhookUrl`. BugSmash returns the `secret_key` only once, when you create the webhook through the API, so paste the whole value into the source, including the `bgsmsh_` prefix. BugSmash's docs don't name the HTTP method it uses, so Hookdeck's BugSmash source pre-selects `POST` without fixing it. If your first test delivery arrives with a different method, add that method to the source's allowed methods.

## Securing BugSmash webhooks

Read `X-Webhook-Timestamp` and `X-BugSmash-Signature`. Build the signed bytes from the timestamp exactly as received, a literal `.`, and the raw request body, compute an HMAC-SHA256 of them with the whole `secret_key` as a UTF-8 key, and compare the hex digest with the header in constant time.

```javascript
const crypto = require('crypto');
const express = require('express');

function verifyBugsmashSignature(rawBody, signatureHeader, timestampHeader, secret, toleranceSeconds = 0) {
  if (!signatureHeader || !timestampHeader || !secret) return false;

  const signature = signatureHeader.trim();
  if (!/^[0-9a-fA-F]{64}$/.test(signature)) return false; // check before Buffer.from(x, 'hex')

  // Optional replay window, off by default. Retries and replays resend the original timestamp.
  if (toleranceSeconds > 0) {
    if (!/^\d+$/.test(timestampHeader)) return false;
    if (Math.abs(Date.now() / 1000 - Number(timestampHeader)) > toleranceSeconds) return false;
  }

  // The key is the whole secret_key, bgsmsh_ prefix included.
  const expected = crypto
    .createHmac('sha256', secret)
    .update(Buffer.concat([Buffer.from(`${timestampHeader}.`, 'utf8'), rawBody]))
    .digest();
  const received = Buffer.from(signature, 'hex');
  return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}

const app = express();

app.all('/webhooks/bugsmash', express.raw({ type: '*/*' }), (req, res) => {
  const secret = process.env.BUGSMASH_WEBHOOK_SECRET;
  if (!secret) return res.sendStatus(500); // fail closed

  const valid = verifyBugsmashSignature(
    req.body,
    req.get('x-bugsmash-signature'),
    req.get('x-webhook-timestamp'),
    secret
  );
  if (!valid) return res.sendStatus(401);

  const event = JSON.parse(req.body.toString('utf8'));
  // The body isn't documented: log it, then route once you know which field names the event.
  res.sendStatus(200);
});

```

The same check in Python:

```python
import hashlib
import hmac
import re
import time

def verify_bugsmash_signature(raw_body: bytes, signature_header, timestamp_header, secret: str,
                              tolerance_seconds: int = 0) -> bool:
    if not signature_header or not timestamp_header or not secret:
        return False
    signature = signature_header.strip()
    if not re.fullmatch(r"[0-9a-fA-F]{64}", signature):
        return False
    # Optional replay window, off by default.
    if tolerance_seconds > 0:
        if not (timestamp_header.isascii() and timestamp_header.isdigit()):
            return False
        if abs(time.time() - int(timestamp_header)) > tolerance_seconds:
            return False
    signed = timestamp_header.encode("utf-8") + b"." + raw_body
    expected = hmac.new(secret.encode("utf-8"), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(signature.lower().encode("utf-8"), expected.encode("utf-8"))

# In FastAPI: raw_body = await request.body()
# verify_bugsmash_signature(raw_body, request.headers.get("x-bugsmash-signature"),
#                           request.headers.get("x-webhook-timestamp"),
#                           os.environ["BUGSMASH_WEBHOOK_SECRET"])

```

BugSmash's own Node sample builds the signed string from `JSON.stringify(payload)` of the parsed body, and calls `crypto.timingSafeEqual` without checking lengths. The same page warns that if verification "fails unexpectedly, sign against the **raw** request body rather than a re-serialized object". Follow the warning. Deliveries come from a PHP HTTP client (the user agent is `GuzzleHttp/7`), and re-serialising the parsed JSON in JavaScript won't always reproduce the bytes it sent.

BugSmash also suggests ignoring "any request older than five minutes". The samples above leave that off unless you pass a tolerance. Hookdeck applies no timestamp tolerance, and its retries and replays resend the original `X-Webhook-Timestamp`, so a five-minute window in your own code would reject a legitimate retry that arrives later.

> **Make BugSmash webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) verifies the `X-BugSmash-Signature` header over the timestamp and raw body, stores every comment event, and delivers it to your service with retries you control.

## BugSmash webhook limitations and pain points

### The secret is returned once

**The Problem:** A new receiver needs the secret, and nobody saved it.

**Why It Happens:** BugSmash returns `secret_key` only in the create response. There is no endpoint to read it again, and no rotation call: you delete the webhook and create a new one.

**Workarounds:**

* Write the create call as a script that stores the secret in your secret manager in the same step.
* Use `PATCH` to change events or the URL, because it keeps the existing secret.

**How Hookdeck Can Help:** With Hookdeck in front, the secret is pasted once into the BugSmash source, and moving your own service to a new URL is a change to the Hookdeck destination rather than to the BugSmash webhook.

### Verification fails with BugSmash's own sample

**The Problem:** Signatures don't match even though the secret is right.

**Why It Happens:** The sample signs `JSON.stringify` of the parsed body rather than the raw bytes. A body framework that parses JSON before your handler, or any difference in escaping between PHP and JavaScript, changes the bytes.

**Workarounds:**

* Read the raw body (`express.raw`, `request.arrayBuffer()`, `await request.body()`) and sign that.
* Include the `bgsmsh_` prefix in the key.

**How Hookdeck Can Help:** Hookdeck's BugSmash source verifies against the raw body it received. Hookdeck answers BugSmash with a `200` either way, then records a request that fails the check as failed verification, so it never reaches your destination, and you can inspect its headers and body.

### No documented payload or method

**The Problem:** You can't write handlers from the docs, and you don't know which HTTP method to accept.

**Why It Happens:** BugSmash lists the delivery headers but shows no body and doesn't state the method.

**Workarounds:**

* Use `POST /webhook/test` for each subscribed event and record the method and body that arrive.
* Read the event name defensively and log anything you can't route.

**How Hookdeck Can Help:** Hookdeck stores every request with its method, headers and body, so the test deliveries are there to inspect before you write routing logic.

### Delivery guarantees are unstated

**The Problem:** You don't know whether a failed delivery is retried.

**Why It Happens:** BugSmash documents no retries, timeout or success rule. `GET /webhooks` shows `is_active` and `last_triggered_at`, but not why a webhook might be disabled.

**Workarounds:**

* Return `200` quickly and process asynchronously.
* Check `is_active` on a schedule and alert if a webhook turns inactive.

**How Hookdeck Can Help:** Hookdeck accepts each delivery as it arrives and stores it, then delivers to your service with [retries](/docs/retries) you configure.

## Best practices

### Verify against the raw body

The signature covers the exact bytes BugSmash sent. In Express, use `express.raw({ type: '*/*' })`. In Next.js, use `Buffer.from(await request.arrayBuffer())`. In FastAPI, use `await request.body()`.

### Use the test endpoint before going live

`POST /webhook/test` exercises the whole chain: delivery, signature and your handler. Run it once per event type, because it only reaches webhooks subscribed to that event.

### Make handlers idempotent

BugSmash doesn't document an event ID, so build a key from what the body contains, such as the comment ID, the event and its timestamp. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

### Acknowledge fast and process asynchronously

Verify, persist the event, return `200`, and do the work afterwards. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

## Conclusion

BugSmash webhooks are created through the API, and the `bgsmsh_` secret comes back once, so save it then. Verify `X-BugSmash-Signature` as a hex HMAC-SHA256 of `X-Webhook-Timestamp`, a dot and the raw body, not a re-serialised object, and use test events to learn the body and method before you write handlers.

[Hookdeck Event Gateway](https://hookdeck.com) sits in front of your receiver, verifies every BugSmash signature, and holds each comment event until your service has processed it.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle BugSmash webhooks reliably in minutes.