Skip to main content

In-Meeting Chat

Metered ships a native text and file chat that lives inside the meeting. Messages travel over the meeting's own connection, so there is no separate chat service to provision, authenticate, or keep in sync with the call.

Chat is enabled per room with the enableChat room setting. It works in the prebuilt iframe UI and, now, directly from the Video SDK, so you can build your own chat surface with the methods and events below.

Turning chat on

Set enableChat to true on the room, either when you create it or later through the

Update Room APIor the dashboard. When the room has chat enabled, the SDK chat methods start working for every participant that joins.

Sending and receiving text

Call meeting.sendChatMessage(text) to send a message to everyone in the meeting. Each participant receives it through the chatMessageReceived event.

Sender
meeting.sendChatMessage("Hello everyone 👋");
Any participant in the meeting
meeting.on("chatMessageReceived", (message) => {
// message.type is "text" | "file" | "image"
console.log(`${message.senderName}: ${message.content}`);
});

A chatMessageReceived payload carries:

{
_id: "…", // stable message id (use it for de-duping / downloads)
senderParticipantSessionId: "…",
senderName: "…",
recipientParticipantSessionId: null, // set only for directed (private) messages
type: "text", // "text" | "file" | "image"
content: "Hello everyone 👋", // text body (raw — always escape before rendering)
fileName: null, // set on file / image messages
fileMimeType: null,
fileSizeBytes: null,
created: 1700000000000, // sort key, paired with _id
downloadToken: null // set on file / image messages
}

Text is delivered raw (trimmed, up to 5000 characters). Always escape it before you put it in the DOM. React does this for you.

See the SDK reference for

`sendChatMessage`and the`chatMessageReceived`event.

Directed (private) messages

To send a message to one participant instead of the whole meeting, call

`meeting.sendChatMessageTo(participantSessionId, text)`. The participant session id is the `_id` you get from the`participantJoined`event (or from your own `join()` result).
meeting.on("participantJoined", (participant) => {
// participant._id is the participantSessionId you can direct a message to
meeting.sendChatMessageTo(participant._id, "Welcome — this note is just for you.");
});

Both the sender and the recipient receive the message through chatMessageReceived, with recipientParticipantSessionId set to the recipient. No other participant ever sees a directed message.

Typing indicator

Call

`meeting.sendTypingIndicator()`while the local user is typing. Other participants receive a`chatTyping`event. The server throttles typing broadcasts to about one every 2 seconds, so it is safe to call on every keystroke.
Local user typing
inputEl.addEventListener("input", () => meeting.sendTypingIndicator());
Other participants
meeting.on("chatTyping", ({ participantSessionId, senderName }) => {
showTypingHint(`${senderName} is typing…`);
});

Loading history

Call

`meeting.getChatHistory(before, limit, beforeId)`to fetch stored messages. Results arrive on the`chatHistory`event, oldest-first. To load the first page, call it with no arguments.
// First page
meeting.getChatHistory();

meeting.on("chatHistory", ({ chatRoomId, before, messages }) => {
render(messages);

// Page backwards: use the OLDEST message in this batch as the cursor.
const oldest = messages[0];
if (oldest) {
meeting.getChatHistory(oldest.created, 50, oldest._id);
}
});

History pages backwards by (created, _id): pass the oldest message's created as before and its _id as beforeId. When a page comes back empty, you have reached the start of the stored history.

Chat history retention

How long messages are stored is controlled per room by the chatHistoryRetentionHours setting (a value from 0 to 24), which you set through the

Update Room APIor the dashboard.
Ephemeral by default

The default is 0: messages are delivered but never stored. So that late joiners still see the conversation, the SDK keeps an in-session buffer and serves messages sent after they joined — nothing from before their arrival, and nothing survives the session.

Set it to any whole number of hours from 1 to 24 (the dashboard offers 1, 6, 12 and 24) to store messages and have them auto-deleted after that many hours. A few things to keep in mind:

  • Lowering the value is not retroactive. Messages already written keep the expiry they were stored with; only messages written after the change use the shorter window.
  • File attachments are always removed after 24 hours, regardless of the retention setting.

File sharing

Sharing a file is a two-step flow: upload the bytes to get a file reference, then send a chat message that points at it.

  1. Upload the file with POST /api/v1/chat/upload, authenticated with the chat access token from`meeting.getChatAccessToken()`in the `Authorization: Bearer` header. The response gives you four fields describing the stored file.
  2. Send a file message with`meeting.sendChatFileMessage(...)`, passing those four fields.
async function shareFile(file) {
const res = await fetch("https://<appname>.metered.live/api/v1/chat/upload", {
method: "POST",
headers: { Authorization: `Bearer ${meeting.getChatAccessToken()}` },
body: (() => {
const form = new FormData();
form.append("file", file);
return form;
})(),
});
const { fileS3Key, fileName, fileMimeType, fileSizeBytes } = await res.json();

meeting.sendChatFileMessage(fileS3Key, fileName, fileMimeType, fileSizeBytes);
}

Recipients receive the file message as a normal chatMessageReceived event whose type is file (or image) and which carries a downloadToken scoped to that one message. Download the attachment with GET /api/v1/chat/file/<messageId>?dl=<downloadToken>:

meeting.on("chatMessageReceived", (message) => {
if (message.type === "file" || message.type === "image") {
const url =
`https://<appname>.metered.live/api/v1/chat/file/${message._id}` +
`?dl=${message.downloadToken}`;
// e.g. render <a href={url}>{message.fileName}</a>
}
});

For the full request/response details, constraints, and the header-token download option for SDK callers, see

Chat File Sharing.

Errors and limits

Operations that are rejected surface on the

`chatMessageError`event rather than throwing:
meeting.on("chatMessageError", ({ context, message }) => {
console.warn(`Chat ${context} failed: ${message}`);
});

Per-participant rate limits:

OperationLimit
Sending messages20 per 10 seconds
Loading history10 per 10 seconds
File uploads10 per minute
Typing indicator1 broadcast per 2 seconds

File constraints:

  • 10 MB maximum per file.
  • Uploads must match the MIME allowlist (images, PDF, plain text/CSV, ZIP, and common Office documents). The full list is on theChat File Sharingpage.

Chat methods throw while the socket is disconnected, and until the meeting has finished reconnecting. Guard against this by only sending once the meeting is connected.