Agent skill
BugSmash Webhooks Skill
Receive and verify BugSmash webhooks. Use when setting up BugSmash webhook handlers, debugging X-BugSmash-Signature verification (HMAC-SHA256 over X-Webhook-Timestamp + "." + raw body), storing the one-time bgsmsh_ secret_key, or handling new_comment, comment_replied, status_changed and comment_updated events.
Install this skill
npx skills add hookdeck/webhook-skills --skill bugsmash-webhooks
BugSmash (bugsmash.io) is a visual feedback and review tool for websites, images, video and documents. Workspace webhooks are created only through the API (POST https://api.bugsmash.io/api/v2/webhook) and every delivery is signed with HMAC-SHA256 using the webhook's secret_key.
When to Use This Skill
- How do I receive BugSmash webhooks in Express, Next.js, or FastAPI?
- How do I verify the
X-BugSmash-Signatureheader? - Why does my BugSmash signature verification fail?
- How do I handle
new_comment,comment_replied,status_changedorcomment_updatedevents? - Where do I get the BugSmash webhook secret?
Signature at a Glance
| Property | Value |
|---|---|
| Signature header | X-BugSmash-Signature: bare lowercase hex digest, no prefix |
| Timestamp header | X-Webhook-Timestamp (generic name, Unix seconds) |
| Algorithm | HMAC-SHA256, hex-encoded |
| Signed content | {X-Webhook-Timestamp}.{raw request body} |
| Key | The secret_key as UTF-8 bytes, used whole including bgsmsh_ (not decoded) |
| Replay window | None by default; opt in with BUGSMASH_ |
BugSmash returns the secret_key only once, when you create the webhook through the API. Paste the whole value, including the bgsmsh_ prefix.
Verification (core)
Build the signed message as bytes: the timestamp header string as latin1, a literal ., then the raw body bytes exactly as received. Never JSON.parse/JSON.stringify first (BugSmash's own Node sample signs JSON.stringify(payload), but its page also says to sign the raw body).
const crypto = require('crypto');
function verifyBugsmash(rawBody /* Buffer */, signature, timestamp, secret) {
if (!secret || !signature || !timestamp) return false;
if (!/^[0-9a-fA-F]{64}$/.test(signature)) return false; // validate before decoding
const signed = Buffer.concat([Buffer.from(timestamp, 'latin1'), Buffer.from('.'), rawBody]);
const expected = crypto.createHmac('sha256', Buffer.from(secret, 'utf8')).update(signed).digest();
return crypto.timingSafeEqual(expected, Buffer.from(signature, 'hex')); // hex is case-insensitive
}
import hashlib, hmac, re
def verify_bugsmash(raw_body: bytes, signature: str, timestamp: str, secret: str) -> bool:
if not (secret and signature and timestamp) or not re.fullmatch(r"[0-9a-fA-F]{64}", signature):
return False
signed = b"".join([timestamp.encode("latin-1"), b".", raw_body])
expected = hmac.new(secret.encode("utf-8"), signed, hashlib.sha256).digest()
return hmac.compare_digest(expected, bytes.fromhex(signature))
Missing or empty headers, a malformed signature or a mismatch all return 401. A missing secret returns 500 (fail closed).
Timestamp window: off by default. Hookdeck retries and replays resend the original headers and timestamp, and Hookdeck applies no tolerance, so a fixed window would reject legitimate retries. BugSmash's docs suggest 5 minutes; the examples offer it as opt-in: set BUGSMASH_ (unset or 0 = off). The check runs after the signature check.
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
HTTP Methods
BugSmash's docs don't name the HTTP method of a delivery. Hookdeck's BugSmash source pre-selects POST without managing the method (you can change it), so any other method the sender uses must also be allowed on the source. The examples accept GET, POST, PUT, PATCH and DELETE, verify the signature first for every method, and treat a missing body as empty bytes. Read the body raw: express.raw({ type: '*/*' }) (Express), await request.arrayBuffer() (Next.js), await request.body() (FastAPI).
Common Event Types
Subscribe with the events array on POST /webhook. These are the only values the docs allow:
| Event | Triggered When |
|---|---|
new_comment | A comment is added |
comment_replied | A reply is posted on a comment |
status_changed | A comment's status changes |
comment_updated | A comment is edited |
The docs document no delivery body, so which field carries the event name is unknown. The examples read event then type from a clearly labelled illustrative payload; check a real delivery (POST /webhook/test) and adjust. See references/overview.md.
Environment Variables
BUGSMASH_WEBHOOK_SECRET=bgsmsh_xxxxx # secret_key from the create-webhook response (shown once)
BUGSMASH_WEBHOOK_TOLERANCE_SECONDS=0 # optional replay window; unset or 0 = off
Local Development
Receive webhooks locally with the Hookdeck CLI, no account required:
npx hookdeck-cli listen 3000 bugsmash --path /webhooks/bugsmash
Use the printed URL as webhookUrl when you create the webhook (use port 8000 for FastAPI).
Reference Materials
- references/overview.md - What BugSmash webhooks are, events, illustrative payload
- references/setup.md - Create the webhook through the API, store the secret, Hookdeck source note
- references/verification.md - Signature details, byte-exact signing, replay window, debugging