Agent skill
Featurebase Webhooks Skill
Receive and verify Featurebase webhooks (feedback, roadmap, changelog and customer support platform). Use when setting up a Featurebase webhook endpoint, getting the whsec_ signing secret, debugging X-Webhook-Signature and X-Webhook-Timestamp verification, or handling topics like post.created, post.voted, ticket.created, comment.created, changelog.published and conversation.admin.closed.
Install this skill
npx skills add hookdeck/webhook-skills --skill featurebase-webhooks
Featurebase is a feedback, roadmap, changelog and customer support platform. Its webhooks (Growth plan and higher) report Requests (post.*), tickets, comments, Updates (changelogs) and inbox conversations. Each delivery is a POST signed with a hex HMAC-SHA256 in X-Webhook-Signature, computed over {X-Webhook-Timestamp}.{raw body}.
This is not Standard Webhooks: nothing is base64-decoded, there is no id header and the digest is hex. Do not use the standardwebhooks or svix libraries; verify manually with the code below.
When to Use This Skill
- How do I receive Featurebase webhooks?
- How do I verify a Featurebase webhook signature (
X-Webhook-Signature)? - Where do I find my Featurebase webhook signing secret?
- How do I handle
post.created,comment.createdorchangelog.publishedevents? - Why is my Featurebase webhook signature verification failing?
- Should I reject old
X-Webhook-Timestampvalues?
Verification (core)
The signing secret starts with whsec_. Paste the whole secret, including the whsec_ prefix. Featurebase uses it as a plain HMAC key. Do not strip the prefix or base64-decode it.
const crypto = require('crypto');
function verifyFeaturebaseWebhook(rawBody, signature, timestamp, secret) {
// rawBody: Buffer of the exact bytes received. Validate the format before decoding.
if (!secret || !signature || !timestamp || !/^[0-9a-fA-F]{64}$/.test(signature)) return false;
const signed = Buffer.concat([Buffer.from(timestamp, 'latin1'), Buffer.from('.'), rawBody]);
const expected = crypto.createHmac('sha256', Buffer.from(secret, 'utf8')).update(signed).digest();
const provided = Buffer.from(signature.toLowerCase(), 'hex'); // hex is case-insensitive
return provided.length === expected.length && crypto.timingSafeEqual(provided, expected);
}
// Headers: x-webhook-signature, x-webhook-timestamp. Return 401 when this is false.
import hashlib, hmac, re
def verify_featurebase_webhook(raw_body: bytes, signature: str, timestamp: str, secret: str) -> bool:
if not secret or not signature or not timestamp or not re.fullmatch(r"[0-9a-fA-F]{64}", signature):
return False
signed = timestamp.encode("latin-1") + b"." + raw_body
expected = hmac.new(secret.encode("utf-8"), signed, hashlib.sha256).digest()
return hmac.compare_digest(bytes.fromhex(signature), expected) # bytes, never str
Rules that matter:
- Sign bytes: the timestamp header value as received (latin1), a literal
., then the raw body bytes. NeverJSON.parse/JSON.stringifyor decode and re-encode the body first. - Read the raw body:
express.raw({ type: '*/*' })in Express,await request.arrayBuffer()in Next.js,await request.body()in FastAPI. - Check the signature is exactly 64 hex characters before decoding. Compare the whole header value as one digest, with a constant-time comparison.
- Missing or empty headers, a malformed signature or a mismatch: respond
401. - Timestamp window is off by default. Hookdeck retries and replays resend the original headers and timestamp, and Hookdeck applies no tolerance itself, so a fixed window would reject legitimate retries. Featurebase's own sample rejects timestamps older than 5 minutes. If you receive directly and want that, opt in with
FEATUREBASE_(unset orWEBHOOK_ TOLERANCE_ SECONDS 0= off).X-Webhook-Timestampis Unix seconds.
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Events
Dispatch on the body's topic field; data.item holds the changed object and update events add data.changes. Common topics: post.created, post.updated, post.deleted, post.voted, ticket.created, ticket.updated, ticket.deleted, comment.created, comment.updated, comment.deleted, changelog.published, conversation.user.created, conversation.admin.assigned, conversation.admin.closed. The full list of 27 topics is in references/overview.md.
Delivery
Featurebase needs 200, 201, 202 or 204 within 5 seconds (the API can raise the timeout to 30 seconds); anything else is a failure. It retries up to 3 times, pauses a webhook after 100 consecutive failures and disables it after 500 failures or 3 pauses in 24 hours. Events are not kept while a webhook is paused or disabled. The event id (notif_...) is the same on every retry, so dedupe on it.
Environment Variables
FEATUREBASE_WEBHOOK_SECRET=whsec_your_signing_secret_here
# Optional, unset or 0 = off:
FEATUREBASE_WEBHOOK_TOLERANCE_SECONDS=300
Local Development
# Express / Next.js (use 8000 for FastAPI). No install or account required.
npx hookdeck-cli listen 3000 featurebase --path /webhooks/featurebase
Add the printed URL in Settings → Developers → Webhooks (see references/setup.md). Paste the whole secret, including the whsec_ prefix. Featurebase uses it as a plain HMAC key.
Reference Materials
- references/overview.md - All 27 topics, payload shape, delivery and retries
- references/setup.md - Dashboard and API setup, signing secret, rotation
- references/verification.md - Signature details, timestamp tolerance, gotchas, debugging