Skip to main content
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
}

Webhook Notification Format

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

400
Bad Request
Invalid input data or webhook URL format
403
Forbidden
  • Insufficient permissions (not owner/admin)
  • Plan doesn’t support integrations
404
Not Found
  • Workspace not found
  • Integration not found
500
Internal Server Error
  • Webhook delivery failed
  • Database error

Error Response Format

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
  • 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

Build docs developers (and LLMs) love