← Back to Blog
Chatbot Webhook Integration: Connect to Slack, Discord, CRMs & More in 2026

Chatbot Webhook Integration: Connect to Slack, Discord, CRMs & More in 2026

WebhooksSlackDiscordCRMIntegration

Chatbot Webhook Integration: Connect to Slack, Discord, CRMs & More in 2026

Webhooks transform your chatbot from an isolated widget into an integrated business tool. This guide covers webhook architecture, implementation patterns for popular platforms, and production best practices for reliable event-driven integrations.

Why Webhooks Matter for Chatbots

The Integration Problem

Standalone chatbots create data silos:

  • Conversations don't reach your team
  • Leads aren't captured in CRM
  • Support tickets aren't created
  • Analytics are fragmented

Webhooks as the Solution

Webhooks enable real-time, event-driven communication:

Chatbot Event → Webhook → External Service → Action
Event Webhook Target Result
New conversation Slack Team notification
Lead captured HubSpot CRM New contact created
Support request Zendesk Ticket opened
Purchase intent Discord Sales alert
Negative sentiment PagerDuty On-call notification

Webhook Architecture

Core Components

┌─────────────────────────────────────────────────────────────┐
│                   Webhook System                            │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌──────────┐    ┌──────────┐    ┌──────────┐             │
│  │  Event   │───▶│  Queue   │───▶│ Webhook  │             │
│  │ Emitter  │    │ (Redis)  │    │ Processor│             │
│  └──────────┘    └──────────┘    └────┬─────┘             │
│                                       │                    │
│                     ┌─────────────────┼─────────────────┐  │
│                     │                 │                 │  │
│                     ▼                 ▼                 ▼  │
│               ┌──────────┐     ┌──────────┐     ┌────────┐│
│               │  Slack   │     │  CRM     │     │Discord ││
│               │ Webhook  │     │ Webhook  │     │Webhook ││
│               └──────────┘     └──────────┘     └────────┘│
│                                                             │
└─────────────────────────────────────────────────────────────┘

Event Types

// types/webhookEvents.ts
export type WebhookEventType =
  | 'conversation.started'
  | 'conversation.ended'
  | 'message.received'
  | 'message.sent'
  | 'lead.captured'
  | 'handoff.requested'
  | 'sentiment.negative'
  | 'feedback.received'

export interface WebhookEvent<T = any> {
  id: string
  type: WebhookEventType
  timestamp: string
  chatbotId: string
  conversationId: string
  data: T
}

export interface ConversationStartedData {
  visitorId: string
  pageUrl: string
  referrer?: string
  userAgent: string
  metadata?: Record<string, any>
}

export interface LeadCapturedData {
  email: string
  name?: string
  phone?: string
  company?: string
  source: string
  conversationSummary: string
}

export interface MessageData {
  role: 'user' | 'assistant'
  content: string
  sentiment?: 'positive' | 'neutral' | 'negative'
}

Building the Webhook System

Webhook Configuration

// lib/webhooks/config.ts
export interface WebhookConfig {
  id: string
  name: string
  url: string
  secret: string
  events: WebhookEventType[]
  enabled: boolean
  retryPolicy: {
    maxRetries: number
    backoffMs: number
  }
  headers?: Record<string, string>
}

// Store webhook configurations
const webhookConfigs: Map<string, WebhookConfig[]> = new Map()

export function registerWebhook(chatbotId: string, config: WebhookConfig) {
  const existing = webhookConfigs.get(chatbotId) || []
  existing.push(config)
  webhookConfigs.set(chatbotId, existing)
}

export function getWebhooksForEvent(
  chatbotId: string,
  eventType: WebhookEventType
): WebhookConfig[] {
  const configs = webhookConfigs.get(chatbotId) || []
  return configs.filter(
    c => c.enabled && c.events.includes(eventType)
  )
}

Event Emitter

// lib/webhooks/emitter.ts
import { EventEmitter } from 'events'
import type { WebhookEvent, WebhookEventType } from './types'

class WebhookEventEmitter extends EventEmitter {
  emit<T>(type: WebhookEventType, data: Omit<WebhookEvent<T>, 'id' | 'timestamp' | 'type'>): boolean {
    const event: WebhookEvent<T> = {
      id: crypto.randomUUID(),
      type,
      timestamp: new Date().toISOString(),
      ...data,
    }

    return super.emit(type, event)
  }
}

export const webhookEmitter = new WebhookEventEmitter()

// Usage in chatbot code
export function emitConversationStarted(
  chatbotId: string,
  conversationId: string,
  data: ConversationStartedData
) {
  webhookEmitter.emit('conversation.started', {
    chatbotId,
    conversationId,
    data,
  })
}

export function emitLeadCaptured(
  chatbotId: string,
  conversationId: string,
  data: LeadCapturedData
) {
  webhookEmitter.emit('lead.captured', {
    chatbotId,
    conversationId,
    data,
  })
}

Webhook Processor with Queue

// lib/webhooks/processor.ts
import { Queue, Worker } from 'bullmq'
import { Redis } from 'ioredis'
import { getWebhooksForEvent } from './config'
import { signPayload, sendWebhook } from './sender'

const redis = new Redis(process.env.REDIS_URL!)

const webhookQueue = new Queue('webhooks', {
  connection: redis,
  defaultJobOptions: {
    attempts: 3,
    backoff: {
      type: 'exponential',
      delay: 1000,
    },
  },
})

// Enqueue webhook deliveries
export async function queueWebhookDelivery(event: WebhookEvent) {
  const webhooks = getWebhooksForEvent(event.chatbotId, event.type)

  for (const webhook of webhooks) {
    await webhookQueue.add('deliver', {
      event,
      webhook,
    })
  }
}

// Process webhook deliveries
const worker = new Worker(
  'webhooks',
  async (job) => {
    const { event, webhook } = job.data

    const signature = signPayload(event, webhook.secret)
    const response = await sendWebhook(webhook.url, event, {
      ...webhook.headers,
      'X-Webhook-Signature': signature,
      'X-Webhook-Event': event.type,
    })

    if (!response.ok) {
      throw new Error(`Webhook failed: ${response.status}`)
    }

    return { delivered: true }
  },
  { connection: redis }
)

worker.on('failed', (job, err) => {
  console.error(`Webhook delivery failed: ${job?.id}`, err)
})

Secure Webhook Delivery

// lib/webhooks/sender.ts
import crypto from 'crypto'

export function signPayload(payload: any, secret: string): string {
  const hmac = crypto.createHmac('sha256', secret)
  hmac.update(JSON.stringify(payload))
  return `sha256=${hmac.digest('hex')}`
}

export function verifySignature(
  payload: string,
  signature: string,
  secret: string
): boolean {
  const expected = signPayload(JSON.parse(payload), secret)
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  )
}

export async function sendWebhook(
  url: string,
  payload: any,
  headers: Record<string, string>
): Promise<Response> {
  const controller = new AbortController()
  const timeout = setTimeout(() => controller.abort(), 10000) // 10s timeout

  try {
    return await fetch(url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        ...headers,
      },
      body: JSON.stringify(payload),
      signal: controller.signal,
    })
  } finally {
    clearTimeout(timeout)
  }
}

Slack Integration

Setting Up Slack Incoming Webhooks

  1. Go to Slack API
  2. Create new app → From scratch
  3. Add "Incoming Webhooks" feature
  4. Activate and add to workspace
  5. Copy webhook URL

Slack Webhook Handler

// lib/integrations/slack.ts
interface SlackMessage {
  text?: string
  blocks?: SlackBlock[]
  attachments?: SlackAttachment[]
}

interface SlackBlock {
  type: string
  text?: { type: string; text: string; emoji?: boolean }
  elements?: any[]
  accessory?: any
}

interface SlackAttachment {
  color?: string
  title?: string
  text?: string
  fields?: Array<{ title: string; value: string; short?: boolean }>
  footer?: string
  ts?: number
}

export class SlackWebhook {
  constructor(private webhookUrl: string) {}

  async send(message: SlackMessage): Promise<void> {
    const response = await fetch(this.webhookUrl, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(message),
    })

    if (!response.ok) {
      throw new Error(`Slack webhook failed: ${response.status}`)
    }
  }

  // Pre-built message templates
  async notifyNewConversation(data: {
    visitorId: string
    pageUrl: string
    conversationId: string
  }) {
    await this.send({
      blocks: [
        {
          type: 'header',
          text: {
            type: 'plain_text',
            text: '💬 New Chat Conversation',
            emoji: true,
          },
        },
        {
          type: 'section',
          fields: [
            {
              type: 'mrkdwn',
              text: `*Visitor:*\n${data.visitorId}`,
            },
            {
              type: 'mrkdwn',
              text: `*Page:*\n${data.pageUrl}`,
            },
          ],
        },
        {
          type: 'actions',
          elements: [
            {
              type: 'button',
              text: { type: 'plain_text', text: 'View Conversation' },
              url: `https://app.example.com/conversations/${data.conversationId}`,
            },
          ],
        },
      ],
    })
  }

  async notifyLeadCaptured(data: LeadCapturedData & { conversationId: string }) {
    await this.send({
      blocks: [
        {
          type: 'header',
          text: {
            type: 'plain_text',
            text: '🎉 New Lead Captured!',
            emoji: true,
          },
        },
        {
          type: 'section',
          text: {
            type: 'mrkdwn',
            text: `*${data.name || 'Unknown'}* just shared their contact info`,
          },
        },
        {
          type: 'section',
          fields: [
            { type: 'mrkdwn', text: `*Email:*\n${data.email}` },
            { type: 'mrkdwn', text: `*Phone:*\n${data.phone || 'N/A'}` },
            { type: 'mrkdwn', text: `*Company:*\n${data.company || 'N/A'}` },
            { type: 'mrkdwn', text: `*Source:*\n${data.source}` },
          ],
        },
        {
          type: 'section',
          text: {
            type: 'mrkdwn',
            text: `*Conversation Summary:*\n>${data.conversationSummary}`,
          },
        },
      ],
    })
  }

  async notifyHandoffRequest(data: {
    conversationId: string
    visitorName?: string
    reason: string
    urgency: 'low' | 'medium' | 'high'
  }) {
    const urgencyEmoji = {
      low: '🟢',
      medium: '🟡',
      high: '🔴',
    }

    await this.send({
      blocks: [
        {
          type: 'header',
          text: {
            type: 'plain_text',
            text: `${urgencyEmoji[data.urgency]} Human Handoff Requested`,
            emoji: true,
          },
        },
        {
          type: 'section',
          text: {
            type: 'mrkdwn',
            text: `A visitor needs human assistance.\n\n*Reason:* ${data.reason}`,
          },
        },
        {
          type: 'actions',
          elements: [
            {
              type: 'button',
              text: { type: 'plain_text', text: 'Take Over Chat' },
              style: 'primary',
              url: `https://app.example.com/conversations/${data.conversationId}/takeover`,
            },
          ],
        },
      ],
    })
  }
}

Registering Slack Webhook

// Setup for a chatbot
import { SlackWebhook } from './integrations/slack'
import { webhookEmitter } from './webhooks/emitter'

const slack = new SlackWebhook(process.env.SLACK_WEBHOOK_URL!)

webhookEmitter.on('conversation.started', async (event) => {
  await slack.notifyNewConversation({
    visitorId: event.data.visitorId,
    pageUrl: event.data.pageUrl,
    conversationId: event.conversationId,
  })
})

webhookEmitter.on('lead.captured', async (event) => {
  await slack.notifyLeadCaptured({
    ...event.data,
    conversationId: event.conversationId,
  })
})

webhookEmitter.on('handoff.requested', async (event) => {
  await slack.notifyHandoffRequest({
    conversationId: event.conversationId,
    ...event.data,
  })
})

Discord Integration

Setting Up Discord Webhooks

  1. Server Settings → Integrations → Webhooks
  2. New Webhook
  3. Choose channel and copy URL

Discord Webhook Handler

// lib/integrations/discord.ts
interface DiscordEmbed {
  title?: string
  description?: string
  color?: number
  fields?: Array<{ name: string; value: string; inline?: boolean }>
  footer?: { text: string }
  timestamp?: string
}

interface DiscordMessage {
  content?: string
  embeds?: DiscordEmbed[]
  username?: string
  avatar_url?: string
}

export class DiscordWebhook {
  constructor(
    private webhookUrl: string,
    private defaultUsername = 'Chatbot',
    private defaultAvatar?: string
  ) {}

  async send(message: DiscordMessage): Promise<void> {
    const response = await fetch(this.webhookUrl, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        username: this.defaultUsername,
        avatar_url: this.defaultAvatar,
        ...message,
      }),
    })

    if (!response.ok) {
      throw new Error(`Discord webhook failed: ${response.status}`)
    }
  }

  async notifyNewLead(data: LeadCapturedData) {
    await this.send({
      embeds: [
        {
          title: '🎯 New Lead Captured',
          color: 0x00ff00, // Green
          fields: [
            { name: 'Name', value: data.name || 'Unknown', inline: true },
            { name: 'Email', value: data.email, inline: true },
            { name: 'Company', value: data.company || 'N/A', inline: true },
            { name: 'Source', value: data.source, inline: true },
            { name: 'Summary', value: data.conversationSummary },
          ],
          timestamp: new Date().toISOString(),
        },
      ],
    })
  }

  async notifySalesOpportunity(data: {
    visitorId: string
    intent: string
    estimatedValue?: number
    conversationId: string
  }) {
    await this.send({
      content: '@here Potential sale detected!',
      embeds: [
        {
          title: '💰 Sales Opportunity',
          color: 0xffd700, // Gold
          fields: [
            { name: 'Visitor', value: data.visitorId, inline: true },
            { name: 'Intent', value: data.intent, inline: true },
            {
              name: 'Est. Value',
              value: data.estimatedValue
                ? `$${data.estimatedValue.toLocaleString()}`
                : 'Unknown',
              inline: true,
            },
          ],
          footer: {
            text: `Conversation: ${data.conversationId}`,
          },
        },
      ],
    })
  }

  async notifyNegativeSentiment(data: {
    conversationId: string
    message: string
    sentimentScore: number
  }) {
    await this.send({
      embeds: [
        {
          title: '⚠️ Negative Sentiment Detected',
          color: 0xff0000, // Red
          description: `A visitor appears frustrated or unhappy.`,
          fields: [
            { name: 'Message', value: `"${data.message}"` },
            {
              name: 'Sentiment Score',
              value: `${(data.sentimentScore * 100).toFixed(0)}% negative`,
              inline: true,
            },
          ],
          timestamp: new Date().toISOString(),
        },
      ],
    })
  }
}

CRM Integrations

HubSpot Integration

// lib/integrations/hubspot.ts
interface HubSpotContact {
  email: string
  firstname?: string
  lastname?: string
  phone?: string
  company?: string
  website?: string
  lifecyclestage?: string
  hs_lead_status?: string
}

export class HubSpotIntegration {
  private baseUrl = 'https://api.hubapi.com'

  constructor(private accessToken: string) {}

  private async request(
    method: string,
    endpoint: string,
    data?: any
  ): Promise<any> {
    const response = await fetch(`${this.baseUrl}${endpoint}`, {
      method,
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${this.accessToken}`,
      },
      body: data ? JSON.stringify(data) : undefined,
    })

    if (!response.ok) {
      const error = await response.json()
      throw new Error(`HubSpot API error: ${error.message}`)
    }

    return response.json()
  }

  async createOrUpdateContact(contact: HubSpotContact): Promise<string> {
    // Search for existing contact
    const searchResult = await this.request(
      'POST',
      '/crm/v3/objects/contacts/search',
      {
        filterGroups: [
          {
            filters: [
              {
                propertyName: 'email',
                operator: 'EQ',
                value: contact.email,
              },
            ],
          },
        ],
      }
    )

    if (searchResult.total > 0) {
      // Update existing contact
      const contactId = searchResult.results[0].id
      await this.request(
        'PATCH',
        `/crm/v3/objects/contacts/${contactId}`,
        { properties: contact }
      )
      return contactId
    }

    // Create new contact
    const result = await this.request('POST', '/crm/v3/objects/contacts', {
      properties: contact,
    })
    return result.id
  }

  async addNoteToContact(contactId: string, note: string): Promise<void> {
    await this.request('POST', '/crm/v3/objects/notes', {
      properties: {
        hs_note_body: note,
        hs_timestamp: Date.now(),
      },
      associations: [
        {
          to: { id: contactId },
          types: [
            {
              associationCategory: 'HUBSPOT_DEFINED',
              associationTypeId: 202, // Note to Contact
            },
          ],
        },
      ],
    })
  }

  async createDeal(data: {
    contactId: string
    dealName: string
    amount?: number
    pipeline?: string
    stage?: string
  }): Promise<string> {
    const result = await this.request('POST', '/crm/v3/objects/deals', {
      properties: {
        dealname: data.dealName,
        amount: data.amount,
        pipeline: data.pipeline || 'default',
        dealstage: data.stage || 'appointmentscheduled',
      },
      associations: [
        {
          to: { id: data.contactId },
          types: [
            {
              associationCategory: 'HUBSPOT_DEFINED',
              associationTypeId: 3, // Deal to Contact
            },
          ],
        },
      ],
    })
    return result.id
  }
}

// Webhook handler for HubSpot
export async function handleLeadForHubSpot(
  event: WebhookEvent<LeadCapturedData>
) {
  const hubspot = new HubSpotIntegration(process.env.HUBSPOT_ACCESS_TOKEN!)

  // Create or update contact
  const contactId = await hubspot.createOrUpdateContact({
    email: event.data.email,
    firstname: event.data.name?.split(' ')[0],
    lastname: event.data.name?.split(' ').slice(1).join(' '),
    phone: event.data.phone,
    company: event.data.company,
    lifecyclestage: 'lead',
    hs_lead_status: 'NEW',
  })

  // Add conversation summary as note
  await hubspot.addNoteToContact(
    contactId,
    `Chatbot Conversation Summary:\n\n${event.data.conversationSummary}\n\nSource: ${event.data.source}`
  )

  return contactId
}

Salesforce Integration

// lib/integrations/salesforce.ts
import jsforce from 'jsforce'

export class SalesforceIntegration {
  private conn: jsforce.Connection

  constructor() {
    this.conn = new jsforce.Connection({
      loginUrl: process.env.SALESFORCE_LOGIN_URL,
    })
  }

  async connect(): Promise<void> {
    await this.conn.login(
      process.env.SALESFORCE_USERNAME!,
      process.env.SALESFORCE_PASSWORD! + process.env.SALESFORCE_SECURITY_TOKEN!
    )
  }

  async createLead(data: {
    email: string
    firstName?: string
    lastName: string
    company: string
    phone?: string
    description?: string
    leadSource?: string
  }): Promise<string> {
    const result = await this.conn.sobject('Lead').create({
      Email: data.email,
      FirstName: data.firstName,
      LastName: data.lastName || 'Unknown',
      Company: data.company || 'Unknown',
      Phone: data.phone,
      Description: data.description,
      LeadSource: data.leadSource || 'Chatbot',
    })

    if (!result.success) {
      throw new Error(`Failed to create Salesforce lead: ${result.errors}`)
    }

    return result.id
  }

  async createTask(data: {
    whoId: string // Lead or Contact ID
    subject: string
    description: string
    priority?: 'High' | 'Normal' | 'Low'
    status?: string
  }): Promise<string> {
    const result = await this.conn.sobject('Task').create({
      WhoId: data.whoId,
      Subject: data.subject,
      Description: data.description,
      Priority: data.priority || 'Normal',
      Status: data.status || 'Not Started',
    })

    return result.id
  }

  async findContactByEmail(email: string): Promise<any> {
    const result = await this.conn.query(
      `SELECT Id, Name, Email FROM Contact WHERE Email = '${email}' LIMIT 1`
    )
    return result.records[0]
  }
}

Zendesk Integration (Support Tickets)

// lib/integrations/zendesk.ts
interface ZendeskTicket {
  subject: string
  description: string
  requester: {
    name?: string
    email: string
  }
  priority?: 'urgent' | 'high' | 'normal' | 'low'
  tags?: string[]
  custom_fields?: Array<{ id: number; value: string }>
}

export class ZendeskIntegration {
  private baseUrl: string

  constructor(
    private subdomain: string,
    private email: string,
    private apiToken: string
  ) {
    this.baseUrl = `https://${subdomain}.zendesk.com/api/v2`
  }

  private get authHeader(): string {
    const credentials = `${this.email}/token:${this.apiToken}`
    return `Basic ${Buffer.from(credentials).toString('base64')}`
  }

  private async request(
    method: string,
    endpoint: string,
    data?: any
  ): Promise<any> {
    const response = await fetch(`${this.baseUrl}${endpoint}`, {
      method,
      headers: {
        'Content-Type': 'application/json',
        Authorization: this.authHeader,
      },
      body: data ? JSON.stringify(data) : undefined,
    })

    if (!response.ok) {
      throw new Error(`Zendesk API error: ${response.status}`)
    }

    return response.json()
  }

  async createTicket(ticket: ZendeskTicket): Promise<number> {
    const result = await this.request('POST', '/tickets.json', {
      ticket: {
        subject: ticket.subject,
        comment: { body: ticket.description },
        requester: ticket.requester,
        priority: ticket.priority || 'normal',
        tags: ticket.tags || ['chatbot'],
      },
    })

    return result.ticket.id
  }

  async addCommentToTicket(ticketId: number, comment: string): Promise<void> {
    await this.request('PUT', `/tickets/${ticketId}.json`, {
      ticket: {
        comment: { body: comment, public: false },
      },
    })
  }
}

// Webhook handler for Zendesk
export async function handleHandoffForZendesk(
  event: WebhookEvent<{
    reason: string
    conversationHistory: Array<{ role: string; content: string }>
    visitorEmail?: string
    visitorName?: string
  }>
) {
  const zendesk = new ZendeskIntegration(
    process.env.ZENDESK_SUBDOMAIN!,
    process.env.ZENDESK_EMAIL!,
    process.env.ZENDESK_API_TOKEN!
  )

  const conversationText = event.data.conversationHistory
    .map(m => `${m.role}: ${m.content}`)
    .join('\n\n')

  const ticketId = await zendesk.createTicket({
    subject: `Chatbot Handoff: ${event.data.reason}`,
    description: `A visitor requested human assistance.

Reason: ${event.data.reason}

--- Conversation History ---
${conversationText}`,
    requester: {
      email: event.data.visitorEmail || 'anonymous@chatbot.local',
      name: event.data.visitorName,
    },
    priority: 'high',
    tags: ['chatbot', 'handoff'],
  })

  return ticketId
}

Custom Webhook Endpoints

Receiving Webhooks in Your Application

// app/api/webhooks/receive/route.ts (Next.js)
import { NextRequest, NextResponse } from 'next/server'
import { verifySignature } from '@/lib/webhooks/sender'

export async function POST(req: NextRequest) {
  const signature = req.headers.get('x-webhook-signature')
  const eventType = req.headers.get('x-webhook-event')

  if (!signature) {
    return NextResponse.json(
      { error: 'Missing signature' },
      { status: 401 }
    )
  }

  const body = await req.text()

  // Verify signature
  const isValid = verifySignature(
    body,
    signature,
    process.env.WEBHOOK_SECRET!
  )

  if (!isValid) {
    return NextResponse.json(
      { error: 'Invalid signature' },
      { status: 401 }
    )
  }

  const event = JSON.parse(body)

  // Process event based on type
  switch (eventType) {
    case 'conversation.started':
      await handleConversationStarted(event)
      break
    case 'lead.captured':
      await handleLeadCaptured(event)
      break
    case 'handoff.requested':
      await handleHandoffRequested(event)
      break
    default:
      console.log(`Unhandled event type: ${eventType}`)
  }

  return NextResponse.json({ received: true })
}

async function handleConversationStarted(event: any) {
  // Your custom logic
  console.log('New conversation:', event.conversationId)
}

async function handleLeadCaptured(event: any) {
  // Save to database, notify team, etc.
  console.log('Lead captured:', event.data.email)
}

async function handleHandoffRequested(event: any) {
  // Create support ticket, notify agents
  console.log('Handoff requested:', event.data.reason)
}

Webhook Management API

// app/api/webhooks/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { db } from '@/lib/database'

// List webhooks
export async function GET(req: NextRequest) {
  const chatbotId = req.nextUrl.searchParams.get('chatbotId')

  const webhooks = await db.webhook.findMany({
    where: { chatbotId: chatbotId! },
  })

  return NextResponse.json(webhooks)
}

// Create webhook
export async function POST(req: NextRequest) {
  const data = await req.json()

  // Generate secret
  const secret = crypto.randomUUID()

  const webhook = await db.webhook.create({
    data: {
      ...data,
      secret,
    },
  })

  return NextResponse.json(webhook, { status: 201 })
}

// app/api/webhooks/[id]/route.ts
// Update webhook
export async function PATCH(
  req: NextRequest,
  { params }: { params: { id: string } }
) {
  const data = await req.json()

  const webhook = await db.webhook.update({
    where: { id: params.id },
    data,
  })

  return NextResponse.json(webhook)
}

// Delete webhook
export async function DELETE(
  req: NextRequest,
  { params }: { params: { id: string } }
) {
  await db.webhook.delete({
    where: { id: params.id },
  })

  return new NextResponse(null, { status: 204 })
}

// Test webhook
// app/api/webhooks/[id]/test/route.ts
export async function POST(
  req: NextRequest,
  { params }: { params: { id: string } }
) {
  const webhook = await db.webhook.findUnique({
    where: { id: params.id },
  })

  if (!webhook) {
    return NextResponse.json(
      { error: 'Webhook not found' },
      { status: 404 }
    )
  }

  const testEvent = {
    id: 'test-' + crypto.randomUUID(),
    type: 'webhook.test',
    timestamp: new Date().toISOString(),
    data: { message: 'This is a test webhook delivery' },
  }

  const signature = signPayload(testEvent, webhook.secret)

  try {
    const response = await fetch(webhook.url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Webhook-Signature': signature,
        'X-Webhook-Event': 'webhook.test',
      },
      body: JSON.stringify(testEvent),
    })

    return NextResponse.json({
      success: response.ok,
      status: response.status,
      statusText: response.statusText,
    })
  } catch (error) {
    return NextResponse.json({
      success: false,
      error: error instanceof Error ? error.message : 'Unknown error',
    })
  }
}

Error Handling and Reliability

Retry Logic with Exponential Backoff

// lib/webhooks/retry.ts
interface RetryOptions {
  maxRetries: number
  initialDelayMs: number
  maxDelayMs: number
  backoffMultiplier: number
}

const defaultOptions: RetryOptions = {
  maxRetries: 5,
  initialDelayMs: 1000,
  maxDelayMs: 60000,
  backoffMultiplier: 2,
}

export async function withRetry<T>(
  fn: () => Promise<T>,
  options: Partial<RetryOptions> = {}
): Promise<T> {
  const opts = { ...defaultOptions, ...options }
  let lastError: Error | undefined
  let delay = opts.initialDelayMs

  for (let attempt = 0; attempt <= opts.maxRetries; attempt++) {
    try {
      return await fn()
    } catch (error) {
      lastError = error instanceof Error ? error : new Error(String(error))

      if (attempt === opts.maxRetries) {
        break
      }

      // Wait before retrying
      await new Promise(resolve => setTimeout(resolve, delay))

      // Increase delay with jitter
      delay = Math.min(
        opts.maxDelayMs,
        delay * opts.backoffMultiplier * (0.5 + Math.random())
      )
    }
  }

  throw lastError
}

Dead Letter Queue

// lib/webhooks/deadLetter.ts
interface FailedWebhook {
  id: string
  event: WebhookEvent
  webhookConfig: WebhookConfig
  error: string
  attempts: number
  lastAttempt: Date
}

export class DeadLetterQueue {
  private redis: Redis

  constructor(redis: Redis) {
    this.redis = redis
  }

  async add(failed: FailedWebhook): Promise<void> {
    await this.redis.lpush(
      'webhook:dlq',
      JSON.stringify(failed)
    )

    // Keep only last 1000 failed webhooks
    await this.redis.ltrim('webhook:dlq', 0, 999)
  }

  async list(limit = 50): Promise<FailedWebhook[]> {
    const items = await this.redis.lrange('webhook:dlq', 0, limit - 1)
    return items.map(item => JSON.parse(item))
  }

  async retry(id: string): Promise<boolean> {
    const items = await this.list(1000)
    const item = items.find(i => i.id === id)

    if (!item) return false

    // Re-queue for delivery
    await queueWebhookDelivery(item.event)

    // Remove from DLQ
    await this.redis.lrem('webhook:dlq', 1, JSON.stringify(item))

    return true
  }

  async purge(): Promise<number> {
    const count = await this.redis.llen('webhook:dlq')
    await this.redis.del('webhook:dlq')
    return count
  }
}

Webhook Health Monitoring

// lib/webhooks/monitoring.ts
interface WebhookHealth {
  webhookId: string
  url: string
  successRate: number
  avgLatencyMs: number
  lastSuccess?: Date
  lastFailure?: Date
  consecutiveFailures: number
}

export class WebhookMonitor {
  private redis: Redis

  constructor(redis: Redis) {
    this.redis = redis
  }

  async recordDelivery(
    webhookId: string,
    success: boolean,
    latencyMs: number
  ): Promise<void> {
    const key = `webhook:metrics:${webhookId}`
    const now = Date.now()

    await this.redis
      .multi()
      .hincrby(key, 'total', 1)
      .hincrby(key, success ? 'successes' : 'failures', 1)
      .lpush(`${key}:latencies`, latencyMs.toString())
      .ltrim(`${key}:latencies`, 0, 99) // Keep last 100
      .hset(key, success ? 'lastSuccess' : 'lastFailure', now)
      .exec()

    if (!success) {
      await this.redis.hincrby(key, 'consecutiveFailures', 1)
    } else {
      await this.redis.hset(key, 'consecutiveFailures', 0)
    }
  }

  async getHealth(webhookId: string): Promise<WebhookHealth | null> {
    const key = `webhook:metrics:${webhookId}`

    const [metrics, latencies] = await Promise.all([
      this.redis.hgetall(key),
      this.redis.lrange(`${key}:latencies`, 0, -1),
    ])

    if (!metrics.total) return null

    const total = parseInt(metrics.total)
    const successes = parseInt(metrics.successes || '0')

    const avgLatency = latencies.length
      ? latencies.reduce((a, b) => a + parseInt(b), 0) / latencies.length
      : 0

    return {
      webhookId,
      url: metrics.url || '',
      successRate: total > 0 ? successes / total : 0,
      avgLatencyMs: avgLatency,
      lastSuccess: metrics.lastSuccess
        ? new Date(parseInt(metrics.lastSuccess))
        : undefined,
      lastFailure: metrics.lastFailure
        ? new Date(parseInt(metrics.lastFailure))
        : undefined,
      consecutiveFailures: parseInt(metrics.consecutiveFailures || '0'),
    }
  }

  async checkAndAlert(webhookId: string): Promise<void> {
    const health = await this.getHealth(webhookId)
    if (!health) return

    // Alert on consecutive failures
    if (health.consecutiveFailures >= 5) {
      await this.sendAlert({
        type: 'consecutive_failures',
        webhookId,
        message: `Webhook ${webhookId} has failed ${health.consecutiveFailures} times consecutively`,
      })
    }

    // Alert on low success rate
    if (health.successRate < 0.9) {
      await this.sendAlert({
        type: 'low_success_rate',
        webhookId,
        message: `Webhook ${webhookId} success rate is ${(health.successRate * 100).toFixed(1)}%`,
      })
    }
  }

  private async sendAlert(alert: any): Promise<void> {
    // Send to monitoring service, Slack, PagerDuty, etc.
    console.warn('Webhook alert:', alert)
  }
}

Best Practices

Security

  1. Always verify signatures - Never trust unsigned webhooks
  2. Use HTTPS - Encrypt data in transit
  3. Rotate secrets - Periodically regenerate webhook secrets
  4. Validate payloads - Check event structure before processing
  5. Rate limit - Prevent webhook flooding

Reliability

  1. Use queues - Don't process webhooks synchronously
  2. Implement retries - With exponential backoff
  3. Monitor health - Track success rates and latencies
  4. Dead letter queues - Capture failed deliveries for analysis
  5. Idempotency - Handle duplicate deliveries gracefully

Performance

  1. Async processing - Respond quickly, process later
  2. Batch when possible - Combine related events
  3. Set timeouts - Don't wait forever for slow endpoints
  4. Cache configurations - Avoid database lookups per event

Webhooks transform your chatbot into an integrated part of your business workflow. By connecting to Slack, Discord, CRMs, and other tools, you ensure that every conversation creates value across your organization.

Author

About the author

Widget Chat is a team of developers and designers passionate about creating the best AI chatbot experience for Flutter, web, and mobile apps.

Comments

Comments are coming soon. We'd love to hear your thoughts!