Skip to main content
API guides

Receive events with webhooks

Get an HTTP request when Requests, tickets, comments, Updates, or conversations change, and verify that each request comes from Featurebase.

Written By Robi Rohumaa

Last updated About 3 hours ago

Webhooks send an HTTP POST request to your server when something changes in Featurebase: for example, a new Request, a status change, or a reply in a conversation. Use them to keep your own tools in sync without polling the API.

πŸ’‘ Webhooks are available on paid plans (Growth and higher).

1

Create an endpoint

Add a route to your server that accepts POST requests with a JSON body. Your endpoint must:

  • Use a public https:// URL.

  • Return 200, 201, 202, or 204 within 5 seconds. Featurebase counts any other response as a failure.

  • Keep the raw request body. You need the exact bytes to verify the signature.

2

Register the webhook

  1. Open Settings β†’ Developers β†’ Webhooks in your Featurebase dashboard.

  2. Add your endpoint URL and select the topics you want to receive.

  3. Copy the signing secret. It starts with whsec_. Store it as an environment variable, for example FEATUREBASE_WEBHOOK_SECRET.

You can also create webhooks with the API. The API also lets you add custom headers and set a timeout of up to 30 seconds. A workspace can have up to 10 webhooks.

3

Verify the signature

Every request has two headers:

  • X-Webhook-Signature: an HMAC-SHA256 signature, as a hex string.

  • X-Webhook-Timestamp: the Unix time, in seconds, when Featurebase sent the request.

To verify a request:

  1. Join the timestamp and the raw body with a dot: {timestamp}.{rawBody}.

  2. Compute an HMAC-SHA256 of that string with your signing secret, as a hex string.

  3. Compare the result with X-Webhook-Signature. Use a constant-time comparison.

  4. Reject the request if the timestamp is more than 5 minutes old. This blocks replayed requests.

import crypto from "node:crypto";

const SECRET = process.env.FEATUREBASE_WEBHOOK_SECRET;

// rawBody: the request body exactly as received (Buffer or string)
export function verifyFeaturebaseWebhook(rawBody, headers) {
  const signature = headers["x-webhook-signature"];
  const timestamp = headers["x-webhook-timestamp"];
  if (!signature || !timestamp) return false;

  // Reject requests older than 5 minutes
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;

  const expected = crypto
    .createHmac("sha256", SECRET)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");

  const a = Buffer.from(expected);
  const b = Buffer.from(signature);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

πŸ”’ Verify the raw body before you parse it. If your framework parses the JSON and you serialize it again, the bytes can change and the signature will not match. Keep the signing secret on your server and never commit it to git.

4

Handle the event

Read topic to see what happened. data.item holds the object that changed.

import express from "express";
import { verifyFeaturebaseWebhook } from "./verify.js";

const app = express();

app.post(
  "/webhooks/featurebase",
  express.raw({ type: "application/json" }), // keep the raw body for the signature
  (req, res) => {
    if (!verifyFeaturebaseWebhook(req.body, req.headers)) {
      return res.sendStatus(401);
    }
    res.sendStatus(200); // respond first, then do the work

    const event = JSON.parse(req.body);
    handleEvent(event).catch(console.error);
  },
);

async function handleEvent(event) {
  // The same event can arrive more than once
  if (await alreadyProcessed(event.id)) return;

  switch (event.topic) {
    case "post.created":
      // event.data.item is the new post
      break;
    case "conversation.user.replied":
      // event.data.item is the new message
      break;
  }

  await markProcessed(event.id);
}
  • Respond first. Return a 2xx response before slow work, then process the event in the background.

  • Skip duplicates. The same event can arrive more than once. Store each event id and skip IDs you already handled.

  • Do not rely on order. Events can arrive out of order. Use createdAt, or get the latest state from the API.

Payload

Every event uses the same envelope. data.item uses the same format as the API. Payloads follow your workspace's default API version, which you set in Settings β†’ Developers β†’ API.

{
  "object": "notification_event",
  "id": "notif_0193a70c-3015-757c-a260-22ae37c86608",
  "topic": "post.updated",
  "organizationId": "6595518396205e06b897ad65",
  "webhookId": "675346db13af7340748ce850",
  "createdAt": "2026-10-09T12:00:00.000Z",
  "data": {
    "object": "notification_event_data",
    "item": {
      "object": "post",
      "id": "67546dfb6e1363426b90707f",
      "title": "Dark mode for the dashboard",
      "content": "<p>Please add a dark theme.</p>",
      "boardId": "6755d0970b5d5b1fefdf54f4",
      "postUrl": "https://feedback.example.com/p/dark-mode-for-the-dashboard",
      "createdAt": "2026-10-01T09:30:00.000Z",
      "updatedAt": "2026-10-09T12:00:00.000Z"
    },
    "changes": [
      {
        "field": "title",
        "oldValue": "Dark mode",
        "newValue": "Dark mode for the dashboard"
      }
    ]
  }
}
  • id: a unique event ID that starts with notif_. Use it to skip duplicates.

  • topic: the event type, for example post.updated.

  • organizationId and webhookId: your workspace and the webhook that sent the event.

  • createdAt: when the event happened.

  • data.item: the object that changed.

  • data.changes: for update events, a list of { field, oldValue, newValue } objects.

  • conversationId and conversationUrl: only on conversation events. The URL opens the conversation in your inbox.

Topics

Select the topics you need when you register the webhook.

Requests

  • post.created: a Request was created.

  • post.updated: a Request changed, for example its title, status, board, or tags. data.changes shows what changed.

  • post.deleted: a Request was deleted.

  • post.voted: someone added or removed an upvote. data.item is a post_vote with action (add or remove), postId, and voter.

Tickets

  • ticket.created: a ticket was created.

  • ticket.updated: a ticket changed.

  • ticket.deleted: a ticket was deleted.

Comments

  • comment.created: a comment was posted. Replies include parentCommentId.

  • comment.updated: a comment was edited.

  • comment.deleted: a comment was deleted.

Updates

  • changelog.published: an Update was published. The API calls Updates changelogs.

Conversations: data.item is the conversation, without its messages. To get the messages, get the conversation with the API. Topics that change the conversation include data.changes.

  • conversation.user.created: a customer started a conversation.

  • conversation.admin.assigned: the conversation was assigned to a teammate or team.

  • conversation.admin.closed: a teammate closed the conversation.

  • conversation.admin.opened: a teammate reopened the conversation.

  • conversation.admin.snoozed: a teammate snoozed the conversation.

  • conversation.admin.unsnoozed: the conversation was unsnoozed, by a teammate or when the snooze ended.

  • conversation.priority.updated: the priority changed.

  • conversation.handover_requested: the AI agent handed the conversation over to a human.

  • conversation.contact.attached: a contact was added to the conversation.

  • conversation.contact.detached: a contact was removed from the conversation.

  • conversation.read: the conversation was marked as read.

  • conversation.deleted: the conversation was deleted.

Messages: data.item is one message (a conversation part). Its partType tells you the kind of message. These topics never include data.changes.

  • conversation.user.replied: a customer replied.

  • conversation.admin.replied: a teammate or the AI agent replied.

  • conversation.admin.noted: a teammate added an internal note.

  • conversation_part.redacted: a teammate redacted a message.

Retries and failures

A delivery fails when your endpoint returns another status code, takes longer than the timeout, or cannot be reached. Featurebase then retries up to 3 times: 1 minute, 1 hour, and 6 hours after each failed attempt.

If your endpoint keeps failing, Featurebase stops sending to it:

  • Paused: after 100 failed deliveries in a row. The first pause lasts 5 minutes, and each new pause is twice as long, up to 4 hours. Deliveries start again on their own after the pause.

  • Disabled: after 500 failed deliveries in 24 hours, or after 3 pauses in 24 hours. Fix your endpoint, then enable the webhook again in Settings β†’ Developers β†’ Webhooks, or set its status to active with the API.

⚠️ Featurebase does not keep events while a webhook is paused or disabled. After you fix your endpoint, use the API to get anything you missed.

Rotate the signing secret

Rotate the secret if it leaks, or as a routine precaution. Open the webhook in Settings β†’ Developers β†’ Webhooks and click Rotate secret, or refresh the secret with the API.

πŸ”’ The old secret stops working at once. Update the secret on your server right after you rotate it, or your endpoint will reject new events.

Next steps

Was this helpful?

Still need help? Ask the team