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.created or changelog.published events?
  • Why is my Featurebase webhook signature verification failing?
  • Should I reject old X-Webhook-Timestamp values?

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. Never JSON.parse/JSON.stringify or 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_WEBHOOK_TOLERANCE_SECONDS (unset or 0 = off). X-Webhook-Timestamp is 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


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 10, 2026

View on GitHub →