Skip to main content
Business & GrowthCustomer Support Services262 lines

Intercom

"Intercom: Messenger widget, conversations API, custom bots, product tours, help center, user/company data, events, webhooks, Node SDK"

Quick Summary18 lines
Intercom is a customer communications platform that unifies messaging, support, and engagement into a single system. Integration should treat Intercom as the central hub for customer interactions — connecting the Messenger widget for real-time chat, the Conversations API for programmatic ticket management, and webhooks for event-driven workflows. Design integrations that enrich user and company data continuously, automate repetitive support tasks with custom bots, and leverage product tours and help center content to reduce inbound volume. Always authenticate via OAuth or access tokens, handle rate limits gracefully, and use webhook signatures to verify payloads.

## Key Points

- Store `external_id` mapping between your database and Intercom contacts so you can sync bidirectionally without duplicates.
- Use conversation tags and custom attributes to route tickets automatically via Intercom's assignment rules.
- Batch event tracking calls where possible — Intercom rate-limits to 500 requests per minute per app.
- Always verify webhook signatures before processing payloads to prevent spoofing.
- Use `updated_at` filters when polling for changes instead of fetching all records.
- Set `custom_launcher_selector` to control when the Messenger opens rather than showing it on every page.
- Keep custom attribute names under 190 characters and values under 255 characters.
- **Polling conversations in a loop** — Use webhooks for real-time updates instead of repeatedly calling the list endpoint. Polling wastes quota and introduces latency.
- **Storing Intercom IDs as your primary key** — Intercom IDs can change during merges. Always use your own `external_id` as the canonical reference.
- **Sending PII in event metadata** — Event metadata is visible to all workspace admins. Never include passwords, tokens, or sensitive personal data.
- **Ignoring rate limit headers** — The API returns `X-RateLimit-Remaining` and `X-RateLimit-Limit`. Implement exponential backoff when approaching the limit rather than retrying blindly.
- **Creating contacts without email or external_id** — Orphaned leads clutter the workspace and cannot be merged later. Always provide at least one identifier.
skilldb get customer-support-services-skills/IntercomFull skill: 262 lines
Paste into your CLAUDE.md or agent config

Intercom Integration

Core Philosophy

Intercom is a customer communications platform that unifies messaging, support, and engagement into a single system. Integration should treat Intercom as the central hub for customer interactions — connecting the Messenger widget for real-time chat, the Conversations API for programmatic ticket management, and webhooks for event-driven workflows. Design integrations that enrich user and company data continuously, automate repetitive support tasks with custom bots, and leverage product tours and help center content to reduce inbound volume. Always authenticate via OAuth or access tokens, handle rate limits gracefully, and use webhook signatures to verify payloads.

Setup

Install the Node SDK and configure credentials:

import Intercom from "intercom-client";

const client = new Intercom.Client({
  tokenAuth: { token: process.env.INTERCOM_ACCESS_TOKEN! },
});

// Verify connection
async function verifyConnection(): Promise<void> {
  const admin = await client.admins.me();
  console.log(`Connected as: ${admin.name} (${admin.email})`);
}

Embed the Messenger widget in your frontend:

// Initialize Intercom Messenger on the client side
declare global {
  interface Window {
    Intercom: (...args: unknown[]) => void;
    intercomSettings: Record<string, unknown>;
  }
}

function bootIntercom(user: { id: string; name: string; email: string; createdAt: number }): void {
  window.intercomSettings = {
    api_base: "https://api-eus.intercom.io",
    app_id: process.env.NEXT_PUBLIC_INTERCOM_APP_ID!,
    user_id: user.id,
    name: user.name,
    email: user.email,
    created_at: user.createdAt,
    custom_launcher_selector: "#open-intercom",
  };
  window.Intercom("boot", window.intercomSettings);
}

function updateIntercomUser(attributes: Record<string, unknown>): void {
  window.Intercom("update", attributes);
}

function shutdownIntercom(): void {
  window.Intercom("shutdown");
}

Key Techniques

Managing Contacts and Companies

interface ContactPayload {
  role: "user" | "lead";
  externalId: string;
  email: string;
  name: string;
  customAttributes?: Record<string, unknown>;
}

async function upsertContact(payload: ContactPayload): Promise<string> {
  try {
    const contact = await client.contacts.createUser({
      externalId: payload.externalId,
      email: payload.email,
      name: payload.name,
      customAttributes: payload.customAttributes ?? {},
    });
    return contact.id;
  } catch (err: unknown) {
    if ((err as { statusCode?: number }).statusCode === 409) {
      const existing = await client.contacts.search({
        data: {
          query: { field: "external_id", operator: "=", value: payload.externalId },
        },
      });
      const found = existing.data[0];
      if (found) {
        await client.contacts.update({ id: found.id, name: payload.name, customAttributes: payload.customAttributes });
        return found.id;
      }
    }
    throw err;
  }
}

async function attachContactToCompany(contactId: string, companyName: string, companyId: string): Promise<void> {
  await client.companies.create({ companyId, name: companyName, plan: "business" });
  await client.contacts.attachCompany({ id: contactId, companyId });
}

Conversations API

async function createConversation(fromContactId: string, body: string): Promise<string> {
  const conversation = await client.conversations.create({
    from: { type: "user", id: fromContactId },
    body,
  });
  return conversation.conversationId;
}

async function replyToConversation(
  conversationId: string,
  adminId: string,
  message: string
): Promise<void> {
  await client.conversations.replyByIdAsAdmin({
    id: conversationId,
    adminId,
    messageType: "comment",
    body: message,
  });
}

async function closeConversation(conversationId: string, adminId: string): Promise<void> {
  await client.conversations.close(conversationId, { adminId, body: "Resolved. Closing ticket." });
}

async function searchConversations(query: string): Promise<void> {
  const results = await client.conversations.search({
    data: {
      query: {
        operator: "AND",
        value: [
          { field: "open", operator: "=", value: true },
          { field: "statistics.last_contact_reply_at", operator: ">", value: Math.floor(Date.now() / 1000) - 86400 },
        ],
      },
    },
  });
  for (const convo of results.data) {
    console.log(`#${convo.id} — ${convo.statistics?.lastContactReplyAt}`);
  }
}

Tracking Events

async function trackEvent(contactId: string, eventName: string, metadata?: Record<string, unknown>): Promise<void> {
  await client.events.create({
    eventName,
    userId: contactId,
    createdAt: Math.floor(Date.now() / 1000),
    metadata,
  });
}

// Example: track a purchase event
await trackEvent("user_abc", "completed-purchase", {
  order_id: "ORD-12345",
  amount: 99.99,
  currency: "USD",
  plan: "pro",
});

Webhook Handler

import crypto from "node:crypto";
import type { Request, Response } from "express";

function verifyIntercomWebhook(req: Request, secret: string): boolean {
  const signature = req.headers["x-hub-signature"] as string;
  if (!signature) return false;
  const digest = crypto.createHmac("sha1", secret).update(JSON.stringify(req.body)).digest("hex");
  return crypto.timingSafeEqual(Buffer.from(signature, "utf8"), Buffer.from(`sha1=${digest}`, "utf8"));
}

function handleIntercomWebhook(req: Request, res: Response): void {
  if (!verifyIntercomWebhook(req, process.env.INTERCOM_WEBHOOK_SECRET!)) {
    res.status(401).json({ error: "Invalid signature" });
    return;
  }

  const topic = req.body.topic as string;
  const data = req.body.data?.item;

  switch (topic) {
    case "conversation.user.created":
      console.log(`New conversation from user ${data?.user?.id}`);
      break;
    case "conversation.user.replied":
      console.log(`User replied to conversation ${data?.id}`);
      break;
    case "conversation.admin.closed":
      console.log(`Conversation ${data?.id} closed`);
      break;
    case "user.created":
      console.log(`New user: ${data?.email}`);
      break;
    default:
      console.log(`Unhandled topic: ${topic}`);
  }

  res.status(200).json({ received: true });
}

Help Center Articles

async function createHelpArticle(
  title: string,
  body: string,
  collectionId: string,
  authorId: number
): Promise<string> {
  const article = await client.articles.create({
    title,
    body,
    authorId,
    parentId: collectionId,
    parentType: "collection",
    state: "draft",
  });
  return article.id;
}

async function searchArticles(query: string): Promise<Array<{ id: string; title: string }>> {
  const results = await client.articles.search({ phrase: query, state: "published" });
  return results.data.articles.map((a) => ({ id: a.id, title: a.title }));
}

Best Practices

  • Store external_id mapping between your database and Intercom contacts so you can sync bidirectionally without duplicates.
  • Use conversation tags and custom attributes to route tickets automatically via Intercom's assignment rules.
  • Batch event tracking calls where possible — Intercom rate-limits to 500 requests per minute per app.
  • Always verify webhook signatures before processing payloads to prevent spoofing.
  • Use updated_at filters when polling for changes instead of fetching all records.
  • Set custom_launcher_selector to control when the Messenger opens rather than showing it on every page.
  • Keep custom attribute names under 190 characters and values under 255 characters.

Anti-Patterns

  • Polling conversations in a loop — Use webhooks for real-time updates instead of repeatedly calling the list endpoint. Polling wastes quota and introduces latency.
  • Storing Intercom IDs as your primary key — Intercom IDs can change during merges. Always use your own external_id as the canonical reference.
  • Sending PII in event metadata — Event metadata is visible to all workspace admins. Never include passwords, tokens, or sensitive personal data.
  • Ignoring rate limit headers — The API returns X-RateLimit-Remaining and X-RateLimit-Limit. Implement exponential backoff when approaching the limit rather than retrying blindly.
  • Creating contacts without email or external_id — Orphaned leads clutter the workspace and cannot be merged later. Always provide at least one identifier.
  • Hardcoding admin IDs — Admin IDs change when team members leave. Fetch admin lists dynamically or use assignment rules instead.

Install this skill directly: skilldb add customer-support-services-skills

Get CLI access →