- Get your API key in the WazzaBee dashboard → Connectors → API
- From your backend, send a POST request to
/v1/iframewith the API key - Put the URL you get back into an
<iframe>on your page - Done! Your agent sees every conversation right inside your app
Two ways to use it
| Mode | scope | Where to use it |
|---|---|---|
| All conversations | global | A dedicated “Chats” or “Messengers” page in your app's menu |
| Customer chat | card | The 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.
- Your backend requests an iframe session through the API
- The API returns a URL to embed
- Your frontend renders an
<iframe>with that URL - 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
| Parameter | Type | Required | Description |
|---|---|---|---|
user | object | ✅ Yes | Details of the user in your CRM |
user.id | string | ✅ Yes | The user's identifier in your CRM |
user.name | string | No | User name (shown in the interface) |
scope | string | No | Display mode: global (all conversations) or card (one contact's chat). Default: global |
filter | object | Conditional | Filters for scope=card. Required when scope=card |
filter.phone | string | - | Contact phone number to filter by |
filter.channelIds | array | - | Limit the view to specific channels |
activeChat | object | No | Open a specific conversation right on load |
options | object | No | Extra display settings |
options.lang | string | No | Chat 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 | ✅ | ✅ |
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.
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"
}
| Field | Type | Description |
|---|---|---|
url | string | A ready-to-use URL for <iframe src="..."> |
token | string | Session token (64 hex characters) |
expiresAt | string (ISO 8601) | When the session expires (24 hours after it was created) |
scope | string | The 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:
- the
langparameter in the chat URL, for examplehttps://app.wazzabee.com/chat/embed/TOKEN?lang=en; - the
options.langfield when you create the session withPOST /v1/iframe; - the WazzaBee account language of the API key owner;
- 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"
/>
);
}
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.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
| Event | When it fires | Data (payload fields) |
|---|---|---|
WZ_READY | The chat has loaded and is ready to work | { userId, scope } |
WZ_CHAT_OPENED | The 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_CLOSED | The 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_SENT | The agent sent a message | { conversationId, chatId, phone, contactPhone } |
WZ_MESSAGE_RECEIVED | A new message arrived (incoming or outgoing) | { messageId, conversationId, chatId, phone, direction, contentType, content, text, timestamp } |
WZ_MESSAGES_READ | The agent opened the chat (read the messages) | { conversationId, chatId, phone, contactPhone, unreadCount }⚠️ contactPhone is deprecated, use phone |
WZ_CREATE_CONTACT | A request to create a contact in your CRM | { phone, name } |
WZ_CHAT_NOT_FOUND | The requested chat was not found (for example, a wrong phone number in scope=card) | { phone, reason } |
WZ_ACTION_NOT_SUPPORTED | The action is not available in the current mode (for example, trying to switch conversations outside scope=card) | { action, reason } |
WZ_UNREAD_TOTAL | The 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_REFRESHED | The session token was extended automatically, no action needed | {} |
WZ_SESSION_EXPIRED | The session has expired: request a new iframe URL | {} |
WZ_CONNECTION_LOST | The realtime connection dropped (network). It recovers on its own, you can show an indicator | { scope } |
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".
| Action | Data | Behavior |
|---|---|---|
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.
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
| Code | Error | Reason |
|---|---|---|
400 | The user.id field is required | The CRM user identifier was not passed |
400 | A filter is required for scope=card | You 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
| Field | Why |
|---|---|
user.name | The employee's name: it goes into the signature under the message and into the messages_outgoing webhook (author.name) |
user.id | The 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 |
