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
- Go to Slack API
- Create new app → From scratch
- Add "Incoming Webhooks" feature
- Activate and add to workspace
- 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
- Server Settings → Integrations → Webhooks
- New Webhook
- 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
- Always verify signatures - Never trust unsigned webhooks
- Use HTTPS - Encrypt data in transit
- Rotate secrets - Periodically regenerate webhook secrets
- Validate payloads - Check event structure before processing
- Rate limit - Prevent webhook flooding
Reliability
- Use queues - Don't process webhooks synchronously
- Implement retries - With exponential backoff
- Monitor health - Track success rates and latencies
- Dead letter queues - Capture failed deliveries for analysis
- Idempotency - Handle duplicate deliveries gracefully
Performance
- Async processing - Respond quickly, process later
- Batch when possible - Combine related events
- Set timeouts - Don't wait forever for slow endpoints
- 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.



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