Skip to main content
Business & GrowthCustomer Support Services305 lines

Crisp

"Crisp: live chat widget, chatbot scenarios, CRM contacts, campaigns, helpdesk, REST API, JavaScript SDK, webhooks"

Quick Summary18 lines
Crisp is a business messaging platform combining live chat, CRM, and a helpdesk into one unified inbox. Integration should leverage Crisp as both a real-time communication channel and a lightweight CRM — syncing contacts, automating responses with chatbot scenarios, and triggering campaigns based on user behavior. The REST API provides full programmatic control over conversations, contacts, and helpdesk articles. Design integrations that push context into Crisp (user plan, recent activity, error logs) so agents have full visibility without switching tools. Use webhooks to react to conversation events in real time, and the JavaScript SDK to control widget behavior on the frontend.

## Key Points

- Push session data (plan tier, account age, recent errors) into Crisp so agents see full context without leaving the inbox.
- Use conversation segments to categorize tickets (billing, technical, onboarding) for reporting and routing.
- Implement webhook retry handling — Crisp retries failed deliveries up to 5 times with exponential backoff.
- Prefer the plugin authentication tier over basic token auth for production apps; it supports multi-website access.
- Use `message:send` webhooks (outbound from user) for auto-responses and `message:received` (from operator) for delivery tracking.
- Rate limits are 30 requests per minute for most endpoints — batch operations and cache contact lookups.
- Store `people_id` alongside your user records to avoid repeated email lookups.
- **Initializing the widget on every route change in SPAs** — The Crisp loader script should be loaded once. Calling `initCrisp()` repeatedly creates duplicate iframes and memory leaks.
- **Using REST API for real-time messaging** — The REST API has latency and rate limits. Use the RTM (real-time messaging) WebSocket API or webhooks for live interactions.
- **Overwriting session data on every page load** — Use `session:data` merges, not full replacements. Overwriting clears context that agents may have manually added.
- **Ignoring conversation state transitions** — Always check current state before changing it. Resolving an already-resolved conversation or reopening a closed one creates confusing audit trails.
- **Sending automated messages without throttling** — Crisp flags accounts that send high volumes of operator messages programmatically. Use campaigns or chatbot scenarios for bulk outreach instead.
skilldb get customer-support-services-skills/CrispFull skill: 305 lines
Paste into your CLAUDE.md or agent config

Crisp Integration

Core Philosophy

Crisp is a business messaging platform combining live chat, CRM, and a helpdesk into one unified inbox. Integration should leverage Crisp as both a real-time communication channel and a lightweight CRM — syncing contacts, automating responses with chatbot scenarios, and triggering campaigns based on user behavior. The REST API provides full programmatic control over conversations, contacts, and helpdesk articles. Design integrations that push context into Crisp (user plan, recent activity, error logs) so agents have full visibility without switching tools. Use webhooks to react to conversation events in real time, and the JavaScript SDK to control widget behavior on the frontend.

Setup

Install the Node SDK and configure authentication:

import Crisp from "crisp-api";

const crispClient = new Crisp();
crispClient.authenticateTier(
  "plugin",
  process.env.CRISP_PLUGIN_ID!,
  process.env.CRISP_PLUGIN_KEY!
);

const WEBSITE_ID = process.env.CRISP_WEBSITE_ID!;

// Verify connection
async function verifyConnection(): Promise<void> {
  const website = await crispClient.website.getWebsite(WEBSITE_ID);
  console.log(`Connected to: ${website.name} (${website.domain})`);
}

Embed the chat widget on your frontend:

declare global {
  interface Window {
    $crisp: unknown[];
    CRISP_WEBSITE_ID: string;
  }
}

function initCrisp(websiteId: string): void {
  window.$crisp = [];
  window.CRISP_WEBSITE_ID = websiteId;

  const script = document.createElement("script");
  script.src = "https://client.crisp.chat/l.js";
  script.async = true;
  document.head.appendChild(script);
}

function setCrispUser(email: string, nickname: string, data: Record<string, string>): void {
  window.$crisp.push(["set", "user:email", [email]]);
  window.$crisp.push(["set", "user:nickname", [nickname]]);
  for (const [key, value] of Object.entries(data)) {
    window.$crisp.push(["set", "session:data", [[key, value]]]);
  }
}

function openCrispChat(): void {
  window.$crisp.push(["do", "chat:open"]);
}

function sendCrispMessage(text: string): void {
  window.$crisp.push(["do", "message:send", ["text", text]]);
}

function hideCrisp(): void {
  window.$crisp.push(["do", "chat:hide"]);
}

function onCrispMessageReceived(callback: (message: { content: string }) => void): void {
  window.$crisp.push(["on", "message:received", callback]);
}

Key Techniques

Managing Conversations

interface ConversationFilters {
  status?: "pending" | "unresolved" | "resolved";
  pageNumber?: number;
}

async function listConversations(filters: ConversationFilters = {}): Promise<unknown[]> {
  const conversations = await crispClient.website.listConversations(
    WEBSITE_ID,
    filters.pageNumber ?? 1
  );
  if (filters.status) {
    return conversations.filter((c: { state: string }) => c.state === filters.status);
  }
  return conversations;
}

async function createConversation(userEmail: string, initialMessage: string): Promise<string> {
  const conversation = await crispClient.website.createNewConversation(WEBSITE_ID);
  const sessionId = conversation.session_id;

  await crispClient.website.updateConversationMetas(WEBSITE_ID, sessionId, {
    email: userEmail,
    segments: ["api-created"],
  });

  await crispClient.website.sendMessageInConversation(WEBSITE_ID, sessionId, {
    type: "text",
    from: "operator",
    origin: "chat",
    content: initialMessage,
  });

  return sessionId;
}

async function resolveConversation(sessionId: string): Promise<void> {
  await crispClient.website.changeConversationState(WEBSITE_ID, sessionId, "resolved");
}

async function addNoteToConversation(sessionId: string, note: string): Promise<void> {
  await crispClient.website.sendMessageInConversation(WEBSITE_ID, sessionId, {
    type: "note",
    from: "operator",
    origin: "chat",
    content: note,
  });
}

CRM Contact Management

interface CrispContact {
  email: string;
  nickname?: string;
  avatar?: string;
  company?: { name: string; url?: string };
  segments?: string[];
  data?: Record<string, string>;
}

async function upsertContact(contact: CrispContact): Promise<string> {
  const people = await crispClient.website.findPeopleWithEmail(WEBSITE_ID, contact.email);

  if (people && people.length > 0) {
    const personId = people[0].people_id;
    await crispClient.website.updatePeopleProfile(WEBSITE_ID, personId, {
      email: contact.email,
      person: { nickname: contact.nickname },
      company: contact.company,
    });
    if (contact.segments) {
      await crispClient.website.updatePeopleData(WEBSITE_ID, personId, {
        segments: contact.segments,
      });
    }
    return personId;
  }

  const created = await crispClient.website.addNewPeopleProfile(WEBSITE_ID, {
    email: contact.email,
    person: { nickname: contact.nickname },
    company: contact.company,
  });
  return created.people_id;
}

async function getContactConversations(email: string): Promise<unknown[]> {
  const people = await crispClient.website.findPeopleWithEmail(WEBSITE_ID, email);
  if (!people || people.length === 0) return [];
  return crispClient.website.listPeopleConversations(WEBSITE_ID, people[0].people_id, 1);
}

async function addContactEvent(email: string, eventText: string, eventData?: Record<string, unknown>): Promise<void> {
  const people = await crispClient.website.findPeopleWithEmail(WEBSITE_ID, email);
  if (!people || people.length === 0) return;
  await crispClient.website.addPeopleEvent(WEBSITE_ID, people[0].people_id, {
    text: eventText,
    data: eventData ?? {},
    color: "blue",
  });
}

Webhook Handler

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

function verifyCrispSignature(req: Request, secret: string): boolean {
  const timestamp = req.headers["x-crisp-request-timestamp"] as string;
  const signature = req.headers["x-crisp-signature"] as string;
  if (!timestamp || !signature) return false;

  const body = JSON.stringify(req.body);
  const payload = `${timestamp}${body}`;
  const expected = crypto.createHmac("sha256", secret).update(payload).digest("hex");
  return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}

interface CrispWebhookPayload {
  event: string;
  data: {
    website_id: string;
    session_id?: string;
    content?: string;
    from?: string;
    user?: { email?: string; nickname?: string };
  };
  timestamp: number;
}

function handleCrispWebhook(req: Request, res: Response): void {
  if (!verifyCrispSignature(req, process.env.CRISP_WEBHOOK_SECRET!)) {
    res.status(403).json({ error: "Invalid signature" });
    return;
  }

  const payload = req.body as CrispWebhookPayload;

  switch (payload.event) {
    case "message:send":
      if (payload.data.from === "user") {
        console.log(`User message in ${payload.data.session_id}: ${payload.data.content}`);
      }
      break;
    case "message:received":
      console.log(`Operator replied in ${payload.data.session_id}`);
      break;
    case "session:set_state":
      console.log(`Conversation ${payload.data.session_id} state changed`);
      break;
    case "session:set_data":
      console.log(`Session data updated for ${payload.data.session_id}`);
      break;
    default:
      console.log(`Unhandled event: ${payload.event}`);
  }

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

Helpdesk Articles

async function createHelpdeskArticle(
  title: string,
  content: string,
  category: string,
  locale: string = "en"
): Promise<string> {
  const article = await crispClient.website.createHelpdeskArticle(WEBSITE_ID, {
    title,
    content,
    category,
    status: "draft",
    order: 1,
    locale,
  });
  return article.article_id;
}

async function publishArticle(articleId: string): Promise<void> {
  await crispClient.website.updateHelpdeskArticle(WEBSITE_ID, articleId, {
    status: "published",
  });
}

async function searchHelpdesk(query: string): Promise<Array<{ id: string; title: string }>> {
  const results = await crispClient.website.searchHelpdeskArticles(WEBSITE_ID, {
    query,
    locale: "en",
  });
  return results.map((a: { article_id: string; title: string }) => ({
    id: a.article_id,
    title: a.title,
  }));
}

Best Practices

  • Push session data (plan tier, account age, recent errors) into Crisp so agents see full context without leaving the inbox.
  • Use conversation segments to categorize tickets (billing, technical, onboarding) for reporting and routing.
  • Implement webhook retry handling — Crisp retries failed deliveries up to 5 times with exponential backoff.
  • Prefer the plugin authentication tier over basic token auth for production apps; it supports multi-website access.
  • Use message:send webhooks (outbound from user) for auto-responses and message:received (from operator) for delivery tracking.
  • Rate limits are 30 requests per minute for most endpoints — batch operations and cache contact lookups.
  • Store people_id alongside your user records to avoid repeated email lookups.

Anti-Patterns

  • Initializing the widget on every route change in SPAs — The Crisp loader script should be loaded once. Calling initCrisp() repeatedly creates duplicate iframes and memory leaks.
  • Using REST API for real-time messaging — The REST API has latency and rate limits. Use the RTM (real-time messaging) WebSocket API or webhooks for live interactions.
  • Overwriting session data on every page load — Use session:data merges, not full replacements. Overwriting clears context that agents may have manually added.
  • Ignoring conversation state transitions — Always check current state before changing it. Resolving an already-resolved conversation or reopening a closed one creates confusing audit trails.
  • Sending automated messages without throttling — Crisp flags accounts that send high volumes of operator messages programmatically. Use campaigns or chatbot scenarios for bulk outreach instead.
  • Hardcoding website IDs in client-side code — Website IDs are not secret, but embedding them in multiple places creates maintenance burden. Use environment variables consistently.

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

Get CLI access →