Live Chat — WebSocket Integration

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
ItemValue
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
ProtocolSocket.IO v4 — transport websocket

End-to-end flow

StepWhoWhat
1Live owner (auth)Create Chat Live — yields chat_id.
2Viewer (public)Resolve live_id from chat_id via the public config JSON.
3Viewer (public)POST /api/register-user — yields user_id.
4Viewer (public)Open wss://chat.pandavideo.live and emit join_room.
5Viewer (public)Emit send_message, listen for message, user_joined, user_left, etc.
6Live owner (auth)Moderate via Blocked Chat Users, POST /remove-message, Update Chat Live.

1. Resolve live_id from chat_id

Before 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 Authorization header 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 (their is_admin flag will be true).
  • The server captures the viewer's IP from x-forwarded-for and 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 the user_id on the viewer's side

You'll need it for join_room and every send_message. Storing it in localStorage keyed by chat_id lets 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

EventPayloadNotes
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_id": "uuid",
  "user_id": "uuid",
  "user_name": "Mary",
  "message": "Hello!",
  "sent_at": 1730000000000,
  "is_admin": false
}

user_joined

{ "user_id": "uuid", "joined_at": 1730000000000 }

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

{ "message_id": "uuid", "removed_at": 1730000000000 }

remove_all_messages — every message from a banned user

{ "user_id": "uuid", "removed_at": 1730000000000 }

rate_limit — too many events on this socket

{ "message": "Rate limit exceeded" }

error_event — error; socket is disconnected right after

{ "message": "Chat doesn't exist", "status_code": 404 }
❗️

Socket lifecycle on errors

error_event always disconnects the socket. rate_limit does 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_limit and 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

FieldValueCause
error_event.messageChat doesn't existchat_id not found
error_event.messageChat is not activechat paused or terminated
error_event.messageUser not registeredregister-user was not called
error_event.messageUser is not validviewer was banned
error_event.messagemessage must be less than 500 characterstext too long
errMsg (REST)Invalid <field> fieldmissing 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-messages works only while the chat is active.

Related endpoints