Core API
Interactions
Relay lets you broadcast real-time interactions — reactions, chat messages, hand raises, polls, and custom events — to participants inside a live hall. Interactions are delivered over LiveKit's data channels and received by clients in milliseconds without any additional WebSocket infrastructure.
409 hall_not_active.How it works
When you call the interactions endpoint, Relay sends a typed JSON envelope to every participant in the hall (or a subset you specify) via LiveKit data channels. Your frontend SDK listens for these envelopes and renders the appropriate UI — no separate WebSocket connection needed.
Participants can also send interactions directly from the client using the LiveKit JS SDK, since all join tokens include can_publish_data: true. The server-side endpoint is for interactions you want to send from your backend — polls, system announcements, moderation actions, and so on.
Send an interaction
/v1/halls/{hall_id}/interactionsRequest body
typestringrequiredInteraction type. Built-in types: reaction, chat, hand_raise, poll, custom. Any string is accepted — unknown types are forwarded as-is to clients.
payloadobjectrequiredArbitrary JSON payload. Shape is defined by the interaction type. See examples below.
sender_idstringParticipant ID of the user initiating this interaction. Included in the broadcast envelope so recipients know who sent it. Omit for server-initiated events.
tostring[]Target participant IDs. Omit or pass null to broadcast to all participants in the hall.
curl -X POST https://api.relay.dev/v1/halls/h_01j9x2kp3n/interactions \
-H "Authorization: Bearer rk_live_..." \
-H "Content-Type: application/json" \
-d '{
"type": "reaction",
"payload": { "emoji": "👍", "x": 0.42, "y": 0.75 },
"sender_id": "user_abc123"
}'Response
{
"sent": true,
"type": "reaction",
"broadcast": true,
"recipient_count": null,
"timestamp": "2025-04-05T10:00:00.000Z"
}recipient_count is null for broadcasts (all participants) and a number when to is specified.
Interaction types
Reaction
Floating emoji or thumbs-up overlay. Broadcast to all participants.
{
"type": "reaction",
"payload": {
"emoji": "🔥",
"x": 0.42,
"y": 0.75
},
"sender_id": "user_abc123"
}Chat message
In-hall text chat. Broadcast to all or targeted at specific participants.
{
"type": "chat",
"payload": {
"text": "Great presentation!",
"display_name": "Alice"
},
"sender_id": "user_abc123"
}Hand raise
Signals that a participant wants to speak. The host can lower it by sending the same event with raised: false.
{
"type": "hand_raise",
"payload": {
"raised": true
},
"sender_id": "user_abc123"
}Poll
Server-initiated polls. Send the question and options from your backend; collect responses via a poll_response interaction sent from clients.
{
"type": "poll",
"payload": {
"poll_id": "poll_001",
"question": "How are you finding the session so far?",
"options": ["Great", "Good", "Could be better"],
"duration_seconds": 30
}
}Custom
Use custom (or any string type) for application-specific events. Relay forwards the payload as-is.
{
"type": "scene_change",
"payload": {
"scene": "breakout",
"breakout_room_id": "br_42"
}
}The interaction envelope
Regardless of how an interaction originates (server API or client SDK), every message delivered to participants shares the same envelope format:
{
"relay_version": "1",
"type": "reaction",
"sender_id": "user_abc123",
"timestamp": "2025-04-05T10:00:00.000Z",
"payload": {
"emoji": "👍",
"x": 0.42,
"y": 0.75
}
}relay_versionProtocol version. Currently "1".
typeThe interaction type string.
sender_idParticipant ID of the sender. null for server-initiated events.
timestampISO 8601 UTC timestamp of when the interaction was sent.
payloadThe interaction payload as provided in the request.
Receiving interactions on the client
Use the LiveKit JS SDK to listen for incoming data messages in your frontend. Parse the envelope and dispatch based on the type field.
import { Room, RoomEvent } from "livekit-client";
const room = new Room();
await room.connect(livekitUrl, joinToken);
room.on(RoomEvent.DataReceived, (data: Uint8Array, participant) => {
const envelope = JSON.parse(new TextDecoder().decode(data));
switch (envelope.type) {
case "reaction":
showReaction(envelope.payload.emoji, envelope.payload.x, envelope.payload.y);
break;
case "chat":
appendChatMessage(envelope.sender_id, envelope.payload.text);
break;
case "hand_raise":
updateHandRaise(envelope.sender_id, envelope.payload.raised);
break;
case "poll":
showPoll(envelope.payload);
break;
default:
console.log("custom interaction", envelope.type, envelope.payload);
}
});Sending interactions from the client
All join tokens include can_publish_data: true, so participants can also send interactions directly from the browser without going through your backend. Use the same envelope format so your existing handler works for both server and client-originated events.
const encoder = new TextEncoder();
function sendReaction(room: Room, emoji: string) {
const envelope = {
relay_version: "1",
type: "reaction",
sender_id: room.localParticipant.identity,
timestamp: new Date().toISOString(),
payload: { emoji, x: Math.random(), y: Math.random() },
};
room.localParticipant.publishData(
encoder.encode(JSON.stringify(envelope)),
{ reliable: true }
);
}