From intercom-pack
Handles Intercom incoming webhooks with HMAC-SHA1 signature verification in Node.js/Express and tracks custom data events via Events API.
npx claudepluginhub jeremylongshore/claude-code-plugins-plus-skills --plugin intercom-packThis skill is limited to using the following tools:
Handle incoming Intercom webhooks (notifications) with signature verification and implement outbound data event tracking via the Events API.
Optimizes Intercom API costs with usage auditing, caching, request reduction via webhooks, and monitoring to avoid rate limits and infrastructure overhead.
Handles Instantly.ai v2 webhook events for email campaigns (sent, opened, clicked, replied, bounced) and lead updates. Use for webhook endpoints or CRM sync pipelines.
Implements Klaviyo webhooks with HMAC-SHA256 signature verification, event handling, idempotency, and API subscriptions for profile/list/segment/campaign/flow events.
Share bugs, ideas, or general feedback.
Handle incoming Intercom webhooks (notifications) with signature verification and implement outbound data event tracking via the Events API.
intercom-client SDK installedIntercom 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);
}
);
// 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" }] }
// }
// }
// }
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
}
}
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;
}
}
Submit custom events to track contact activity in Intercom.
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"
// 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 };
}
| 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 |
| 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 |
For performance optimization, see intercom-performance-tuning.