Intercom Webhooks & Events
Overview
Handle incoming Intercom webhooks (notifications) with signature verification and implement outbound data event tracking via the Events API.
Prerequisites
- HTTPS endpoint accessible from internet
- Webhook secret from Intercom Developer Hub
intercom-clientSDK installed- Redis or database for idempotency (recommended)
Part 1: Incoming Webhooks (Notifications)
Step 1: Webhook Endpoint with Signature Verification
Intercom signs webhooks with HMAC-SHA1 via the X-Hub-Signature header.
import express from "express";
import crypto from "crypto";
const app = express();
// IMPORTANT: Use raw body for signature verification
app.post(
"/webhooks/intercom",
express.raw({ type: "application/json" }),
async (req, res) => {
const signature = req.headers["x-hub-signature"] as string;
const secret = process.env.INTERCOM_WEBHOOK_SECRET!;
if (!signature) {
return res.status(401).json({ error: "Missing X-Hub-Signature" });
}
// Verify HMAC-SHA1 signature
const expected = "sha1=" + crypto
.createHmac("sha1", secret)
.update(req.body)
.digest("hex");
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
console.error("Webhook signature verification failed");
return res.status(401).json({ error: "Invalid signature" });
}
// MUST respond within 5 seconds or Intercom treats as failure
// Parse and queue for async processing
const notification = JSON.parse(req.body.toString());
res.status(200).json({ received: true });
// Process asynchronously after responding
processWebhookAsync(notification).catch(console.error);
}
);
Step 2: Notification Payload Shape
// Every Intercom webhook notification follows this structure
interface IntercomNotification {
type: "notification_event";
id: string; // Unique notification ID
topic: string; // e.g., "conversation.user.created"
app_id: string; // Your app ID
created_at: number; // Unix timestamp
delivery_attempts: number; // 1 on first try, 2 on retry
data: {
type: "notification_event_data";
item: any; // The actual resource (conversation, contact, etc.)
};
}
// Example: conversation.user.created
// {
// "type": "notification_event",
// "id": "notif_abc123",
// "topic": "conversation.user.created",
// "created_at": 1711100000,
// "data": {
// "type": "notification_event_data",
// "item": {
// "type": "conversation",
// "id": "123",
// "state": "open",
// "source": { "body": "Hi, I need help!" },
// "contacts": { "contacts": [{ "id": "contact-1", "type": "contact" }] }
// }
// }
// }
Step 3: Topic-Based Event Router
type WebhookHandler = (data: any) => Promise<void>;
const handlers: Record<string, WebhookHandler> = {
"conversation.user.created": async (data) => {
const conversation = data.item;
console.log(`New conversation: ${conversation.id}`);
// Notify support channel, auto-assign, etc.
},
"conversation.user.replied": async (data) => {
const conversation = data.item;
console.log(`Customer replied to: ${conversation.id}`);
// Update ticket system, escalate if needed
},
"conversation.admin.closed": async (data) => {
const conversation = data.item;
console.log(`Conversation closed: ${conversation.id}`);
// Send satisfaction survey, update CRM
},
"contact.created": async (data) => {
const contact = data.item;
console.log(`New contact: ${contact.id} (${contact.email})`);
// Sync to CRM, enrich data, trigger welcome flow
},
"contact.tag.created": async (data) => {
const contact = data.item;
console.log(`Contact tagged: ${contact.id}`);
// Trigger automation based on tag
},
};
async function processWebhookAsync(notification: IntercomNotification): Promise<void> {
const handler = handlers[notification.topic];
if (!handler) {
console.log(`Unhandled topic: ${notification.topic}`);
return;
}
try {
await handler(notification.data);
console.log(`Processed ${notification.topic}: ${notification.id}`);
} catch (error) {
console.error(`Failed ${notification.topic}: ${notification.id}`, error);
// Dead-letter queue for failed events
}
}
Step 4: Idempotency (Prevent Duplicate Processing)
Intercom retries failed webhooks once after 1 minute. Guard against duplicates:
import { Redis } from "ioredis";
const redis = new Redis(process.env.REDIS_URL);
async function processIdempotent(
notification: IntercomNotification,
handler: () => Promise<void>
): Promise<void> {
const key = `intercom:webhook:${notification.id}`;
// SET NX: only succeeds if key doesn't exist
const acquired = await redis.set(key, "processing", "EX", 86400 * 7, "NX");
if (!acquired) {
console.log(`Duplicate webhook skipped: ${notification.id}`);
return;
}
try {
await handler();
await redis.set(key, "completed", "EX", 86400 * 7);
} catch (error) {
await redis.del(key); // Allow retry on failure
throw error;
}
}
Part 2: Outbound Data Events
Submit custom events to track contact activity in Intercom.
Step 5: Track Data Events
import { IntercomClient } from "intercom-client";
const client = new IntercomClient({
token: process.env.INTERCOM_ACCESS_TOKEN!,
});
// Submit a data event
await client.dataEvents.create({
eventName: "completed-onboarding",
createdAt: Math.floor(Date.now() / 1000),
userId: "user-12345", // External ID of the contact
metadata: {
steps_completed: 5,
time_to_complete_minutes: 12,
plan: "pro",
},
});
// Event naming convention: past-tense verb-noun
// Good: "placed-order", "upgraded-plan", "invited-teammate"
// Bad: "order", "click", "page_view"
Step 6: Bulk Event Submission
// Submit events for multiple users efficiently
async function trackBulkEvents(
client: IntercomClient,
events: Array<{ userId: string; eventName: string; metadata?: Record<string, any> }>
): Promise<{ succeeded: number; failed: number }> {
let succeeded = 0;
let failed = 0;
// Intercom doesn't have a batch events endpoint; throttle individual calls
for (const event of events) {
try {
await client.dataEvents.create({
eventName: event.eventName,
createdAt: Math.floor(Date.now() / 1000),
userId: event.userId,
metadata: event.metadata,
});
succeeded++;
// Rate limit: slight delay between calls
if (succeeded % 50 === 0) {
await new Promise(r => setTimeout(r, 500));
}
} catch (error) {
failed++;
console.error(`Failed to track event for ${event.userId}:`, error);
}
}
return { succeeded, failed };
}
Available Webhook Topics
| Topic | Description |
|-------|-------------|
| conversation.user.created | New conversation from contact |
| conversation.user.replied | Contact replies to conversation |
| conversation.admin.replied | Admin replies to conversation |
| conversation.admin.closed | Conversation closed by admin |
| conversation.admin.opened | Conversation reopened |
| conversation.admin.snoozed | Conversation snoozed |
| conversation.admin.assigned | Conversation reassigned |
| contact.created | New contact created |
| contact.signed_up | Lead converts to user |
| contact.tag.created | Tag applied to contact |
| contact.tag.deleted | Tag removed from contact |
| visitor.signed_up | Visitor becomes lead |
Error Handling
| Issue | Cause | Solution | |-------|-------|----------| | Invalid signature | Secret mismatch | Verify secret matches Developer Hub | | Timeout (5s) | Slow processing | Queue events, respond immediately | | Duplicate events | Retry delivery | Implement idempotency with Redis | | Missing topic handler | New topic added | Log unhandled topics, add handler | | Event rejected (422) | Invalid user_id or event_name | Verify contact exists, use verb-noun naming |
Resources
Next Steps
For performance optimization, see intercom-performance-tuning.