Skip to main content
Business & GrowthCustomer Support Services346 lines

Tawk.to

"Tawk.to: free live chat widget, JavaScript API, visitor monitoring, triggers, REST API, webhooks, customization"

Quick Summary16 lines
Tawk.to is a free live chat platform focused on real-time visitor engagement. Integration should maximize the JavaScript API for frontend customization and visitor context, while using the REST API and webhooks for backend automation. Unlike paid platforms, Tawk.to provides unlimited agents and chat history at no cost, making it ideal for startups and small teams that need live chat without per-seat pricing. Design integrations that enrich visitor profiles with session data, use triggers for proactive engagement, and route chats through webhooks to external systems for CRM sync and analytics. The tradeoff is a simpler API surface compared to enterprise tools — plan accordingly.

## Key Points

- Always use the `visitor.hash` property in production to prevent identity spoofing. Generate the HMAC server-side using your Tawk.to API key and the visitor's email.
- Set custom attributes inside the `onLoad` callback to ensure the widget is ready before making API calls.
- Use tags to categorize visitors by plan, source, or intent so agents can prioritize high-value conversations.
- Store chat transcripts externally via webhooks for compliance and CRM integration since Tawk.to's built-in search is limited.
- Use `hideWidget()` on pages where chat is not appropriate (checkout, legal documents) rather than removing the script entirely.
- Monitor the `onStatusChange` callback to show fallback contact options when all agents go offline.
- Set the pre-chat form to collect email before the conversation starts so you can follow up on dropped chats.
- **Loading the widget script conditionally based on user state** — This causes the widget to flash in and out. Use `hideWidget()` and `showWidget()` to control visibility without reloading.
- **Relying on Tawk.to as a primary CRM** — Tawk.to stores visitor data but has limited query and export capabilities. Sync contacts to a dedicated CRM via webhooks for segmentation and campaigns.
- **Ignoring the offline state** — When no agents are online, the widget shows an offline form. If you do not monitor `ticket:create` webhooks, those offline messages are silently lost.
skilldb get customer-support-services-skills/Tawk.toFull skill: 346 lines
Paste into your CLAUDE.md or agent config

Tawk.to Integration

Core Philosophy

Tawk.to is a free live chat platform focused on real-time visitor engagement. Integration should maximize the JavaScript API for frontend customization and visitor context, while using the REST API and webhooks for backend automation. Unlike paid platforms, Tawk.to provides unlimited agents and chat history at no cost, making it ideal for startups and small teams that need live chat without per-seat pricing. Design integrations that enrich visitor profiles with session data, use triggers for proactive engagement, and route chats through webhooks to external systems for CRM sync and analytics. The tradeoff is a simpler API surface compared to enterprise tools — plan accordingly.

Setup

Embed the chat widget with the JavaScript API:

declare global {
  interface Window {
    Tawk_API: TawkAPI;
    Tawk_LoadStart: Date;
  }
}

interface TawkAPI {
  onLoad: () => void;
  onStatusChange: (status: string) => void;
  onChatStarted: () => void;
  onChatEnded: () => void;
  onPrechatSubmit: (data: Record<string, string>) => void;
  onOfflineSubmit: (data: Record<string, string>) => void;
  visitor: { name: string; email: string; hash?: string };
  setAttributes: (attrs: Record<string, string>, callback?: (error?: Error) => void) => void;
  addEvent: (name: string, metadata: Record<string, string>, callback?: (error?: Error) => void) => void;
  addTags: (tags: string[], callback?: (error?: Error) => void) => void;
  maximize: () => void;
  minimize: () => void;
  toggle: () => void;
  popup: () => void;
  hideWidget: () => void;
  showWidget: () => void;
  endChat: () => void;
  isChatMaximized: () => boolean;
  isChatMinimized: () => boolean;
  isChatHidden: () => boolean;
  isChatOngoing: () => boolean;
  isVisitorEngaged: () => boolean;
  getWindowType: () => string;
  getStatus: () => string;
}

function initTawk(propertyId: string, widgetId: string): void {
  window.Tawk_API = window.Tawk_API || ({} as TawkAPI);
  window.Tawk_LoadStart = new Date();

  const script = document.createElement("script");
  script.async = true;
  script.src = `https://embed.tawk.to/${propertyId}/${widgetId}`;
  script.charset = "utf-8";
  script.setAttribute("crossorigin", "*");
  document.head.appendChild(script);
}

function identifyVisitor(name: string, email: string, hash?: string): void {
  window.Tawk_API.visitor = { name, email, hash };
}

Set up the REST API client for backend operations:

import axios, { AxiosInstance } from "axios";

function createTawkClient(apiKey: string): AxiosInstance {
  return axios.create({
    baseURL: "https://api.tawk.to/v1",
    headers: {
      Authorization: `Bearer ${apiKey}`,
      "Content-Type": "application/json",
    },
    timeout: 10000,
  });
}

const tawkApi = createTawkClient(process.env.TAWK_API_KEY!);
const PROPERTY_ID = process.env.TAWK_PROPERTY_ID!;

Key Techniques

Visitor Context and Custom Attributes

function setVisitorContext(user: {
  id: string;
  plan: string;
  accountAge: number;
  lastLogin: string;
  totalSpend: number;
}): void {
  window.Tawk_API.onLoad = () => {
    window.Tawk_API.setAttributes(
      {
        userId: user.id,
        plan: user.plan,
        accountAgeDays: String(user.accountAge),
        lastLogin: user.lastLogin,
        totalSpend: String(user.totalSpend),
      },
      (error) => {
        if (error) console.error("Failed to set Tawk attributes:", error);
      }
    );

    window.Tawk_API.addTags(
      [user.plan, user.accountAge > 365 ? "veteran" : "newer-user"],
      (error) => {
        if (error) console.error("Failed to add tags:", error);
      }
    );
  };
}

function trackVisitorEvent(eventName: string, metadata: Record<string, string>): void {
  window.Tawk_API.addEvent(eventName, metadata, (error) => {
    if (error) console.error(`Failed to track event ${eventName}:`, error);
  });
}

// Usage: track when a user hits a paywall
trackVisitorEvent("paywall-hit", {
  feature: "advanced-reports",
  currentPlan: "free",
  page: "/reports/custom",
});

Widget Behavior Control

function setupConditionalWidget(user: { role: string; plan: string }): void {
  window.Tawk_API.onLoad = () => {
    // Hide widget for admin users who use internal tools
    if (user.role === "admin") {
      window.Tawk_API.hideWidget();
      return;
    }

    // Auto-open for free plan users on pricing page
    if (user.plan === "free" && window.location.pathname === "/pricing") {
      setTimeout(() => {
        if (!window.Tawk_API.isChatOngoing()) {
          window.Tawk_API.maximize();
        }
      }, 5000);
    }
  };
}

function setupChatEventHandlers(
  onStart: () => void,
  onEnd: (feedback?: Record<string, string>) => void
): void {
  window.Tawk_API.onChatStarted = () => {
    console.log("Chat started");
    onStart();
  };

  window.Tawk_API.onChatEnded = () => {
    console.log("Chat ended");
    onEnd();
  };

  window.Tawk_API.onPrechatSubmit = (data) => {
    console.log("Pre-chat form submitted:", data);
  };

  window.Tawk_API.onOfflineSubmit = (data) => {
    console.log("Offline message submitted:", data);
  };

  window.Tawk_API.onStatusChange = (status) => {
    console.log(`Agent status changed to: ${status}`);
    if (status === "offline") {
      // Show alternative contact method
      document.getElementById("email-support-fallback")?.classList.remove("hidden");
    }
  };
}

REST API Chat Operations

interface ChatMessage {
  id: string;
  sender: { type: "visitor" | "agent"; name: string };
  message: string;
  timestamp: string;
}

async function listChats(
  filters: { startDate?: string; endDate?: string; page?: number } = {}
): Promise<{ chats: unknown[]; total: number }> {
  const params: Record<string, string> = {};
  if (filters.startDate) params.startDate = filters.startDate;
  if (filters.endDate) params.endDate = filters.endDate;
  if (filters.page) params.page = String(filters.page);

  const { data } = await tawkApi.get(`/property/${PROPERTY_ID}/chats`, { params });
  return { chats: data.data, total: data.total };
}

async function getChatMessages(chatId: string): Promise<ChatMessage[]> {
  const { data } = await tawkApi.get(`/property/${PROPERTY_ID}/chat/${chatId}/messages`);
  return data.data.map((msg: Record<string, unknown>) => ({
    id: msg.id,
    sender: msg.sender,
    message: msg.message,
    timestamp: msg.time,
  }));
}

async function getChatTranscript(chatId: string): Promise<string> {
  const messages = await getChatMessages(chatId);
  return messages
    .map((m) => `[${m.timestamp}] ${m.sender.name} (${m.sender.type}): ${m.message}`)
    .join("\n");
}

Webhook Handler

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

function verifyTawkWebhook(req: Request, secret: string): boolean {
  const signature = req.headers["x-tawk-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), Buffer.from(digest));
}

interface TawkWebhookPayload {
  event: string;
  chatId: string;
  time: string;
  message?: { text: string; sender: { type: string } };
  visitor?: { name: string; email: string; city: string; country: string };
  property: { id: string; name: string };
}

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

  const payload = req.body as TawkWebhookPayload;

  switch (payload.event) {
    case "chat:start":
      console.log(`Chat started with ${payload.visitor?.name} from ${payload.visitor?.country}`);
      // Create lead in CRM
      break;
    case "chat:end":
      console.log(`Chat ${payload.chatId} ended`);
      // Archive transcript, update CRM
      break;
    case "ticket:create":
      console.log(`Offline ticket created by ${payload.visitor?.email}`);
      // Create ticket in external system
      break;
    default:
      console.log(`Unhandled event: ${payload.event}`);
  }

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

Visitor Monitoring and Proactive Engagement

async function getOnlineVisitors(): Promise<Array<{
  name: string;
  page: string;
  city: string;
  referrer: string;
}>> {
  const { data } = await tawkApi.get(`/property/${PROPERTY_ID}/visitors`);
  return data.data.map((v: Record<string, unknown>) => ({
    name: v.name ?? "Anonymous",
    page: v.currentPage,
    city: v.city,
    referrer: v.referrer,
  }));
}

// Frontend: proactive trigger based on user behavior
function setupProactiveTriggers(): void {
  let pageViewCount = 0;
  const startTime = Date.now();

  window.Tawk_API.onLoad = () => {
    pageViewCount++;

    // Trigger chat after visiting 3+ pages
    if (pageViewCount >= 3 && !window.Tawk_API.isChatOngoing()) {
      window.Tawk_API.maximize();
    }

    // Trigger chat after 60 seconds on page
    setTimeout(() => {
      if (!window.Tawk_API.isChatOngoing() && !window.Tawk_API.isChatHidden()) {
        window.Tawk_API.maximize();
      }
    }, 60000);
  };
}

// Secure visitor identity with HMAC
function generateVisitorHash(email: string, apiKey: string): string {
  return crypto.createHmac("sha256", apiKey).update(email).digest("hex");
}

Best Practices

  • Always use the visitor.hash property in production to prevent identity spoofing. Generate the HMAC server-side using your Tawk.to API key and the visitor's email.
  • Set custom attributes inside the onLoad callback to ensure the widget is ready before making API calls.
  • Use tags to categorize visitors by plan, source, or intent so agents can prioritize high-value conversations.
  • Store chat transcripts externally via webhooks for compliance and CRM integration since Tawk.to's built-in search is limited.
  • Use hideWidget() on pages where chat is not appropriate (checkout, legal documents) rather than removing the script entirely.
  • Monitor the onStatusChange callback to show fallback contact options when all agents go offline.
  • Set the pre-chat form to collect email before the conversation starts so you can follow up on dropped chats.

Anti-Patterns

  • Loading the widget script conditionally based on user state — This causes the widget to flash in and out. Use hideWidget() and showWidget() to control visibility without reloading.
  • Calling API methods before onLoad fires — The Tawk API is not available until the widget fully initializes. All calls to setAttributes, addTags, and addEvent must be inside or after onLoad.
  • Relying on Tawk.to as a primary CRM — Tawk.to stores visitor data but has limited query and export capabilities. Sync contacts to a dedicated CRM via webhooks for segmentation and campaigns.
  • Setting visitor identity client-side without HMAC — Without hash verification, any user can impersonate another by changing the name and email in JavaScript. Always generate and pass the hash from your server.
  • Using multiple widget instances on the same page — Loading the Tawk script twice creates duplicate chat windows and race conditions. Ensure the init function runs exactly once per page lifecycle.
  • Ignoring the offline state — When no agents are online, the widget shows an offline form. If you do not monitor ticket:create webhooks, those offline messages are silently lost.

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

Get CLI access →