Webhooks

When using Featurebase, you might want your applications to react instantly as users interact with your portal. With webhooks, your backend systems can receive and act on real-time updates such as new feedback posts.

Featurebase sends event data to your application's webhook endpoint as a JSON payload, using secure HTTPS. This data contains all the essential event details so you can act on it instantly.

Registering webhooks

To register a new webhook, you need to have a URL in your app that Featurebase can call. You can configure a new webhook from the Featurebase Dashboard -> Settings -> Webhooks. Give your webhook a name, pick the topics you want to listen for, and add your URL.

Your webhook endpoint must be a publicly accessible HTTPS URL in this format:

https://{your-website}/{your-webhook-endpoint}

For example, if your domain is https://yourawesomesite.com and your webhook endpoint route is /featurebase_webhooks, you would register:

https://yourawesomesite.com/featurebase_webhooks

You can register up to 10 webhook endpoints per organization. All endpoints must use HTTPS for security.

Now, whenever something of interest happens, a webhook is fired off by Featurebase. In the next section, we'll look at how to consume webhooks.

Consuming webhooks

When your app receives a webhook request from Featurebase, check the topic attribute to see what event caused it. The first part of the event type will tell you the payload type, e.g., a post, comment, etc.

Example webhook payload

{
  "id": "notif_0193a6e6-fb6b-78ef-b71f-15008d9f9cde",
  "topic": "post.updated",
  "data": {
    "type": "notification_event_data",
    "item": {
      "id": "6755c4afb12a37dffa6319b0",
      "title": "My new title",
      // ...
    },
    "changes": [
      {
        "field": "title",
        "oldValue": "My old title",
        "newValue": "My new title"
      }
    ]
  }
  // ...
}

In the example above, a post was updated, and the payload type is a notification_event_data. Notice how the changes array contains the old and new values for the changed fields.


Topic types

  • Name
    post.created
    Type
    Description

    A new post was created.

  • Name
    post.updated
    Type
    Description

    An existing post was updated.

  • Name
    post.deleted
    Type
    Description

    A post was successfully deleted.

  • Name
    post.voted
    Type
    Description

    A post received a vote.

  • Name
    changelog.published
    Type
    Description

    A new changelog entry was published.

  • Name
    comment.created
    Type
    Description

    A new comment was created.

  • Name
    comment.updated
    Type
    Description

    An existing comment was updated.

  • Name
    comment.deleted
    Type
    Description

    A comment was successfully deleted.

Example payload

{
  "type": "notification_event",
  "topic": "post.updated",
  "organizationId": "6595518396205e06b897ad65",
  "data": {
    "type": "notification_event_data",
    "item": {
      "id": "67546dfb6e1363426b90707f",
      "type": "post",
      "title": "New title",
      "content": "<p>New content</p>",
      // ...
    },
    "changes": [
      {
        "field": "title",
        "oldValue": "Old title",
        "newValue": "New title"
      },
      {
        "field": "content", 
        "oldValue": "<p>Old content</p>",
        "newValue": "<p>New content</p>"
      }
    ]
  },
  "id": "notif_0193a70c-3015-757c-a260-22ae37c86608",
  "webhookId": "675346db13af7340748ce850",
  "createdAt": "2024-12-08T16:13:34.102Z",
  "deliveryStatus": "pending"
}

Topic Examples

Here's a detailed look at the payload structure for each webhook topic.

Post Events

post.created

{
  "type": "notification_event",
  "topic": "post.created",
  "organizationId": "6595518396205e06b897ad65",
  "data": {
    "type": "notification_event_data",
    "item": {
      "id": "6755d2162acedc23e26dd3d1",
      "type": "post",
      "title": "Hello, world!",
      "content": "<p>This is a test post.</p>",
      "user": {
        "id": "5fef50c5e9458a0012f82456",
        "email": "admin@example.com",
        "name": "Admin",
        "profilePicture": "https://example.com/profile.jpg",
        "verified": true,
        "type": "admin",
        "description": ""
      },
      "postStatus": {
        "name": "In Review",
        "color": "Sky",
        "type": "reviewing",
        "isDefault": true,
        "id": "6595518396205e06b897ad6a"
      },
      "postCategory": {
        "category": "Feature Request",
        "private": false,
        "segmentIds": [],
        "roles": [],
        "customInputFields": [],
        "icon": {
          "type": "predefined",
          "value": "LightBulbIcon"
        },
        "id": "6755d0970b5d5b1fefdf54f4"
      }
      "date": "2024-12-08T17:06:30.577Z",
      "slug": "hello-world-29",
      "upvotes": 1,
      "inReview": false,
      "accessUsers": [],
      "accessCompanies": [],
      "clickupTasks": [],
      "githubIssues": [],
      "commentsAllowed": true,
      "categoryId": "6755d0970b5d5b1fefdf54f4",
      "lastModified": "2024-12-08T17:06:30.577Z",
      "lastUpvoted": "2024-12-08T17:06:30.574Z",
      "commentCount": 0,
      "postTags": [],
      "pinned": false,
      "devopsWorkItems": []
    }
  },
  "id": "notif_0193a73c-a97f-76b8-b855-2b85c7cec70a",
  "webhookId": "675346db13af7340748ce850",
  "createdAt": "2024-12-08T17:06:30.911Z",
  "deliveryStatus": "failure",
  "firstSentAt": "2024-12-08T17:06:31.162Z",
  "deliveryAttempts": 1
}

post.updated

{
  "type": "notification_event",
  "topic": "post.updated",
  "organizationId": "6595518396205e06b897ad65",
  "data": {
    "type": "notification_event_data",
    "item": {
      "id": "6755d2162acedc23e26dd3d1",
      "type": "post",
      "title": "New title",
      "content": "<p>New content</p>",
      "user": {
        "id": "5fef50c5e9458a0012f82456",
        "email": "admin@example.com",
        "name": "Admin",
        "profilePicture": "https://example.com/profile.jpg",
        "verified": true,
        "type": "admin",
        "description": ""
      },
      "postStatus": {
        "name": "Completed",
        "color": "Green",
        "type": "completed",
        "isDefault": false,
        "id": "6595518396205e06b897ad6d"
      },
      "postCategory": {
        "category": "Feature Request",
        "private": false,
        "segmentIds": [],
        "roles": [],
        "customInputFields": [],
        "icon": {
          "type": "predefined",
          "value": "LightBulbIcon"
        },
        "id": "6755d0970b5d5b1fefdf54f4"
      }
      "date": "2024-12-08T17:06:30.577Z",
      "slug": "hello-world-29",
      "upvotes": 1,
      "inReview": false,
      "accessUsers": [],
      "accessCompanies": [],
      "clickupTasks": [],
      "githubIssues": [],
      "commentsAllowed": true,
      "categoryId": "6755d0970b5d5b1fefdf54f4",
      "lastModified": "2024-12-08T17:06:30.577Z",
      "lastUpvoted": "2024-12-08T17:06:30.574Z",
      "commentCount": 0,
      "postTags": [],
      "pinned": false,
      "devopsWorkItems": []
    },
    "changes": [
      {
        "field": "postStatus.name",
        "oldValue": "In Review",
        "newValue": "Completed"
      },
      {
        "field": "postStatus.color",
        "oldValue": "Sky",
        "newValue": "Green"
      },
      {
        "field": "postStatus.type",
        "oldValue": "reviewing",
        "newValue": "completed"
      },
      {
        "field": "postStatus.isDefault",
        "oldValue": true,
        "newValue": false
      },
      {
        "field": "postStatus.id",
        "oldValue": "6595518396205e06b897ad6a",
        "newValue": "6595518396205e06b897ad6d"
      },
      {
        "field": "content",
        "oldValue": "<p>Old content</p>",
        "newValue": "<p>New content</p>"
      },
      {
        "field": "title",
        "oldValue": "Old title",
        "newValue": "New title"
      }
    ]
  },
  "id": "notif_0193a73c-a97f-76b8-b855-2b85c7cec70a",
  "webhookId": "675346db13af7340748ce850",
  "createdAt": "2024-12-08T17:06:30.911Z",
  "deliveryStatus": "pending",
  "firstSentAt": "2024-12-08T17:06:31.162Z",
  "deliveryAttempts": 1
}

post.deleted

{
  "type": "notification_event",
  "topic": "post.deleted",
  "organizationId": "6595518396205e06b897ad65",
  "data": {
    "type": "notification_event_data",
    "item": {
      "id": "6755d2162acedc23e26dd3d1",
      "type": "post",
      "title": "Hello, world!",
      "content": "<p>This is a test post.</p>",
      "user": {
        "id": "5fef50c5e9458a0012f82456",
        "email": "admin@example.com",
        "name": "Admin",
        "profilePicture": "https://example.com/profile.jpg",
        "verified": true,
        "type": "admin",
        "description": ""
      },
      "postStatus": {
        "name": "In Review",
        "color": "Sky",
        "type": "reviewing",
        "isDefault": true,
        "id": "6595518396205e06b897ad6a"
      },
      "postCategory": {
        "category": "Feature Request",
        "private": false,
        "segmentIds": [],
        "roles": [],
        "customInputFields": [],
        "icon": {
          "type": "predefined",
          "value": "LightBulbIcon"
        },
        "id": "6755d0970b5d5b1fefdf54f4"
      }
      "date": "2024-12-08T17:06:30.577Z",
      "slug": "hello-world-29",
      "upvotes": 1,
      "inReview": false,
      "accessUsers": [],
      "accessCompanies": [],
      "clickupTasks": [],
      "githubIssues": [],
      "commentsAllowed": true,
      "categoryId": "6755d0970b5d5b1fefdf54f4",
      "lastModified": "2024-12-08T17:06:30.577Z",
      "lastUpvoted": "2024-12-08T17:06:30.574Z",
      "commentCount": 0,
      "postTags": [],
      "pinned": false,
      "devopsWorkItems": []
    }
  },
  "id": "notif_0193a73c-a97f-76b8-b855-2b85c7cec70a",
  "webhookId": "675346db13af7340748ce850",
  "createdAt": "2024-12-08T17:06:30.911Z",
  "deliveryStatus": "failure",
  "firstSentAt": "2024-12-08T17:06:31.162Z",
  "deliveryAttempts": 1
}

post.voted

{
  "type": "notification_event",
  "topic": "post.voted",
  "organizationId": "6595518396205e06b897ad65",
  "data": {
    "type": "notification_event_data",
    "item": {
      "type": "post_vote",
      "action": "add", // or "remove"
      "submissionId": "6755d2162acedc23e26dd3d1",
      "user": {
        "id": "5fef50c5e9458a0012f82456",
        "email": "admin@example.com",
        "name": "Admin",
        "profilePicture": "https://example.com/profile.jpg",
        "verified": true,
        "type": "admin",
        "description": ""
      }
    }
  },
  "id": "notif_0193a741-a81a-7716-80e3-3236608244ec",
  "webhookId": "675346db13af7340748ce850",
  "createdAt": "2024-12-08T17:11:58.234Z",
  "deliveryStatus": "pending"
}

Changelog Events

changelog.published

{
  "type": "notification_event",
  "topic": "changelog.published",
  "organizationId": "6595518396205e06b897ad65",
  "data": {
    "type": "notification_event_data",
    "item": {
      "id": "6755d4e73ba80aa1cd90bae8",
      "type": "changelog",
      "title": "[EN] Hello World!",
      "content": "<p>This is a test changelog.</p>",
      "markdownContent": "This is a test changelog.",
      "featuredImage": "https://fb-usercontent.fra1.cdn.digitaloceanspaces.com/0193a748-9c36-7976-a414-672869c3e160.png",
      "date": "2024-12-08T17:18:31.396Z",
      "state": "live",
      "locale": "en",
      "slug": "en-hello-world-2",
      "firstPublishInLocale": true, // If this is the first publish in this locale
      "changelogCategories": [
        {
          "name": "New",
          "color": "Green",
          "segmentIds": [],
          "roles": [],
          "id": "6595518396205e06b897ad6f"
        },
        {
          "name": "Improved",
          "color": "Sky",
          "segmentIds": [],
          "roles": [],
          "id": "6595518396205e06b897ad70"
        }
      ],
      "commentCount": 0,
      "notifications": {
        "en": {
          "sendEmailNotification": false,
          "hideFromBoardAndWidgets": false,
          "scheduledDate": null
        },
        "de": {
          "hideFromBoardAndWidgets": false,
          "sendEmailNotification": true,
          "scheduledDate": null
        }
      },
      "allowedSegmentIds": [],
      "isDraftDiffersFromLive": false,
      "isPublished": true,
      "availableLocales": [
        "en",
        "de"
      ],
      "publishedLocales": [
        "en",
        "de"
      ],
      "slugs": {
        "en": "en-hello-world-2",
        "de": "de-hallo-welt-2"
      },
      "organization": "robiorg4"
    },
    "changes": []
  },
  "id": "notif_0193a764-e5be-7f0a-90dd-a9cdffc79c43",
  "webhookId": "675346db13af7340748ce850",
  "createdAt": "2024-12-08T17:50:27.774Z",
  "deliveryStatus": "failure",
  "firstSentAt": "2024-12-08T17:50:28.023Z",
  "deliveryAttempts": 1
}

Comment Events

comment.created

{
  "type": "notification_event",
  "topic": "comment.created",
  "organizationId": "6595518396205e06b897ad65",
  "data": {
    "type": "notification_event_data",
    "item": {
      "type": "comment",
      "id": "6755d722fcb2ad2f2e2b16a1",
      "content": "<p>Hey!<br><br>This is a great idea!</p>",
      "user": {
        "id": "5fef50c5e9458a0012f82456",
        "email": "admin@example.com",
        "name": "Admin",
        "profilePicture": "https://example.com/profile.jpg",
        "verified": true,
        "type": "admin",
        "description": ""
      },
      "isPrivate": false,
      "score": 1,
      "upvotes": 1,
      "downvotes": 0,
      "inReview": false,
      "pinned": false,
      "emailSent": false,
      "sendNotification": true,
      "createdAt": "2024-12-08T17:28:02.468Z",
      "updatedAt": "2024-12-08T17:28:02.468Z",
      "organization": "6595518396205e06b897ad65",
      "submission": "67530bae0c2341f39d2c5847",
      "path": "67530bae0c2341f39d2c5847"
    }
  },
  "id": "notif_0193a750-5fc9-7739-8714-758bff6e62c6",
  "webhookId": "675346db13af7340748ce850",
  "createdAt": "2024-12-08T17:28:02.761Z",
  "deliveryStatus": "pending"
}

comment.updated

{
  "type": "notification_event",
  "topic": "comment.updated",
  "organizationId": "6595518396205e06b897ad65",
  "data": {
    "type": "notification_event_data",
    "item": {
      "type": "comment",
      "id": "6755d722fcb2ad2f2e2b16a1",
      "content": "<p>New content</p>",
      "user": {
        "id": "5fef50c5e9458a0012f82456",
        "email": "admin@example.com",
        "name": "Admin",
        "profilePicture": "https://example.com/profile.jpg",
        "verified": true,
        "type": "admin",
        "description": ""
      },
      "isPrivate": false,
      "score": 1,
      "upvotes": 1,
      "downvotes": 0,
      "inReview": false,
      "pinned": false,
      "emailSent": false,
      "sendNotification": true,
      "createdAt": "2024-12-08T17:28:02.468Z",
      "updatedAt": "2024-12-08T17:28:02.468Z",
      "organization": "6595518396205e06b897ad65",
      "submission": "67530bae0c2341f39d2c5847",
      "path": "67530bae0c2341f39d2c5847"
    },
    "changes": [
      {
        "field": "content",
        "oldValue": "<p>Old content</p>",
        "newValue": "<p>New content</p>"
      }
    ]
  },
  "id": "notif_0193a750-5fc9-7739-8714-758bff6e62c6",
  "webhookId": "675346db13af7340748ce850",
  "createdAt": "2024-12-08T17:28:02.761Z",
  "deliveryStatus": "pending"
}

comment.deleted

{
  "type": "notification_event",
  "topic": "comment.deleted",
  "organizationId": "6595518396205e06b897ad65",
  "data": {
    "type": "notification_event_data",
    "item": {
      "type": "comment",
      "id": "6755d722fcb2ad2f2e2b16a1",
      "content": "<p>Hey!<br><br>This is a great idea!</p>",
      "user": {
        "id": "5fef50c5e9458a0012f82456",
        "email": "admin@example.com",
        "name": "Admin",
        "profilePicture": "https://example.com/profile.jpg",
        "verified": true,
        "type": "admin",
        "description": ""
      },
      "isPrivate": false,
      "score": 1,
      "upvotes": 1,
      "downvotes": 0,
      "inReview": false,
      "pinned": false,
      "emailSent": false,
      "sendNotification": true,
      "createdAt": "2024-12-08T17:28:02.468Z",
      "updatedAt": "2024-12-08T17:28:02.468Z",
      "organization": "6595518396205e06b897ad65",
      "submission": "67530bae0c2341f39d2c5847",
      "path": "67530bae0c2341f39d2c5847"
    }
  },
  "id": "notif_0193a750-5fc9-7739-8714-758bff6e62c6",
  "webhookId": "675346db13af7340748ce850",
  "createdAt": "2024-12-08T17:28:02.761Z",
  "deliveryStatus": "pending"
}

Each webhook event includes:

  • type: Always "notification_event"
  • topic: The specific event type
  • organizationId: Your organization's unique identifier
  • data: Contains the event details
    • type: Always "notification_event_data"
    • item: The resource that triggered the event
    • changes: For update events, shows what changed
  • id: Unique identifier for the webhook event
  • webhookId: ID of the webhook configuration
  • createdAt: Timestamp when the event occurred
  • deliveryStatus: Current delivery status of the webhook

The item object structure varies based on the event type, but always includes the resource's essential data and any relevant timestamps.


Example webhook handlers

Here are examples of webhook handlers for different languages.

Webhook Handler Examples

from fastapi import FastAPI, Request, HTTPException
import logging

app = FastAPI()
logger = logging.getLogger(__name__)

async def process_webhook_event(event_data: dict) -> None:
    """Process the webhook event asynchronously."""
    try:
        topic = event_data.get('topic')
        
        match topic:
            case 'post.created':
                await handle_post_created(event_data)
            case 'post.updated':
                await handle_post_updated(event_data)
            case _:
                logger.warning(f"Unhandled webhook topic: {topic}")
                
    except Exception as e:
        logger.error(f"Error processing webhook: {str(e)}", exc_info=True)
        
@app.post("/webhooks/featurebase")
async def handle_webhook(request: Request):
    try:
        # Return 200 immediately to acknowledge receipt
        background_tasks = request.background
        
        # Get the raw payload
        payload = await request.json()
        
        # Verify webhook signature (implemented in security section)
        # verify_webhook_signature(request)
        
        # Process webhook asynchronously
        background_tasks.add_task(process_webhook_event, payload)
        
        return {"status": "accepted"}
        
    except Exception as e:
        logger.error(f"Webhook handler error: {str(e)}", exc_info=True)
        raise HTTPException(status_code=500, detail="Internal server error")

When implementing webhook handlers, you'll want to:

  1. Validate the webhook signature immediately (see security section below)
  2. Return a quick 200 OK response to acknowledge receipt
  3. Move the actual processing to a background task/job
  4. Handle different event types based on the webhook topic
  5. Add error handling and logging for debugging issues

Choose the example that matches your tech stack and adapt it to your needs. Remember to implement the webhook signature verification as shown in the Security section.


Security

To verify webhook authenticity, each request includes these security headers:

  • X-Webhook-Signature: HMAC-SHA256 signature of the request
  • X-Webhook-Timestamp: Unix timestamp when the request was sent

The signature is generated by combining the timestamp and raw request body with a dot (.) separator, then creating an HMAC-SHA256 hash using your webhook secret. Here's how to implement secure verification:

Verifying webhook signatures

import hmac
import hashlib
import time
from fastapi import Request, HTTPException

WEBHOOK_SECRET = "whsec_..."  # Load from environment variables
MAX_TIMESTAMP_DIFF = 300  # 5 minutes

async def verify_webhook_signature(request: Request) -> None:
    """Verify the webhook signature or raise HTTPException."""
    try:
        # Get signature and timestamp
        signature = request.headers.get("X-Webhook-Signature")
        timestamp = request.headers.get("X-Webhook-Timestamp")
        
        if not signature or not timestamp:
            raise HTTPException(401, "Missing signature headers")
            
        # Verify timestamp freshness
        timestamp_diff = abs(int(time.time()) - int(timestamp))
        if timestamp_diff > MAX_TIMESTAMP_DIFF:
            raise HTTPException(401, "Webhook timestamp too old")
            
        # Get raw body
        body = await request.body()
        
        # Create signed payload
        signed_payload = f"{timestamp}.{body.decode('utf-8')}"
        
        # Calculate expected signature
        expected = hmac.new(
            WEBHOOK_SECRET.encode('utf-8'),
            signed_payload.encode('utf-8'),
            hashlib.sha256
        ).hexdigest()
        
        # Secure comparison
        if not hmac.compare_digest(expected, signature):
            raise HTTPException(401, "Invalid signature")
            
    except ValueError:
        raise HTTPException(401, "Invalid signature format")

# Usage in webhook handler:
@app.post("/webhooks/featurebase")
async def handle_webhook(request: Request):
    await verify_webhook_signature(request)
    # ... rest of handler code

For maximum security:

  1. Verify the webhook signature as soon as possible using HMAC-SHA256
  2. Ensure the timestamp is within a 5-minute window to prevent replay attacks
  3. Use constant-time comparison when checking signatures
  4. Return 200 OK immediately after verification
  5. Process the webhook payload asynchronously after responding
  6. Keep your webhook secret key safe and never commit it to source control

Event Delivery Behaviors

This section helps you understand different behaviors to expect regarding how Featurebase sends events to your webhook endpoint.

Retry behavior

If a webhook delivery fails, Featurebase will automatically retry up to 3 times:

  1. 1 minute after the first attempt
  2. 1 hour after the second attempt
  3. 6 hours after the third attempt

Event ordering

Featurebase doesn't guarantee that webhook events arrive in the order they were generated. For example, updating a post might generate the following sequence of events:

  1. post.created
  2. post.updated

However, these events may not be delivered to your endpoint in that exact order. Your integration should not rely on receiving events sequentially, and it must be able to handle them regardless of arrival sequence.

Handle duplicate events

Webhook endpoints might occasionally receive the same event more than once. To prevent double-processing, log the unique event IDs that your endpoint has already handled. If a new incoming event's ID matches a previously processed event, skip it.

Example webhook event with ID

{
    "type": "notification_event",
    "topic": "post.updated",
    "id": "notif_0193a6e6-fb6b-78ef-b71f-15008d9f9cde",
    // ... rest of the event data
}

Quick response time

Your endpoint should return a successful (2xx) status code promptly—before performing any lengthy operations that might lead to timeouts. For example:

// Good: Return 200 immediately, then process
app.post('/webhook', async (req, res) => {
    // 1. Return 200 OK right away
    res.status(200).send('OK');
    
    // 2. Process the webhook asynchronously
    processWebhookEvent(req.body).catch(err => {
        console.error('Webhook processing error:', err);
    });
});

// Bad: Wait for processing before responding
app.post('/webhook', async (req, res) => {
    // Don't do this - it could timeout
    await processWebhookEvent(req.body);
    res.status(200).send('OK');
});

This approach prevents timeouts and ensures Featurebase knows your endpoint received the event, even if processing takes longer.