Chatwoot + Webhooks: Build Custom Chatbots and Integrate with Your CRM
Customer support platforms are usually closed boxes. Messages come in, agents respond, tickets get closed. But what if you need to sync conversations to your CRM? Or trigger automated responses based on customer data? Or route tickets based on custom business logic?
Chatwoot solves this with webhooks and a comprehensive API. Every conversation event can trigger external systems. Every message can be enriched with data from your existing tools.
How Chatwoot Webhooks Work
Chatwoot fires webhooks for key events: new conversations, incoming messages, conversation status changes, and more. Configure them in Settings → Integrations → Webhooks.
Each webhook receives a JSON payload with the event type and relevant data:
{
"event": "message_created",
"id": 1234,
"content": "I need help with my order",
"message_type": "incoming",
"conversation": {
"id": 567,
"status": "open",
"contact": {
"id": 89,
"email": "customer@example.com",
"name": "Jane Smith"
}
},
"account": {
"id": 1
}
}
Your endpoint receives this payload and can respond with actions, sync data, or trigger workflows.
Webhook Signature Verification (Coming Soon)
Note: Per-webhook HMAC signatures have been merged into Chatwoot's development branch but are not yet available in a stable release. The headers and verification logic described below will ship in a future version. We're including it now so you can prepare your integration — skip this section if you're running a current stable release.
Once released, every delivery will include three security headers:
X-Chatwoot-Signature— HMAC-SHA256 signature of the payload using your webhook's unique secret (format:sha256=<hex>)X-Chatwoot-Timestamp— Unix timestamp (seconds) when the request was sent, included in the signed payload for replay protectionX-Chatwoot-Delivery— UUID that stays the same across Sidekiq retries, so you can deduplicate deliveries
Each webhook will get its own auto-generated secret. You'll find it in the API response when you create or retrieve a webhook — use it to verify that payloads actually came from your Chatwoot instance.
Here's how to verify signatures in Node.js once the feature lands:
const crypto = require('crypto');
function verifyWebhook(req, secret) {
const signature = req.headers['x-chatwoot-signature'];
const timestamp = req.headers['x-chatwoot-timestamp'];
const deliveryId = req.headers['x-chatwoot-delivery'];
if (!signature || !timestamp) return { valid: false };
// Reject requests older than 5 minutes to prevent replay attacks
const age = Math.floor(Date.now() / 1000) - parseInt(timestamp, 10);
if (age > 300) return { valid: false };
// Reconstruct the signed payload: "timestamp.body"
const signedPayload = `${timestamp}.${req.rawBody}`;
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
// Constant-time comparison to prevent timing attacks
const valid = crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
return { valid, deliveryId };
}
The raw body must be captured before JSON parsing — if you verify against JSON.stringify(req.body), key ordering or whitespace differences will break the signature. The 5-minute window stops old payloads from being replayed, and timingSafeEqual prevents timing-based attacks against the signature comparison.
To capture the raw body in Express:
app.use(express.json({
verify: (req, res, buf) => { req.rawBody = buf.toString(); }
}));
Building a Simple Webhook Handler
Here's a Node.js endpoint that receives Chatwoot webhooks and logs them:
const express = require('express');
const app = express();
app.use(express.json());
app.post('/chatwoot-webhook', (req, res) => {
const { event, content, conversation } = req.body;
console.log(`Event: ${event}`);
console.log(`Message: ${content}`);
console.log(`Contact: ${conversation?.contact?.email}`);
// Acknowledge receipt
res.status(200).json({ received: true });
});
app.listen(3000);
Syncing Conversations to Your CRM
A common integration: push new conversations to your CRM as leads. When a conversation starts, create a contact record with the customer details:
app.post('/chatwoot-webhook', async (req, res) => {
const { event, conversation } = req.body;
if (event === 'conversation_created') {
const contact = conversation.contact;
// Create lead in your CRM
await fetch('https://your-crm.com/api/leads', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.CRM_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: contact.name,
email: contact.email,
phone: contact.phone_number,
source: 'Chatwoot',
conversation_url: `https://your-chatwoot.com/app/accounts/1/conversations/${conversation.id}`
})
});
}
res.status(200).json({ synced: true });
});
Now every new customer conversation automatically appears in your sales pipeline.
Building Automated Bot Responses
Chatwoot's API lets you send messages programmatically. Combine this with webhooks to build custom bots:
const CHATWOOT_API = 'https://your-chatwoot.com/api/v1';
const API_KEY = process.env.CHATWOOT_API_KEY;
async function sendMessage(accountId, conversationId, content) {
await fetch(
`${CHATWOOT_API}/accounts/${accountId}/conversations/${conversationId}/messages`,
{
method: 'POST',
headers: {
'api_access_token': API_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({
content,
message_type: 'outgoing',
private: false
})
}
);
}
app.post('/chatwoot-webhook', async (req, res) => {
const { event, content, conversation, account } = req.body;
if (event === 'message_created' && req.body.message_type === 'incoming') {
// Auto-respond to common questions
if (content.toLowerCase().includes('hours')) {
await sendMessage(
account.id,
conversation.id,
"We're open Monday-Friday, 9 AM - 6 PM EST. How can I help you today?"
);
}
if (content.toLowerCase().includes('pricing')) {
await sendMessage(
account.id,
conversation.id,
"You can find our pricing at https://yoursite.com/pricing. Would you like me to connect you with sales?"
);
}
}
res.status(200).json({ processed: true });
});
This simple bot handles FAQs instantly, reducing response time for common queries.
Routing Based on Custom Logic
Route conversations to specific teams or agents based on external data. Check your database, verify subscription status, or apply any business logic:
app.post('/chatwoot-webhook', async (req, res) => {
const { event, conversation } = req.body;
if (event === 'conversation_created') {
const customerEmail = conversation.contact.email;
// Check subscription tier in your database
const customer = await db.query(
'SELECT subscription_tier FROM customers WHERE email = $1',
[customerEmail]
);
// Assign to appropriate team
let teamId;
if (customer?.subscription_tier === 'enterprise') {
teamId = ENTERPRISE_TEAM_ID; // Priority support
} else {
teamId = STANDARD_TEAM_ID;
}
// Update conversation assignment via API
await fetch(
`${CHATWOOT_API}/accounts/${conversation.account_id}/conversations/${conversation.id}/assignments`,
{
method: 'POST',
headers: { 'api_access_token': API_KEY },
body: JSON.stringify({ team_id: teamId })
}
);
}
res.status(200).json({ routed: true });
});
Enterprise customers automatically get priority routing. No manual triage needed.
Available Webhook Events
Chatwoot triggers webhooks for these events:
conversation_created— New conversation startedconversation_status_changed— Status update (open, resolved, pending)conversation_updated— Any conversation changemessage_created— New message (incoming or outgoing)message_updated— Message editedwebwidget_triggered— Chat widget interaction
Subscribe to the events relevant to your integration. Less noise, more signal.
Deploy on Elestio
Running Chatwoot with proper webhook infrastructure means managing SSL, database backups, and keeping the application updated. Elestio handles this out of the box.
Deploy Chatwoot in minutes, configure your webhooks, and start building integrations. Your customer conversations become part of your broader system instead of sitting in a silo.
Ready to connect your support platform to everything else? Deploy Chatwoot on Elestio and start building.
Thanks for reading!