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 typeorganizationId
: Your organization's unique identifierdata
: Contains the event detailstype
: Always "notification_event_data"item
: The resource that triggered the eventchanges
: For update events, shows what changed
id
: Unique identifier for the webhook eventwebhookId
: ID of the webhook configurationcreatedAt
: Timestamp when the event occurreddeliveryStatus
: 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:
- Validate the webhook signature immediately (see security section below)
- Return a quick 200 OK response to acknowledge receipt
- Move the actual processing to a background task/job
- Handle different event types based on the webhook topic
- 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 requestX-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:
- Verify the webhook signature as soon as possible using HMAC-SHA256
- Ensure the timestamp is within a 5-minute window to prevent replay attacks
- Use constant-time comparison when checking signatures
- Return 200 OK immediately after verification
- Process the webhook payload asynchronously after responding
- 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 minute after the first attempt
- 1 hour after the second attempt
- 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:
post.created
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.