Featul provides a type-safe API powered by jstack (RPC over Hono) that allows you to build custom integrations and automate your feedback workflow.
Authentication
All API requests require authentication using session-based authentication. The API uses Better Auth with cross-subdomain cookie support.
Server-Side Authentication
import { getServerSession } from '@featul/auth/session'
const session = await getServerSession ()
if ( ! session ) {
// User not authenticated
}
Client-Side API Calls
Use the Featul client from @featul/api:
import { client } from '@featul/api'
// Client automatically handles authentication via cookies
const result = await client . integration . list . $post ({
workspaceSlug: 'your-workspace'
})
Integration Endpoints
The integration API provides four main endpoints for managing webhook integrations.
List Integrations
Retrieve all integrations for a workspace.
const response = await client . integration . list . $post ({
workspaceSlug: 'your-workspace'
})
const { integrations } = response . data
// integrations: Array<{
// id: string
// type: 'discord' | 'slack'
// isActive: boolean
// lastTriggeredAt: Date | null
// createdAt: Date
// }>
Permissions : Workspace owner or administrator
Response :
integrations: Array of integration objects
id: Unique integration ID
type: Integration type (discord or slack)
isActive: Whether the integration is active
lastTriggeredAt: Last time a notification was sent
createdAt: When the integration was created
Webhook URLs are not returned for security reasons. Only owners and admins can view/modify them.
Connect Integration
Connect a new webhook integration or update an existing one.
const response = await client . integration . connect . $post ({
workspaceSlug: 'your-workspace' ,
type: 'discord' , // or 'slack'
webhookUrl: 'https://discord.com/api/webhooks/...'
})
if ( response . data . success ) {
console . log ( response . data . message )
// "Discord integration connected"
}
Input :
workspaceSlug: Your workspace slug
type: Integration type (discord or slack)
webhookUrl: Valid webhook URL
Webhook URL Validation :
Discord : Must match https://discord.com/api/webhooks/{id}/{token}
Slack : Must match https://hooks.slack.com/services/{T...}/{B...}/{...}
Permissions : Workspace owner or administrator
Plan Requirement : Starter or Professional plan
Behavior :
If an integration of the same type exists, it will be updated
Only one integration per type is allowed per workspace
Disconnect Integration
Remove a webhook integration.
const response = await client . integration . disconnect . $post ({
workspaceSlug: 'your-workspace' ,
type: 'discord'
})
if ( response . data . success ) {
console . log ( response . data . message )
// "Discord integration disconnected"
}
Input :
workspaceSlug: Your workspace slug
type: Integration type to disconnect
Permissions : Workspace owner or administrator
Error Cases :
404: Integration not found
403: Insufficient permissions
Test Integration
Send a test notification to verify the webhook is working.
const response = await client . integration . test . $post ({
workspaceSlug: 'your-workspace' ,
type: 'slack'
})
if ( response . data . success ) {
console . log ( response . data . message )
// "Test notification sent successfully"
}
Input :
workspaceSlug: Your workspace slug
type: Integration type to test
Permissions : Workspace owner or administrator
Plan Requirement : Starter or Professional plan
Test Notification Data :
{
title : "Test Notification" ,
content : "This is a test notification from Featul..." ,
boardName : "Test Board" ,
workspaceName : "Your Workspace" ,
authorName : "Featul Bot" ,
// ... other test data
}
When new feedback is posted, Featul automatically sends notifications to all active integrations.
Notification Trigger
Webhooks are triggered automatically when:
A new feedback post is created
The workspace is on Starter or Professional plan
At least one integration is active
Notification Data Structure
interface PostNotificationData {
id : string
title : string
content : string
slug : string
boardName : string
boardSlug : string
workspaceName : string
workspaceSlug : string
authorName ?: string
status ?: string
image ?: string | null
createdAt : Date
}
Discord Webhook Payload
{
"embeds" : [
{
"author" : {
"name" : "New Feedback Submission"
},
"title" : "Feature request: Dark mode" ,
"url" : "https://workspace.featul.com/board/p/dark-mode" ,
"color" : 5814783 ,
"description" : "Please add dark mode to the dashboard..." ,
"thumbnail" : {
"url" : "https://..."
},
"fields" : [
{
"name" : "Board" ,
"value" : "**Feature Requests**" ,
"inline" : true
},
{
"name" : "Status" ,
"value" : "**Pending**" ,
"inline" : true
},
{
"name" : "Submitted by" ,
"value" : "**John Doe**" ,
"inline" : true
}
],
"timestamp" : "2024-03-15T10:30:00Z" ,
"footer" : {
"text" : "My Workspace • Powered by Featul"
}
}
]
}
Slack Webhook Payload
{
"blocks" : [
{
"type" : "header" ,
"text" : {
"type" : "plain_text" ,
"text" : "🆕 New Feedback Submission"
}
},
{
"type" : "section" ,
"text" : {
"type" : "mrkdwn" ,
"text" : "*<https://workspace.featul.com/board/p/dark-mode|Feature request: Dark mode>*"
}
},
{
"type" : "section" ,
"text" : {
"type" : "mrkdwn" ,
"text" : "Please add dark mode to the dashboard..."
}
},
{
"type" : "context" ,
"elements" : [
{
"type" : "mrkdwn" ,
"text" : "*Board:* Feature Requests"
},
{
"type" : "mrkdwn" ,
"text" : "*Status:* Pending"
},
{
"type" : "mrkdwn" ,
"text" : "*Submitted by:* John Doe"
}
]
}
]
}
Building Custom Integrations
Programmatic Webhook Management
You can build a custom integration dashboard or automation tool:
import { client } from '@featul/api'
// Example: Rotation tool for webhook URLs
async function rotateWebhook (
workspaceSlug : string ,
type : 'discord' | 'slack' ,
newWebhookUrl : string
) {
// Update the webhook URL
const result = await client . integration . connect . $post ({
workspaceSlug ,
type ,
webhookUrl: newWebhookUrl
})
if ( ! result . data . success ) {
throw new Error ( 'Failed to update webhook' )
}
// Test the new webhook
const testResult = await client . integration . test . $post ({
workspaceSlug ,
type
})
if ( ! testResult . data . success ) {
throw new Error ( 'New webhook test failed' )
}
return { success: true }
}
Monitoring Integration Health
import { client } from '@featul/api'
async function checkIntegrationHealth ( workspaceSlug : string ) {
const { integrations } = await client . integration . list . $post ({
workspaceSlug
}). then ( r => r . data )
for ( const integration of integrations ) {
if ( ! integration . isActive ) {
console . warn ( ` ${ integration . type } integration is inactive` )
continue
}
if ( ! integration . lastTriggeredAt ) {
console . warn ( ` ${ integration . type } has never been triggered` )
continue
}
const hoursSinceLastTrigger =
( Date . now () - integration . lastTriggeredAt . getTime ()) / ( 1000 * 60 * 60 )
if ( hoursSinceLastTrigger > 24 ) {
console . log ( ` ${ integration . type } hasn't been triggered in ${ hoursSinceLastTrigger . toFixed ( 1 ) } hours` )
}
}
}
Error Handling
Common Error Codes
Invalid input data or webhook URL format
Insufficient permissions (not owner/admin)
Plan doesn’t support integrations
Workspace not found
Integration not found
Webhook delivery failed
Database error
try {
await client . integration . connect . $post ({ ... })
} catch ( error ) {
if ( error instanceof HTTPException ) {
console . error ( `Error ${ error . status } : ${ error . message } ` )
// Example: "Error 403: Integrations are available on Starter or Professional plans"
}
}
Database Schema
The integration data is stored in the workspace_integration table:
export const workspaceIntegration = pgTable ( 'workspace_integration' , {
id: text ( 'id' ). primaryKey (),
workspaceId: text ( 'workspace_id' ). notNull (). references (() => workspace . id ),
type: text ( 'type' , { enum: [ 'discord' , 'slack' ] }). notNull (),
webhookUrl: text ( 'webhook_url' ). notNull (),
isActive: boolean ( 'is_active' ). default ( true ),
lastTriggeredAt: timestamp ( 'last_triggered_at' ),
createdAt: timestamp ( 'created_at' ). notNull (). defaultNow (),
updatedAt: timestamp ( 'updated_at' ). notNull (). defaultNow ()
})
Constraints :
One integration per type per workspace (unique index)
Cascade delete when workspace is deleted
Workspace ID index for fast lookups
Rate Limits
Currently, Featul does not enforce rate limits on integration API endpoints. However:
Webhook notifications are triggered once per post creation
Test notifications should be used sparingly
Failed webhook deliveries are logged but don’t retry automatically
Be mindful of Discord and Slack’s rate limits when testing webhooks:
Discord: ~5 messages per 2 seconds per webhook
Slack: ~1 message per second per webhook
Type Safety
Featul’s API is fully type-safe using jstack. All endpoints have TypeScript types automatically inferred:
import type {
ConnectWebhookInput ,
DisconnectWebhookInput ,
TestWebhookInput ,
ListIntegrationsInput
} from '@featul/api/validators/integration'
// Types are automatically enforced
const input : ConnectWebhookInput = {
workspaceSlug: 'my-workspace' ,
type: 'discord' , // Type-safe: only 'discord' | 'slack'
webhookUrl: 'https://discord.com/api/webhooks/...' // Validated at runtime
}
Best Practices
Store webhook URLs securely
Never commit them to version control
Use environment variables
Rotate them if exposed
Always use the test endpoint before going live
Verify notifications appear correctly
Check formatting in both light and dark themes
Monitor integration health
Check lastTriggeredAt regularly
Set up alerts for inactive integrations
Review webhook delivery failures
Implement proper error handling
Don’t retry failed requests immediately
Log errors for debugging
Example: Complete Integration Flow
Here’s a complete example of setting up and managing an integration:
import { client } from '@featul/api'
async function setupDiscordIntegration (
workspaceSlug : string ,
webhookUrl : string
) {
try {
// 1. Connect the integration
const connectResult = await client . integration . connect . $post ({
workspaceSlug ,
type: 'discord' ,
webhookUrl
})
console . log ( connectResult . data . message )
// 2. Test the connection
const testResult = await client . integration . test . $post ({
workspaceSlug ,
type: 'discord'
})
if ( ! testResult . data . success ) {
throw new Error ( 'Test notification failed' )
}
// 3. Verify it's active
const { integrations } = await client . integration . list . $post ({
workspaceSlug
}). then ( r => r . data )
const discord = integrations . find ( i => i . type === 'discord' )
if ( discord ?. isActive ) {
console . log ( 'Discord integration is active and ready!' )
return { success: true , integrationId: discord . id }
}
} catch ( error ) {
console . error ( 'Failed to setup Discord integration:' , error )
throw error
}
}
Next Steps
Webhook Integrations Set up Discord or Slack webhooks
API Reference Explore the full API documentation