Live Chat — WebSocket Integration
The Live Chat can be embedded outside the Panda Video player — in member areas, custom layouts, mobile apps — by combining a small REST flow for setup with a WebSocket connection for real-time messaging.
Use this page when you want a chat for a live stream to live in your own UI, not in the embedded player iframe.
Already have a live and a chat?This page assumes you already created the chat. If not, start with Create live followed by Create Chat Live.
Overview
REST (setup)
your app ───────────────────────────► chat.pandavideo.live/api
│
│ wss (messages)
└───────────────────────────────► chat.pandavideo.live
| Item | Value |
|---|---|
| REST base (prod) | https://chat.pandavideo.live/api |
| REST base (staging) | https://chat-staging.pandavideo.live/api |
| WebSocket (prod) | wss://chat.pandavideo.live |
| WebSocket (staging) | wss://chat-staging.pandavideo.live |
| Protocol | Socket.IO v4 — transport websocket |
End-to-end flow
| Step | Who | What |
|---|---|---|
| 1 | Live owner (auth) | Create Chat Live — yields chat_id. |
| 2 | Viewer (public) | Resolve live_id from chat_id via the public config JSON. |
| 3 | Viewer (public) | POST /api/register-user — yields user_id. |
| 4 | Viewer (public) | Open wss://chat.pandavideo.live and emit join_room. |
| 5 | Viewer (public) | Emit send_message, listen for message, user_joined, user_left, etc. |
| 6 | Live owner (auth) | Moderate via Blocked Chat Users, POST /remove-message, Update Chat Live. |
1. Resolve live_id from chat_id
live_id from chat_idBefore registering a viewer, look up the live_id (and confirm the chat is active) from the public config JSON. This is the same source the Panda player uses.
const cfg = await fetch(
`https://config-live.tv.pandavideo.com.br/${libraryId}/${chatId}-chat.json?nocache=${Date.now()}`
).then((r) => r.json());
// cfg = { id, pullzone_name, chat_id, chat_status, live_id, created_at, updated_at }
if (cfg.chat_status !== "active") throw new Error("Chat is inactive");
const liveId = cfg.live_id;2. Register the viewer
POST https://chat.pandavideo.live/api/register-user
Content-Type: application/json
{ "live_id": "<live_id>", "user_name": "Mary" }- Public — no
Authorizationheader required. - If you do send a valid
Authorization: Bearer <token>belonging to the live's owner, the user is registered as admin of the chat (theiris_adminflag will betrue). - The server captures the viewer's IP from
x-forwarded-forand refuses a registration if a previously banned user has the same IP.
Success (200):
{
"message": "User registered successfully",
"registered_user": {
"user_id": "uuid",
"user_name": "Mary",
"chat_id": "<chat_id>",
"is_valid": true,
"is_admin": false,
"name_occurence": 1
}
}Validation error (400):
{ "errCode": "BadRequest", "errMsg": "Invalid user_name field" }
Persist theuser_idon the viewer's sideYou'll need it for
join_roomand everysend_message. Storing it inlocalStoragekeyed bychat_idlets a returning viewer reuse the same identity.
3. Connect and join the room
import { io } from "socket.io-client";
const socket = io("wss://chat.pandavideo.live", {
transports: ["websocket"],
});
socket.on("connect", () => {
socket.emit("join_room", {
chat_id: CHAT_ID,
user_id: USER_ID,
});
});Events sent by the client
| Event | Payload | Notes |
|---|---|---|
join_room | { chat_id, user_id } | Joins the room. Fires user_joined to everyone else. |
leave_room | { chat_id, user_id } | Optional clean exit. Fires user_left to everyone else. |
send_message | { chat_id, user_id, message } | message ≤ 500 chars. |
4. Server events
The server broadcasts these events to every socket in the same room.
message — new chat message
message — new chat message{
"message_id": "uuid",
"user_id": "uuid",
"user_name": "Mary",
"message": "Hello!",
"sent_at": 1730000000000,
"is_admin": false
}user_joined
user_joined{ "user_id": "uuid", "joined_at": 1730000000000 }user_left
user_left{ "user_id": "uuid", "left_at": 1730000000000 }(When an admin forcibly removes a user, the payload is { "message": "User Mary left from chat", "left_at": ... }.)
remove_message — moderated message
remove_message — moderated message{ "message_id": "uuid", "removed_at": 1730000000000 }remove_all_messages — every message from a banned user
remove_all_messages — every message from a banned user{ "user_id": "uuid", "removed_at": 1730000000000 }rate_limit — too many events on this socket
rate_limit — too many events on this socket{ "message": "Rate limit exceeded" }error_event — error; socket is disconnected right after
error_event — error; socket is disconnected right after{ "message": "Chat doesn't exist", "status_code": 404 }
Socket lifecycle on errors
error_eventalways disconnects the socket.rate_limitdoes NOT — the event is just dropped silently. Track both differently in your UI.
Rate limits
- REST: 100 requests / minute per IP.
- WebSocket: 5 events every 10 seconds per socket. The 6th event triggers
rate_limitand is dropped; the connection stays open.
Browser example (viewer)
<!doctype html>
<script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script>
<script>
const LIBRARY_ID = "vz-xxxxxxxx-xxx";
const CHAT_ID = "chat-uuid";
async function init(userName) {
const cfg = await fetch(
`https://config-live.tv.pandavideo.com.br/${LIBRARY_ID}/${CHAT_ID}-chat.json?nocache=${Date.now()}`
).then((r) => r.json());
if (cfg.chat_status !== "active") return alert("Chat inactive");
const reg = await fetch("https://chat.pandavideo.live/api/register-user", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ live_id: cfg.live_id, user_name: userName }),
}).then((r) => r.json());
const USER_ID = reg.registered_user.user_id;
const socket = io("wss://chat.pandavideo.live", { transports: ["websocket"] });
socket.on("connect", () =>
socket.emit("join_room", { chat_id: CHAT_ID, user_id: USER_ID })
);
socket.on("message", (m) => render(m));
socket.on("user_joined", (e) => log("joined: " + e.user_id));
socket.on("user_left", (e) => log("left: " + e.user_id));
socket.on("remove_message",(e) => removeFromUi(e.message_id));
socket.on("rate_limit", (e) => warn(e.message));
socket.on("error_event", (e) => error(e));
window.sendMessage = (text) =>
socket.emit("send_message", { chat_id: CHAT_ID, user_id: USER_ID, message: text });
}
init(prompt("Your name:"));
</script>Common errors
| Field | Value | Cause |
|---|---|---|
error_event.message | Chat doesn't exist | chat_id not found |
error_event.message | Chat is not active | chat paused or terminated |
error_event.message | User not registered | register-user was not called |
error_event.message | User is not valid | viewer was banned |
error_event.message | message must be less than 500 characters | text too long |
errMsg (REST) | Invalid <field> field | missing or wrong-typed body field |
Persistence
- During the broadcast, messages live in MongoDB (dynamic collection
chat_messages_<chat_id>). - When the live owner calls terminate-chat, the messages and viewer list are archived to S3 under
live_chat/<year>/<month>/<day>/<chat_id>/...and the MongoDB collections are dropped. GET /api/chat-messagesworks only while the chat is active.
