Data messages are small payloads, either text or bytes, that travel with the call over the same connection as the media. Use them for things that belong to this call and nowhere else:
- a "raise hand" or an emoji reaction,
- a cursor position on a shared whiteboard,
- game state in a multiplayer lobby,
- a "you're on mute" nudge sent to one person.
For a chat history that is kept, searchable and delivered to people who join later, use Chat instead.
Before you start: your app has joined a room with a token that has
publishData: true (see Authentication). All code on
this page runs in your app. To send from your backend, see
Messages from your server.
Send and receive
await room.sendData('hello'); // string or Uint8Array: reliable, to everyone
await room.sendData(JSON.stringify({ emoji: '🎉' }), { topic: 'reaction' });
await room.sendData('you are muted', { destinationIdentities: ['bob'] });
await room.sendData(cursorBytes, { topic: 'cursor', reliable: false }); // lossy
room.on('dataReceived', (payload, participant, topic) => {
// participant: who sent it, or undefined for a message from your server
// topic: undefined when the sender gave none
console.log(topic, participant?.identity, new TextDecoder().decode(payload));
});import { useDataReceived } from '@livqeno/react-native';
await room.sendData('hello', { topic: 'reaction' });
function Reactions({ room }) {
useDataReceived(room, (payload, participant) => {
showReaction(participant?.identity, payload);
}, { topic: 'reaction' }); // optional: only this topic
return null;
}Same room.sendData() options and dataReceived event as the web.
await room.sendData(utf8.encode('hello')); // reliable, to everyone
await room.sendData(bytes, topic: 'reaction');
await room.sendData(bytes, destinationIdentities: ['bob']);
await room.sendData(cursor, topic: 'cursor', reliable: false); // lossy
room.dataMessages.listen((message) {
message.participant; // the sender, or null for a message from your server
message.topic; // null when none
message.payload;
});room.data (the payload alone) still works.
try room.sendData("hello") // reliable, to everyone
try room.sendData(json, options: DataPublishOptions(topic: "reaction"))
try room.sendData("psst", options: DataPublishOptions(destinationIdentities: ["bob"]))
try room.sendData(cursor, options: DataPublishOptions(topic: "cursor", reliable: false)) // lossy
for await event in room.events() {
if case let .dataMessageReceived(message) = event {
// message.participant: the sender, or nil for a message from your server
// message.topic: nil when none
handle(message.payload, from: message.participant, topic: message.topic)
}
}The older .dataReceived(_, participant:) event still fires for every
message, now with the sender. Handle one of the two, not both.
room.sendData("hello") // reliable, to everyone
room.sendData(bytes, DataPublishOptions(topic = "reaction"))
room.sendData(bytes, DataPublishOptions(destinationIdentities = listOf("bob")))
room.sendData(cursor, DataPublishOptions(topic = "cursor", reliable = false)) // lossy
room.events.filterIsInstance<RoomEvent.DataReceived>().collect { e ->
// e.participant: the sender, or null for a message from your server
// e.topic: null when none
}With no options, sendData() does exactly what it always did: a reliable
message to everyone in the room.
Options
| Option | Default | What it does |
|---|---|---|
topic | none | A label for the message, up to 255 bytes of UTF-8. Receivers get it next to the payload, so one listener can route reactions, cursors and game moves apart. |
destinationIdentities | everyone | Send only to these identities, up to 64 of them. Identities that aren't in the room are skipped without an error. |
reliable | true | true: ordered and retransmitted, up to 64 KiB. false: the lossy channel, unordered and never retransmitted, up to 1,200 bytes. |
When to use lossy. Use it when only the latest value matters, such as cursor positions, a player's position or a live slider. A lost update is replaced by the next one anyway, and the lossy channel never makes a newer message wait behind an older one that is being retransmitted. Anything that must arrive, such as a reaction or a command, stays reliable.
Who sent it
The sender is filled in by Livqeno's media server, not by the sending app, so a client can't pretend to be someone else.
- A message from a participant arrives with that participant.
- A message from your server (see below) arrives with no participant
(
undefined/null/nil). - A message from an SDK that is older than this feature also arrives with no participant and no topic.
Messages from your server
Your backend can send to a room with an API key. Recipients see it with no participant:
curl -X POST "$LIVQENO_API_URL/v1/rooms/$ROOM_ID/data" \
-H "Authorization: Bearer $LIVQENO_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "payload": "'"$(printf 'meeting ends in 5 min' | base64)"'", "topic": "system", "reliable": true }'Or with a server SDK:
await livqeno.rooms.sendData(roomId, 'meeting ends in 5 min', { topic: 'system' });
await livqeno.rooms.sendData(roomId, cursorBytes, { destinationIdentities: ['bob'], reliable: false });livqeno.rooms.send_data(room_id, "meeting ends in 5 min", topic="system")
livqeno.rooms.send_data(room_id, cursor_bytes, destination_identities=["bob"], reliable=False)The API answers 202 { "messageId": "…" } once the message is handed on
for delivery. It answers 404 RAVEN_PARTICIPANT_NOT_CONNECTED when nobody
is in the room, and 400 for a payload or topic over its limit (the same
limits as below). :roomId takes the room's id or its name.
destinationIdentities works here too. Delivery is best-effort, like any
data message, so the SDKs never retry this call. See the
RTC API reference for the full request.
Limits
| Limit | Value |
|---|---|
| Reliable payload | 64 KiB |
| Lossy payload | 1,200 bytes |
| Topic | 255 bytes of UTF-8 |
| Destinations per message | 64 |
| Reliable rate per participant | 100 messages/s and 1 MiB/s |
| Lossy rate per participant | 300 messages/s |
Every limit is checked before anything is sent. A topic or destination
list over its limit, or a payload over its size limit, throws
INVALID_ARGUMENT on every platform.
Over the rate limit, the server drops the extra messages and tells the sender. The SDK logs a warning and fires an event, so you can slow down:
| Platform | Event |
|---|---|
| Web / React Native | room.on('dataThrottled', (channel) => …), where channel is 'reliable' or 'lossy' |
| Flutter | room.dataThrottled.listen((e) => e.channel) |
| iOS | RoomEvent.dataThrottled(channel:) |
| Android | RoomEvent.DataThrottled(channel) |
Receiving: add the listener right after joining
The SDK opens both data channels (reliable and lossy) as soon as it joins a server that supports them, so messages sent to you are delivered even before you add a listener. Still add the listener once, right after joining: messages that arrive before it are not replayed to it. (Against an older server, listening for data is what opens your channel.)
A message only reaches people who are connected at that moment. It isn't saved for people who join later.
Older servers
Topics, targeting, the lossy channel and sender identity need a media
server that supports them. The SDK finds out when it joins (room.joined
lists data.v2 in its capabilities). Against an older server:
- a plain
sendData(payload)works exactly as before, reliable: falsefalls back to the reliable channel, with a warning,- received messages arrive with no participant and no topic.
Mixed rooms work. Someone on an SDK from before this feature still gets
your messages, as the plain payload with no topic or sender, exactly as
they always did. Targeting still applies: a message for bob never
reaches anyone else.
Livqeno Cloud supports all of it.
Common errors
| Error | Cause | Fix |
|---|---|---|
PERMISSION_DENIED on sendData() | The token has no publishData. | Set publishData: true when you mint the token. |
INVALID_ARGUMENT on sendData() | The payload is over 64 KiB, or over 1,200 bytes with reliable: false. | Send less, split it up, or use the reliable channel. |
INVALID_ARGUMENT on sendData() | The topic is over 255 bytes, there are more than 64 destinations, or a destination identity is empty. | Shorten the topic, or send in batches. |
| Messages from some people arrive with no participant | They use an SDK from before sender identity existed. | Upgrade their SDK. |
Related
- Rooms & Participants: participant metadata and attributes, for state that should stay in place rather than travel as a message.
- Events: the full event list.
- Chat: for messages that need history.