Skip to main content

Rooms with the Dart SDK

Room lifecycle and moderation are covered by two SDK surfaces:

  • client.rooms — the typed REST facade (create, list, join grants, moderation, recordings).
  • client.joinRoom() / RoomHandle — the high-level path that joins a room, wires signaling + media, and gives you live events and in-room moderation methods.

Most apps only need joinRoom() plus a few client.rooms calls.

Create, list, get, delete

final created = await client.rooms.create(CreateRoomRequest(
name: 'Team Standup',
slug: 'team-standup', // optional
maxParticipants: 10, // optional (default 50)
));
final room = created.room; // Room: id, name, slug, status, ...

final page = await client.rooms.list(status: 'active', limit: 20, offset: 0);
// page.data -> List<Room>, page.pagination -> PaginationMeta

final one = await client.rooms.get(room.id); // throws NotFoundException on 404
await client.rooms.delete(room.id); // soft-delete

Join a room

The recommended path is the client-level orchestration:

final handle = await client.joinRoom(roomId,
options: const JoinOptions(audio: true, telemetry: true));

handle.events.listen((event) {
switch (event) {
case RosterSnapshot(:final participants):
// full roster (also available any time as handle.participants)
case ParticipantJoined(:final participant):
// participant.displayName, .role, .muted, .speaking
case ParticipantLeft(:final participant):
// ...
case RoomClosing():
// the room is shutting down
default:
break;
}
});

joinRoom() calls the join endpoint, opens the signaling WebSocket, waits for the hello handshake, connects LiveKit media with the server-provisioned ICE servers, starts QoS telemetry, and requests an initial roster — then returns the RoomHandle.

If you need the raw join grant (for example, to drive media yourself), call the facade directly:

final grant = await client.rooms.join(roomId);
// grant.sessionId, grant.livekitToken, grant.livekitUrl,
// grant.signalUrl, grant.signalTicket, grant.iceServers

Leave a room

await handle.leave();

leave() stops telemetry, disconnects signaling (sending the bye frame) and media, notifies the server (POST /v1/rooms/{id}/leave, best-effort), and closes the event stream. If you joined via the facade instead, call client.rooms.leave(roomId) yourself.

Participants and roster

// Live roster, maintained from signaling events:
final roster = handle.participants; // List<RosterParticipant>

// One-off REST query (outside a joined room):
final participants = await client.rooms.listParticipants(roomId);

Prefer the live roster over polling — RosterSnapshot, ParticipantJoined, and ParticipantLeft events keep it current (see Signaling).

Moderation

From inside a room, RoomHandle has host/moderator methods:

await handle.muteParticipant(userId, reason: 'Background noise');
await handle.kickParticipant(userId, reason: 'Disruptive');
await handle.setParticipantRole(userId, 'speaker');
await handle.requestSpeak(); // listener raises hand

The full moderation set — including unmute, ban/unban — is on the facade:

await client.rooms.muteParticipant(roomId, userId, MuteRequest(reason: 'Background noise'));
await client.rooms.unmuteParticipant(roomId, userId);
await client.rooms.kickParticipant(roomId, userId, KickRequest(reason: 'Disruptive'));
await client.rooms.banParticipant(roomId, userId, BanRequest(reason: 'Repeated violations'));
await client.rooms.unbanParticipant(roomId, userId);
await client.rooms.setParticipantRole(roomId, userId, SetRoleRequest(role: RoomRole.speaker));
await client.rooms.requestSpeak(roomId);

Moderation actions arrive to affected clients as room events: Muted, Unmuted, RoleChanged, SpeakRequested.

Recordings

final rec = await handle.startRecording(); // or client.rooms.startRecording(roomId)
await handle.stopRecording(); // or client.rooms.stopRecording(roomId)
final recs = await client.rooms.listRecordings(roomId); // List<Recording>

Rooms emit RecordingStarted / RecordingStopped events so every participant can reflect recording state in the UI.

Endpoints without a facade method

A few room-service endpoints do not have a dedicated facade method yet; use the authenticated raw client:

// Pending speak requests (hosts):
final reqs = await client.httpClient.get('/rooms/$roomId/speak-requests');

// Moderation audit log:
final log = await client.httpClient.get('/rooms/$roomId/moderation-log');

// Recording consent policy:
await client.httpClient.put('/tenant/recording-consent',
body: {'consent_mode': 'all_consent'}); // all_consent | host_only | no_consent
await client.httpClient.put('/rooms/$roomId/recording-consent',
body: {'consent_mode': 'host_only'}); // per-room override

Under the hood

The facade maps one-to-one onto the room-service REST contract (all endpoints require Authorization: Bearer <JWT>):

SDK callEndpoint
rooms.create(req)POST /v1/roomsname (required), slug, max_participants; returns 201.
rooms.list(...)GET /v1/rooms?status=&limit=&offset=
rooms.get(id) / rooms.delete(id)GET / DELETE /v1/rooms/{id}
rooms.join(id)POST /v1/rooms/{id}/join
rooms.leave(id)POST /v1/rooms/{id}/leave
rooms.listParticipants(id)GET /v1/rooms/{id}/participants
rooms.muteParticipant(...) etc.POST /v1/rooms/{id}/participants/{uid}/mute / unmute / kick / ban / unbanmute, kick, ban take {"reason": "..."}.
rooms.setParticipantRole(...)POST /v1/rooms/{id}/participants/{uid}/role{"role": "host" | "speaker" | "listener"}.
rooms.requestSpeak(id)POST /v1/rooms/{id}/request-speak
rooms.startRecording(id) / stopRecording(id) / listRecordings(id)POST /v1/rooms/{id}/recordings, POST .../recordings/stop, GET .../recordings

The join response is the credential bundle for everything real-time:

{
"session_id": "…", // UUID for signaling session resume
"livekit_token": "…", // media token — see Media page
"livekit_url": "…", // LiveKit SFU URL
"signal_url": "…", // WebSocket signaling URL
"signal_ticket": "…", // one-time ticket (st_…) — see Signaling page
"ice_servers": [ ] // STUN/TURN configuration (server-provisioned)
}