Embedding the chat (iframe)

    8 min read
    ·
    332 views
    ·
    Updated 10/21/2026
    🚀 Quick start:
    1. Get your API key in the WazzaBee dashboard → Connectors → API
    2. From your backend, send a POST request to /v1/iframe with the API key
    3. Put the URL you get back into an <iframe> on your page
    4. Done! Your agent sees every conversation right inside your app

    Two ways to use it

    ModescopeWhere to use it
    All conversationsglobalA dedicated “Chats” or “Messengers” page in your app's menu
    Customer chatcardThe customer card: shows only the conversation with that customer

    POST /v1/iframe: embedding the WazzaBee chat

    This endpoint lets you embed the WazzaBee chat into your CRM or any web app through an <iframe>. It is a perfect fit for custom CRMs built on React, Vue, Angular or any other framework.

    💡 How it works:
    1. Your backend requests an iframe session through the API
    2. The API returns a URL to embed
    3. Your frontend renders an <iframe> with that URL
    4. The user gets the full WazzaBee chat inside your app

    URL

    POST https://api.wazzabee.com/api-gateway/v1/iframe

    Headers

    Authorization: Bearer YOUR_API_KEY
    Content-Type: application/json

    Request body parameters

    ParameterTypeRequiredDescription
    userobject✅ YesDetails of the user in your CRM
    user.idstring✅ YesThe user's identifier in your CRM
    user.namestringNoUser name (shown in the interface)
    scopestringNoDisplay mode: global (all conversations) or card (one contact's chat). Default: global
    filterobjectConditionalFilters for scope=card. Required when scope=card
    filter.phonestring-Contact phone number to filter by
    filter.channelIdsarray-Limit the view to specific channels
    activeChatobjectNoOpen a specific conversation right on load
    optionsobjectNoExtra display settings
    options.langstringNoChat interface language: ru, en, kk or ky. See “Chat language” below

    Display modes (scope)

    global: all conversations

    Shows the full list of conversations, just like the main WazzaBee interface. A good fit for a dedicated “Chats” page in your CRM.

    card: one contact's chat

    Filters conversations by the contact's phone number. Perfect for embedding into a customer card: the agent sees only the conversation with that customer.

    UI actions available in the conversation list header

    The iframe comes with a built-in set of control buttons above the chat list. Which ones are visible depends on the scope you chose. The UI adapts to how the agent works inside the CRM:

    Action scope=global scope=card
    📌 Pin a chat✅✅
    👁 Mark as unread✅✅
    📦 Archive a chat + the “active / archived” switch✅❌ hidden
    ✓✓ Mark all as read (in the current channel / in all channels)✅❌ hidden
    🔄 Refresh the conversation list✅❌ hidden
    ➕ Add a contact manually✅❌ hidden. Instead, the “Start a chat” pop-up opens automatically if the contact from filter.phone does not exist yet
    Sending messages, media, voice notes and templates✅✅
    💡 The logic: in card mode the CRM agent works with a single contact: the list control buttons are of no use there and only distract. That is why they are hidden on purpose, so the UI stays focused on the conversation. In scope=global you get every feature of the main WazzaBee dashboard.
    📦 Archived chats: archiving hides a conversation from the active list, but the history is kept. There is a separate Archive icon in the list header: one click switches the view between active and archived chats. Every archived conversation gets a restore button.

    Request examples

    Example 1. All conversations (scope=global)

    curl -X POST "https://api.wazzabee.com/api-gateway/v1/iframe" \
      -H "Authorization: Bearer YOUR_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "user": {
          "id": "manager-42",
          "name": "Alex Manager"
        },
        "scope": "global"
      }'

    Example 2. One customer's chat (scope=card)

    curl -X POST "https://api.wazzabee.com/api-gateway/v1/iframe" \
      -H "Authorization: Bearer YOUR_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "user": {
          "id": "manager-42",
          "name": "Alex Manager"
        },
        "scope": "card",
        "filter": {
          "phone": "77001234567"
        }
      }'

    Example 3. Limited to specific channels

    curl -X POST "https://api.wazzabee.com/api-gateway/v1/iframe" \
      -H "Authorization: Bearer YOUR_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "user": {
          "id": "manager-42",
          "name": "Alex Manager"
        },
        "scope": "card",
        "filter": {
          "phone": "77001234567",
          "channelIds": ["550e8400-e29b-41d4-a716-446655440000"]
        }
      }'

    Successful response (200 OK)

    {
      "success": true,
      "url": "https://app.wazzabee.com/chat/embed/a1b2c3d4...64-character-token",
      "token": "a1b2c3d4...64-character-token",
      "expiresAt": "2026-03-13T10:00:00.000Z",
      "scope": "card"
    }
    FieldTypeDescription
    urlstringA ready-to-use URL for <iframe src="...">
    tokenstringSession token (64 hex characters)
    expiresAtstring (ISO 8601)When the session expires (24 hours after it was created)
    scopestringThe selected mode: global or card

    Chat language

    The embedded chat is shown in one of four languages: Russian (ru), English (en), Kazakh (kk) or Kyrgyz (ky). The language is chosen in this order:

    1. the lang parameter in the chat URL, for example https://app.wazzabee.com/chat/embed/TOKEN?lang=en;
    2. the options.lang field when you create the session with POST /v1/iframe;
    3. the WazzaBee account language of the API key owner;
    4. if none of these is set, the chat opens in Russian.

    The simplest way is to pass your user's language in options.lang: then you can put the URL from the response into the <iframe> as is.

    {
      "user": { "id": "manager-42", "name": "Alex" },
      "scope": "global",
      "options": { "lang": "en" }
    }

    Embedding on the frontend

    Plain HTML

    <iframe
      src="https://app.wazzabee.com/chat/embed/SESSION_TOKEN"
      style="width: 100%; height: 600px; border: none;"
      allow="clipboard-write"
    ></iframe>

    React component

    import { useEffect, useState } from "react";
    
    function WazzaBeeChat({ contactPhone }) {
      const [iframeUrl, setIframeUrl] = useState(null);
    
      useEffect(() => {
        // A call to your backend, which proxies the request to the WazzaBee API
        fetch("/api/wazzabee/iframe", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({
            phone: contactPhone,
            scope: "card"
          })
        })
          .then(res => res.json())
          .then(data => setIframeUrl(data.url));
      }, [contactPhone]);
    
      if (!iframeUrl) return <div>Loading chat...</div>;
    
      return (
        <iframe
          src={iframeUrl}
          style={{ width: "100%", height: "600px", border: "none" }}
          allow="clipboard-write"
        />
      );
    }
    ⚠️ Important: the request to the WazzaBee API (the one with the API key) must go through your backend, not straight from the customer's browser. Never keep the API key on the frontend!

    postMessage events

    The iframe sends events to the parent window through window.postMessage. You can listen to them and wire the chat into your CRM.

    📌 Event format: every event arrives as an event.data object with the fields source: "wazzabee", event: "WZ_..." and the payload fields. Check event.origin and event.data.source to stay safe.

    Available events

    EventWhen it firesData (payload fields)
    WZ_READYThe chat has loaded and is ready to work{ userId, scope }
    WZ_CHAT_OPENEDThe agent opened a conversation or switched to another one. Fires every time the active chat changes: use it to update the customer card in your CRM{ conversationId, chatId, phone, name, contactId, isGroup, contactPhone, contactName }
    ⚠️ contactPhone, contactName are deprecated, use phone, name
    WZ_CHAT_CLOSEDThe chat was unloaded: the user left the page with the iframe. Not sent when switching between conversations: catch active chat changes with WZ_CHAT_OPENED{}
    WZ_MESSAGE_SENTThe agent sent a message{ conversationId, chatId, phone, contactPhone }
    WZ_MESSAGE_RECEIVEDA new message arrived (incoming or outgoing){ messageId, conversationId, chatId, phone, direction, contentType, content, text, timestamp }
    WZ_MESSAGES_READThe agent opened the chat (read the messages){ conversationId, chatId, phone, contactPhone, unreadCount }
    ⚠️ contactPhone is deprecated, use phone
    WZ_CREATE_CONTACTA request to create a contact in your CRM{ phone, name }
    WZ_CHAT_NOT_FOUNDThe requested chat was not found (for example, a wrong phone number in scope=card){ phone, reason }
    WZ_ACTION_NOT_SUPPORTEDThe action is not available in the current mode (for example, trying to switch conversations outside scope=card){ action, reason }
    WZ_UNREAD_TOTALThe total number of unread messages changed: handy for a badge on a CRM tab{ channelId, total }
    channelId = null if several channels are linked to the session
    WZ_SESSION_REFRESHEDThe session token was extended automatically, no action needed{}
    WZ_SESSION_EXPIREDThe session has expired: request a new iframe URL{}
    WZ_CONNECTION_LOSTThe realtime connection dropped (network). It recovers on its own, you can show an indicator{ scope }
    📞 About the phone and contactPhone fields: in every contact-related event we send both versions: phone (recommended) and the legacy contactPhone. The same goes for name and contactName. That way existing integrations keep working. Use phone/name in new integrations; the legacy fields are still supported and will not be removed.

    Handling events: an example

    window.addEventListener("message", (event) => {
      // Check the origin
      if (event.origin !== "https://app.wazzabee.com") return;
      if (event.data?.source !== "wazzabee") return;
    
      const { event: type, ...data } = event.data;
    
      switch (type) {
        case "WZ_READY":
          console.log("WazzaBee chat loaded, user:", data.userId, "scope:", data.scope);
          break;
    
        case "WZ_CHAT_OPENED":
          // Recommended: data.phone, data.name
          console.log("Conversation opened:", data.conversationId, "phone:", data.phone);
          // You can refresh the customer card in your CRM here
          break;
    
        case "WZ_CHAT_CLOSED":
          console.log("Conversation closed");
          break;
    
        case "WZ_MESSAGE_SENT":
          console.log("The agent sent a message to chat:", data.chatId, data.phone);
          // You can add a record to the CRM history here
          break;
    
        case "WZ_MESSAGE_RECEIVED":
          console.log("New message:", data.content);
          console.log("Direction:", data.direction);
          break;
    
        case "WZ_MESSAGES_READ":
          console.log("The agent read the chat:", data.conversationId);
          break;
    
        case "WZ_CREATE_CONTACT":
          console.log("Request to create a contact:", data.phone, data.name);
          // You can create the contact in your CRM automatically
          break;
    
        case "WZ_CHAT_NOT_FOUND":
          console.warn("Chat not found:", data.phone, "reason:", data.reason);
          // For example, offer to create a contact
          break;
    
        case "WZ_ACTION_NOT_SUPPORTED":
          console.warn("Action not available:", data.action, "reason:", data.reason);
          break;
      }
    });

    Controlling the chat from your CRM

    The exchange works both ways: the iframe does not just tell you what is happening, you can also control it. Send a postMessage to the iframe with the field source: "crm".

    ActionDataBehavior
    OPEN_CHAT_BY_PHONE{ phone }Open the chat with this number. You get WZ_CHAT_OPENED back (found) or WZ_CHAT_NOT_FOUND (no such contact). In scope=card mode it returns WZ_ACTION_NOT_SUPPORTED

    The number is normalized by its digits, so any format works: +7 (700) 123-45-67, 77001234567 and 87001234567 all behave the same way.

    iframeRef.current?.contentWindow?.postMessage({
      source: "crm",
      action: "OPEN_CHAT_BY_PHONE",
      phone: "+77001234567",
    }, "*");

    Scenario: customer card on the left, chat on the right

    A typical task when embedding into a CRM: on the right the agent handles the conversation, on the left there is the customer card, a product catalog and action buttons. When the agent switches to another chat, the left side should load the new customer's data by itself.

    ✅ No browser extensions or page markup parsing needed. The embedded chat reports active conversation changes itself with the WZ_CHAT_OPENED event: just subscribe to message in the parent window.
    const [activeChat, setActiveChat] = useState(null);
    
    useEffect(() => {
      const handler = (event) => {
        if (event.origin !== "https://app.wazzabee.com") return;
        if (event.data?.source !== "wazzabee") return;
    
        // The agent switched to another chat
        if (event.data.event === "WZ_CHAT_OPENED") {
          setActiveChat({
            chatId: event.data.chatId,
            phone: event.data.phone,
            name: event.data.name,
          });
          // Load the customer card by phone number from your CRM
          loadCustomerCard(event.data.phone);
        }
      };
    
      window.addEventListener("message", handler);
      return () => window.removeEventListener("message", handler);
    }, []);

    The other direction: the agent opened a deal in the CRM and wants to jump straight to the conversation. Send OPEN_CHAT_BY_PHONE to the iframe, and the chat switches to the right conversation by itself.

    Possible errors

    CodeErrorReason
    400The user.id field is requiredThe CRM user identifier was not passed
    400A filter is required for scope=cardYou chose card mode but did not pass a filter

    Session lifetime

    • A session is valid for 24 hours from the moment it is created
    • Once it expires the iframe shows an error: you need to request a new token
    • We recommend refreshing the token in advance (every 12 hours, for example)
    • A separate session is created for every CRM user

    Which agent replied to the customer

    The name you pass in user.name when creating the session is the message author. Every outgoing message in the chat feed carries a signature like “from WazzaBee · Nazerke A.”, so the conversation itself shows which agent replied. The last name is shortened to an initial.

    The signature works for everything an agent sends from the embedded chat: text, files, voice notes and WABA templates.

    What you need to pass

    FieldWhy
    user.nameThe employee's name: it goes into the signature under the message and into the messages_outgoing webhook (author.name)
    user.idThe employee's identifier in your CRM: we return it in the webhook as author.userId so you can match the reply to your own agent
    ✅ You do not need to create separate users in WazzaBee. We take the name and the identifier straight from the session, so there is nothing to map between your CRM staff and WazzaBee users. Adding employees in the “Team” section only makes sense for people who log into the WazzaBee dashboard directly rather than through the embedded chat.
    ℹ️ The signature is visible to your team only. It is not sent to the customer in WhatsApp or Telegram: they see just the message text. The signature format cannot be changed in the settings.